Vihaan Reddy
@vihaan_reddy • 3 days ago
Review a code snippet for security flaws. Each finding names the CWE, the exact attacker request and the fixed code, plus what was checked and found safe.
untrusted_sourcescodecontext{{untrusted_sources}}{{code}}{{context}}untrusted_sources: query strings, path parameters, and any URL a user submits
context: Flask app behind nginx on AWS EC2, Postgres on RDS, multi-tenant (each customer only sees their own invoices). Logged-in users are customers of our SaaS.
code:
```python
@app.route("/invoices/<invoice_id>/download")
@login_required
def download_invoice(invoice_id):
filename = request.args.get("file", f"{invoice_id}.pdf")
path = os.path.join("/srv/app/invoices", filename)
return send_file(path)
@app.route("/api/link-preview")
@login_required
def link_preview():
url = request.args["url"]
resp = requests.get(url, timeout=5)
title = re.search(r"<title>(.*?)</title>", resp.text, re.S)
return {"title": title.group(1) if title else None, "status": resp.status_code}
@app.route("/search")
def search():
q = request.args.get("q", "")
rows = db.execute(f"SELECT id, name FROM products WHERE name ILIKE '%{q}%'").fetchall()
return f"<h2>Results for {q}</h2>" + "".join(f"<p>{r.name}</p>" for r in rows)
```GET /search?q=' UNION SELECT id, email || ':' || password_hash FROM users-- dumps the users table into the results page, with no login needed.`python`GET /invoices/1/download?file=../../../etc/passwd reads arbitrary files. ?file=/srv/app/.env works too: os.path.join discards the base when the second argument is absolute. Separately, ?file=.pdf (or just another invoice_id) downloads another tenant's invoice, because nothing checks ownership.`python`file parameter entirely.GET /api/link-preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ asks your server to fetch the EC2 metadata service. If the instance allows IMDSv1, the response's status and any leak information, and internal services on the VPC become reachable. Varying the host and port turns the status field into a port scanner. What decides severity: whether IMDSv2 is enforced (HttpTokens=required) and what's reachable inside the VPC.http/https, resolve the hostname and reject private, loopback and link-local addresses, set allow_redirects=False (or re-check every hop), and cap the body size by streaming. Better, run previews from an isolated worker with no VPC access./search?q= runs in the victim's browser. Product names are also unescaped (stored XSS if sellers can edit them).`python`resp.text loads any size of page into memory. Use stream=True and read at most about 1 MB.login_required is present on the download and preview routes./search being public looks intentional, and fine once 1 and 4 are fixed.