The vulnerabilities that actually show up
Most breaches that make the news do not use sophisticated technique. They use one of these six flaws, and all six have a known, cheap fix.
There is a large gap between the security that shows up at conferences and the security that protects you. The first is about exploit chains and new technique. The second is about an endpoint that did not check whether that user was allowed to see that record.
This article is about the second.
Before the flaws: what changes the game
Two ideas worth more than any tool.
Attack surface. Everything through which somebody can get in: every endpoint, every parameter, every dependency, every port, every person with access. Reducing surface is the most effective and cheapest measure there is: an endpoint that does not exist cannot be exploited.
STRIDE, to think systematically: Spoofing (pretending to be someone else), Tampering (altering data), Repudiation (denying you did it), Information disclosure (leaking), Denial of service (taking it down), Elevation of privilege (becoming admin).
Run any feature through those six letters and you will find things you would not have thought of. It takes fifteen minutes.
1. Broken authorisation (IDOR)
You change the number in the URL (/orders/1042 to /orders/1043) and see somebody else's order.
It is the most common flaw there is. It is not sophisticated; a curious user finds it by accident.
The root cause is almost always the same: authorisation was checked in the UI, not on the server. The menu does not show it, the button does not appear, and nobody remembered that the route accepts any id.
The rule, with no exceptions: every query filters by owner.
Not SELECT * FROM orders WHERE id = ?
But SELECT * FROM orders WHERE id = ? AND customer_id = ?
Two extra defences: row-level security in the database, if it supports it, enforces the rule at the lower layer even when somebody forgets it at the upper one. And a non-sequential identifier (UUID, ULID) removes accidental discovery: it is mitigation, not a solution.
The cousin: mass assignment. You accept the whole request body and dump it into the object. The
user throws in "role": "admin". Defence: an explicit list of the fields allowed to come from the
client.
2. Injection
The root cause is always the same: user data being interpreted as code.
In SQL, the defence is a parameterised query. It is not escaping strings: it is parameterising, always. The database receives the query structure and the values separately, and a value never becomes a command.
And the same principle applies everywhere else: system commands (use the form that takes a list of arguments, never concatenation into a shell), templates (never render a template that came from the user), LDAP, XPath.
And today, the prompt of a language model, which is the same problem in new clothes, with the added problem that there is no equivalent to a parameterised query. I come back to this at the end.
3. XSS
The attacker's code running in the victim's browser, with the victim's session.
Layered defence:
Escape on output, by context. HTML, HTML attributes, JavaScript and URLs all escape differently.
Modern frameworks do this by default, and the danger lives in the escape hatches:
dangerouslySetInnerHTML, v-html, raw templates.
Content-Security-Policy. It limits where scripts can come from. A well configured CSP turns an exploitable XSS into a harmless one. It is the defence with the best payoff and the most ignored.
4. CSRF
The victim's browser makes an authenticated request they never intended, because the cookie goes along automatically.
Modern defence: SameSite on the cookie (Lax already solves most of it; Strict is safer and breaks
some flows), plus an anti-CSRF token on state-changing operations.
And note: if your API uses a token in the Authorization header instead of a cookie, it is not
vulnerable to CSRF, because the browser does not send that header automatically. It is one of the few
real technical arguments in favour of tokens over cookies.
5. SSRF
You make the server fetch a URL supplied by the user, and it points inside your network.
The classic cloud target is the metadata service at 169.254.169.254, which returns the instance's
credentials. That is exactly how the Capital One incident happened, in 2019, with over a hundred
million records exposed.
Defences:
→ An allow list of destinations, rather than a block list.
→ Block internal ranges, and resolve the name before validating, because a domain the attacker controls can point at an internal IP.
→ On AWS machines, require IMDSv2, which uses a token and is not reachable with a simple request.
→ Short timeouts and no automatic redirect following.
Where it shows up without you noticing: PDF generation from a URL, image import by link, configurable webhooks, link previews.
6. Deserialisation and uploads
Insecure deserialisation: turning user data into objects can execute code, depending on the library and the language. Never deserialise a binary format from an untrusted source. Prefer JSON with a validated schema.
File upload: validate the type by content, not by extension and not by Content-Type. Store it
outside the directory the web server serves. Serve it from a separate domain, so that a malicious HTML
upload does not run on your application's domain. Limit the size.
- Authorisation (IDOR)the most commonChange the id in the URL and see somebody else's record. Every query filters by owner, no exceptions.
- Injectiondata becomes codeParameterised queries, always. It applies to SQL, shell, templates, LDAP and prompts.
- XSScode in the victimEscape by context on output. A well set CSP turns an exploitable XSS into a harmless one.
- CSRFthe cookie tags alongSameSite plus an anti-CSRF token. An API with a header token is not vulnerable.
- SSRFthe server fetchesAllow list, block internal ranges, IMDSv2. This was the Capital One case.
- Deserialisation and uploadsobjects and filesNever deserialise untrusted binary. Validate uploads by content and serve them from another domain.
The three things worth more than the six
Secrets. They do not go in the repository, nor in an environment variable that shows up in logs, nor in the Dockerfile. They go in a vault, with rotation. Run a scanner like gitleaks in the pipeline: it finds what has already leaked. And if it leaked, the only answer is to rotate: deleting it from git history does not help, somebody already cloned it.
Supply chain. Your dependencies are strangers' code running with your privileges. Pin versions, use a lockfile, generate a component inventory, scan for known vulnerabilities. The xz-utils incident in 2024 (a maintainer cultivated over two years to insert a backdoor) shows the limits of tooling, but pinned versions and reproducible builds limit the damage.
Least privilege. In the cloud, in the database, in the container. The question to ask in every review: if this component is compromised, what can the attacker do? If the answer is "everything", the problem is not the vulnerability: it is the design.
Prompt injection: the new entry on the list
It deserves a paragraph because it changes the risk calculation of any system with AI in it.
The root cause is the same as SQL injection: instruction and data in the same channel. The difference is that there is no parameterised query for natural language. There is no complete solution today.
And the dangerous variant is the indirect one: the malicious content is in a document your RAG retrieves, on a page your agent reads. The attacker never spoke to your system.
What helps: delimiting untrusted content, giving tools minimum privilege, requiring human confirmation for irreversible actions, validating output with code, and limiting the blast radius of the action.
The mental rule: treat model output as untrusted user input.
The one hour exercise
Get three people together:
10 min: draw the feature's data flow, marking the trust boundaries.
30 min: run each boundary through STRIDE, writing everything down without filtering.
15 min: sort by likelihood times impact.
5 min: pick the top three and create tasks with owners.
You need no tool and no consultant, and it catches more than any automated scan.
Read this next
- EngineeringStep 19Tests worth what they costTests do not exist to prove the code is right. They exist so you can change it tomorrow without fear. That change of goal reorganises everything.Read article
- EngineeringStep 18OAuth2, OIDC and JWT demystifiedThe three most confused acronyms in authentication, what each one solves, and the mistakes that show up in almost every codebase.Read article
- EngineeringStep 16Diagnose the network in ten minutesA six command routine that turns "it must be the network" into "it is layer X, on hop Y".Read article