A working engineer's tour of the OWASP Top 10, each risk paired with the concrete fix that actually closes it in production.
The OWASP Top 10 gets cited in every pentest report and half the security-vendor pitches, usually as a checklist someone waves at an auditor. That's a waste. It's the most reliable map we have of where real applications actually break, ranked by how often it shows up in breach data. Below is the 2021 list, which is still the current stable version, with the fix that closes each one. The 2025 revision is in draft and reshuffles a few categories, but nothing here goes stale.
This is a hub. Each category links to a deeper post where one exists, and it all rolls up into our application security best practices guide.
A user reaches data or actions that should be off-limits, usually by changing an ID in the URL or calling an endpoint the UI never exposed. It's the number one category because authorization is enforced per-endpoint and it's easy to miss one. The fix: deny by default and check ownership on every server-side request, never trust a client-supplied role or object ID. See API security best practices.
# Don't just fetch by ID from the URL — scope it to the caller
invoice = Invoice.query.filter_by(
id=invoice_id,
org_id=current_user.org_id, # ownership check, server-side
).first_or_404()
Sensitive data is exposed because it wasn't encrypted, or was protected with something weak like MD5 or a hardcoded key. It matters because this is the category behind most "passwords leaked in plaintext" headlines. The fix: TLS everywhere in transit, AES-256 or equivalent at rest, and password hashing with bcrypt, scrypt, or Argon2, never a raw hash.
Untrusted input gets interpreted as code or a query, letting an attacker read your database or run commands. SQL injection is the classic, but the same flaw covers OS command, LDAP, and NoSQL injection. The fix: parameterized queries and safe APIs, so user input is always data and never syntax.
# Parameterized — the driver escapes the value, not string concat
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
Deeper dives: preventing SQL injection and, for the browser side of injection, preventing XSS.
The vulnerability is baked into the architecture, not a coding mistake, like a password-reset flow that leaks whether an account exists. It matters because you can't patch your way out of a bad design; the flaw survives every code review. The fix: threat model before you build, and design in controls like rate limits and abuse cases. See threat modeling.
Default credentials, verbose error pages, open cloud buckets, or missing hardening on a framework that shipped permissive out of the box. It's common because every layer has knobs and the safe setting is rarely the default. The fix: hardened baselines, least-privilege config, and response headers that lock down browser behavior. See HTTP security headers.
You're running a library or base image with a known CVE, often several dependencies deep where nobody looks. Most of your codebase is code you didn't write, so this is where a lot of real exposure lives. The fix: maintain an inventory, scan continuously, and patch on a schedule instead of when something explodes. See dependency and SCA scanning.
Weak passwords, no brute-force protection, guessable session tokens, or credential stuffing that walks right in. It matters because a broken login undoes every other control you built. The fix: enforce MFA, rate-limit and lock out on failed attempts, and use a battle-tested session framework rather than rolling your own. See authentication and session security.
Your code or data is trusted without verifying it wasn't tampered with, think an unsigned auto-update or a CI pipeline pulling an unpinned dependency. This is the supply-chain category, and it's the vector behind SolarWinds-style attacks. The fix: verify signatures, pin and hash dependencies, and protect the build pipeline as if it were production, because it is.
An attack happens and nobody sees it, because logins, access-control denials, and server errors aren't logged or nobody's watching. It matters because dwell time is measured in months when detection is broken. The fix: log security-relevant events with enough context to trace an incident, ship them somewhere tamper-resistant, and alert on the patterns that matter.
The server is tricked into making a request to a URL an attacker controls, often the cloud metadata endpoint at 169.254.169.254 to steal credentials. It's newly ranked because so much code fetches URLs on a user's behalf. The fix: validate and allowlist outbound destinations, block requests to internal and link-local ranges, and disable unused URL schemes.
Don't treat the Top 10 as a linear checklist you tick once a year. Use it as a coverage map. Take your real endpoints and ask, for each category, where in this system could this happen and what already stops it. Access control and injection will account for the bulk of your real findings, so weight your time there. Wire the mechanical categories (vulnerable components, misconfiguration) into CI so they're caught continuously instead of during an audit crunch, and reserve human review for the ones a scanner can't see: insecure design and broken access-control logic.
If you're starting cold, close A01 and A03 first. Parameterized queries and server-side ownership checks eliminate the two categories that show up most in actual breaches, and they're cheap. Then automate A06 and A05 in the pipeline so they stay closed without anyone remembering to look. The rest is real work, but that sequence buys down the most risk per hour spent, and it's the order we'd run it in on a system we owned.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A production-focused CircleCI guide: orbs, reusable commands and executors, workflows with fan-in/fan-out, contexts and OIDC for secrets, layered caching, test splitting, approval jobs, and dynamic config — with copy-paste examples.
A field-tested workflow for finding which process burns your CPU and whether the time is user, system, or iowait.
Explore more articles in this category
Run only the CI jobs a change actually affects using if conditionals, trigger path filters, and per-job path detection in a monorepo.
A prioritized toolkit for cutting CI time: measure the critical path first, then cache, parallelize, run only what changed, and shrink the work itself.
Stop stashing long-lived AWS access keys in GitHub secrets and let OIDC hand your workflows short-lived, scoped credentials instead.
Evergreen posts worth revisiting.