The bugs that actually ship
Injection, broken authz, SSRF, XSS, CSRF, deserialisation. For each: how it happens, how to spot it in a review, and the fix that removes the class, not the instance.
01Think in classes, not instances
- Recognise the six bug classes that cause most incidents
- Give the class-level fix for each, not a patch
- Spot them in a code review in under a minute each
A fix that escapes one parameter is a patch. A fix that makes the unsafe API unavailable is a class fix. Interviewers and good reviewers look for the second kind.
| Class | Root cause | Class-level fix |
|---|---|---|
| Injection (SQL, command, LDAP) | Data concatenated into a parser's input | Parameterised APIs only; lint-ban string-built queries |
| Broken object / function authz | Authz checked at the route, not the object | Scope every query by tenant/owner; central policy engine |
| SSRF | Server fetches a URL the user controls | Egress allow-list, metadata endpoint blocked, resolve-then-connect with IP checks |
| XSS | Untrusted data rendered as HTML | Auto-escaping templates, CSP with nonces, no innerHTML |
| CSRF | Browser sends cookies cross-site automatically | SameSite cookies + anti-CSRF token for state changes |
| Insecure deserialisation | Deserialising attacker-controlled objects | Data-only formats (JSON) with schemas; never native object deserialisation of untrusted input |
02SSRF: the cloud-era injection
Server-side request forgery is the bug behind several of the largest cloud breaches. A feature like "import from URL" or a webhook tester lets an attacker make your server call http://169.254.169.254/ and read cloud credentials, or scan the internal network.
// Vulnerable
const res = await fetch(req.body.url);
// Safer: resolve, check, then connect to the checked IP
const target = new URL(req.body.url);
if (!["https:"].includes(target.protocol)) throw new Error("scheme");
const ips = await dns.lookup(target.hostname, { all: true });
if (ips.some(({ address }) => isPrivateOrLinkLocal(address))) throw new Error("blocked range");
// Prefer: route all outbound fetches through an egress proxy with an allow-list,
// and enforce IMDSv2 (session tokens) so metadata cannot be read with a plain GET.Checking the IP then letting the HTTP client resolve again lets an attacker swap the answer between checks. Connect to the IP you validated, or use the egress proxy.
03A reviewer's checklist
- Where does untrusted input enter? Query, body, headers, file names, webhook payloads, third-party API responses.
- Where does it leave? Into SQL, a shell, HTML, a URL fetch, a log line, a file path.
- Is each exit through a safe API (parameterised, escaped, allow-listed)?
- Is every data read scoped to the caller?
- Are secrets absent from code, config files and logs?
- Do errors leak stack traces, SQL or internal hostnames?
"I'd fix this instance, then I'd ask why it was possible: we have a raw query helper exposed. I'd deprecate it, add a lint rule, and grep for the other call sites. That removes the class."
Which control removes the XSS class rather than one instance?