Learn / Security

The OWASP Top 10, by shape

Lesson 25 of 37 · 12 min read ·

How to use this

The OWASP Top 10 is a list of the most impactful web vulnerability classes, updated every few years. Memorising the names is worthless. Recognising the shape in code is the skill — almost every one reduces to the same root cause: data from a user was treated as something more trusted than it was.

A01: Broken access control

The most common and most damaging. The code checks whether you are logged in but not whether this resource is yours.

// Authenticated. Also completely broken.
app.get('/api/orders/:id', requireAuth, async (req, res) => {
  res.json(await db.orders.findById(req.params.id))
})

Change the id in the URL and you read someone else's order. This is IDOR, and it is everywhere.

Fix: authorise on every request, scoped to the resource: WHERE id = ? AND user_id = ?. Deny by default. Never rely on the UI not showing a button — the API is the boundary.

A02: Cryptographic failures

Data that should be encrypted is not, or is protected with something broken. Passwords with SHA-256 and no salt, secrets over plain HTTP, MD5 anywhere, a homemade cipher.

Fix: TLS everywhere, argon2/bcrypt/scrypt for passwords, AES-GCM for data, and a library rather than your own construction. See encryption basics.

A03: Injection

User input is interpreted as code by an interpreter. SQL is the classic; command injection, LDAP and NoSQL injection are the same shape.

db.query(`SELECT * FROM users WHERE email = '${email}'`)
// email = "' OR '1'='1' --"

Fix: parameterised queries, always. The parameter is data and can never become syntax. This includes ORDER BY — if the column comes from user input, validate it against an allowlist, because you cannot parameterise an identifier.

XSS is injection into the browser. User input rendered into HTML executes as script. Fix with context-aware escaping (React and modern templating do this by default — until someone uses dangerouslySetInnerHTML or v-html), plus a Content-Security-Policy as the second line.

A04: Insecure design

The vulnerability is in the design, not the code. A password reset that lets you request unlimited codes. A refund flow with no maximum. Business logic nobody threat-modelled.

Fix: ask "how would I abuse this?" during design. Rate limits and hard business limits are security controls.

A05: Security misconfiguration

Default credentials, debug mode on in production, verbose stack traces to users, S3 buckets left public, admin panels reachable from the internet, missing security headers.

Fix: harden by default, disable what you do not use, and make production config differ from development deliberately rather than accidentally.

A06: Vulnerable and outdated components

Your code is a small fraction of what you ship. A CVE in a transitive dependency is your CVE.

Fix: npm audit / Dependabot in CI, patch promptly, remove dependencies you do not need. Every dependency is trust extended to a stranger.

A07: Identification and authentication failures

Credential stuffing with no rate limiting, weak password rules, session ids that do not rotate on login, tokens that never expire, password reset tokens that are guessable or reusable.

Fix: rate-limit and lock out on auth endpoints hardest of all, offer MFA, rotate the session on privilege change, expire and revoke properly. See OAuth, JWT and PKCE.

A08: Software and data integrity failures

Trusting code or data whose origin you cannot verify: an unpinned CI action, a package installed from a compromised registry, deserialising untrusted input into objects.

Fix: pin versions and digests, verify signatures, never deserialise untrusted data into arbitrary types.

A09: Security logging and monitoring failures

The breach that goes unnoticed for eight months. No record of failed logins, no alert on privilege changes, no way to reconstruct what an attacker did.

Fix: log auth events, access-control failures and admin actions — without logging the credentials themselves. See observability.

A10: Server-side request forgery (SSRF)

Your server fetches a URL supplied by a user. The attacker supplies http://169.254.169.254/latest/meta-data/ and your server obligingly returns its own cloud credentials.

Fix: allowlist destinations, block private IP ranges and link-local addresses, resolve the DNS name and validate the resolved IP (not just the string), disable redirects.

And CSRF, which still matters

A malicious site causes the user's browser to send an authenticated request to yours, using cookies it sends automatically.

Fix: SameSite=Lax cookies (now the browser default, and it handles most cases), anti-CSRF tokens for state-changing forms, and never mutate on GET. APIs authenticated with an Authorization header rather than cookies are not vulnerable — the browser does not attach headers automatically.

Prove you know it

Take one endpoint you have written and try to break it as a user who is logged in as someone else. Change the id in the URL. If you get data back that is not yours, you have just found A01 in your own code — which is exactly where most people find it.

Go deeper