Common web security bugs and gotchas — a field reference
Every bug in this lesson has already been explained mechanically somewhere earlier in this domain — this is the field-reference version, the shape each one actually takes in real code, so it's recognizable on sight instead of requiring the mechanism to be re-derived from scratch every time.
3 min read
Bug 1: string concatenation building a database query
query = f"SELECT * FROM users WHERE id = {user_id}"Covered mechanically in the SQL-injection lesson: the database can't distinguish intended query structure from structure smuggled in through unvalidated input. The fix: parameterized queries, always — cursor.execute(query, (user_id,)).
Bug 2: rendering untrusted content without escaping
<div dangerouslySetInnerHTML={{ __html: userComment }} />Covered mechanically in the XSS lesson: content interpreted as HTML instead of displayed as text executes with full page privileges. The fix: let the framework's default escaping do its job — reach for an explicit HTML-rendering escape hatch only when the content is genuinely trusted, never for raw user input.
Bug 3: a login-required check standing in for an ownership check
@login_required
def get_invoice(request, invoice_id):
return jsonify(db.get_invoice(invoice_id)) # no check that THIS user owns THIS invoiceCovered mechanically in the broken-access-control lesson: authentication (who is this) is not authorization (is this specific user allowed to access this specific resource) — this is IDOR. The fix: an explicit ownership or permission check on every single access to a directly-referenced resource, not just once somewhere earlier in the request.
Bug 4: a blocklist checking for known-bad patterns instead of an allowlist
if "127.0.0.1" not in url and "localhost" not in url:
fetch(url) # bypassed by DNS rebinding, alternate IP encoding, or a redirect chainCovered mechanically in both the SSRF and open-redirects lessons: a blocklist checking known-bad patterns is fundamentally more fragile than an allowlist checking known-good ones, since an attacker only needs to find one pattern the blocklist's author didn't think of. The fix: resolve to the actual final destination (DNS-resolved IP, final redirect target) and validate that against an allowlist, not the surface-level input string.
Bug 5: trusting the client-provided filename for an uploaded file
file.save(f"/app/uploads/{file.filename}") # client-controlled — could be "../../etc/passwd" or "shell.php"Covered mechanically in the path-traversal-and-file-uploads lesson: a client-supplied filename can escape the intended directory via ../, or land an executable file somewhere the server will run it. The fix: generate a new, random filename server-side, validate the extension against an allowlist, and store uploads where the server never executes files from.
Bug 6: a secret hardcoded directly in source code
STRIPE_SECRET_KEY = "sk_live_51H8x..." # committed to git — permanently in HISTORY, even if later "removed"Covered mechanically in the secrets-management lesson (and this platform's Node.js domain): a hardcoded secret is recoverable from git history indefinitely, and gets scraped by bots scanning public repositories specifically for exposed keys. The fix: process.env (or the equivalent), with the actual value living only in the deployment environment, never in the repository at all.
The actual throughline across all six
Every one of these traces back to the same handful of principles this domain already covered in depth: untrusted input needs explicit validation at every trust boundary, authentication is never a substitute for authorization, and an allowlist of known-good patterns beats a blocklist of known-bad ones almost every time. Recognizing a bug's shape on sight — "this smells like a missing ownership check," "this smells like a blocklist that a determined attacker will route around" — is what separates catching a real vulnerability in review from discovering it the way an attacker eventually would.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is a blocklist checking for known-bad patterns (specific IPs, hostnames, file extensions) a recurring, fragile fix across multiple vulnerability types in this domain?
2. Why does a @login_required check alone fail to prevent unauthorized access to a specific resource by ID?
3. What's the actual throughline connecting all six bugs in this field-reference lesson?