The OWASP Top 10 (2021)
The OWASP Top 10 is a community-driven awareness document from the Open Worldwide Application Security Project that ranks the most critical web application security risks. The 2021 edition remains the current baseline in 2026 and is organized around root-cause categories rather than individual bugs. This guide summarizes each category defensively: what it is, a concrete example, and the fix. Practice everything in dedicated labs (WebGoat, Juice Shop, PortSwigger Academy) — never against systems you do not own or have written authorization to test.
How to use the Top 10
Treat it as a checklist for threat modeling and code review, not an exhaustive list. Map each category to concrete controls in your stack, then verify with automated scanning plus manual testing in a lab.
A01 — Broken Access Control
The most common category. Users act outside their intended permissions: viewing others' records (IDOR), escalating role, or accessing admin functions. The root cause is trusting client-supplied identifiers or hiding functionality instead of enforcing authorization server-side.
// VULNERABLE (IDOR): any authenticated user reads any invoice by ID
app.get('/api/invoices/:id', requireLogin, async (req, res) => {
const inv = await db.invoice.findById(req.params.id);
res.json(inv);
});
// SECURE: scope every query to the authenticated principal
app.get('/api/invoices/:id', requireLogin, async (req, res) => {
const inv = await db.invoice.findOne({
id: req.params.id, ownerId: req.user.id, // server-side authz
});
if (!inv) return res.status(404).end();
res.json(inv);
});
Fix: deny by default, enforce ownership/role checks on the server for every request, and prefer opaque or per-user-scoped identifiers.
A02 — Cryptographic Failures
Sensitive data exposed through missing or weak cryptography: plaintext transport, weak/absent password hashing, hard-coded keys, or outdated algorithms (MD5, SHA1, DES).
| Weak | Modern replacement |
|---|---|
| MD5/SHA1 for passwords | Argon2id (or bcrypt/scrypt) |
| HTTP / mixed content | TLS 1.3 + HSTS |
| DES / ECB mode | AES-256-GCM (authenticated) |
| Keys in source code | KMS / secrets manager |
A03 — Injection
Untrusted input is interpreted as code or a command by an interpreter (SQL, NoSQL, OS, LDAP, ORM). SQL injection is the classic case.
// VULNERABLE: string concatenation into SQL
const q = "SELECT * FROM users WHERE email = '" + email + "'";
db.query(q);
// email = "' OR '1'='1" returns every row.
// SECURE: parameterized query / prepared statement
db.query('SELECT * FROM users WHERE email = ?', [email]);
// The driver sends data separately from code; input is never parsed as SQL.
Fix: always use parameterized queries or a safe ORM, validate input against allowlists, and escape only as a last resort in contexts parameterization cannot cover.
A04 — Insecure Design
Flaws baked into the architecture before a line of code is written — missing rate limits, no threat model, business logic that trusts the client. You cannot patch an insecure design with better implementation. Fix: threat model early, define security requirements and abuse cases, use secure design patterns and reference architectures.
A05 — Security Misconfiguration
Default accounts, verbose error messages, unnecessary features enabled, missing security headers, open cloud storage. Fix: harden with a repeatable baseline, disable defaults, automate configuration (IaC), minimize the attack surface, and verify with configuration scanners.
A06 — Vulnerable & Outdated Components
Using libraries, frameworks, or runtimes with known CVEs. Fix: maintain an inventory/SBOM, run dependency scanning in CI, remove unused dependencies, and patch on a defined cadence.
npm audit --production # Node
pip-audit # Python
osv-scanner -r . # multi-ecosystem (Google OSV)
# Wire these into CI and fail the build on known-high CVEs.
A07 — Identification & Authentication Failures
Weak credentials, missing brute-force protection, broken session handling, credential stuffing exposure. Fix: enforce strong password policies checked against breach lists, add MFA, apply rate limiting and account lockout/backoff, and regenerate session IDs on login.
A08 — Software & Data Integrity Failures
Trusting code or data whose integrity is not verified: unsigned updates, compromised CI/CD pipelines, untrusted plugins, and insecure deserialization that lets attacker-controlled objects execute code. Fix: verify signatures and checksums, pin and lock dependencies (integrity hashes), secure the build pipeline (SLSA), and avoid deserializing untrusted data — prefer plain data formats like JSON with schema validation.
A09 — Security Logging & Monitoring Failures
Without adequate logging and alerting, breaches go undetected. Fix: log authentication events, access-control failures, and input-validation failures with enough context (never secrets/PII in cleartext), centralize logs, set alerts, and rehearse incident response.
Log the security-relevant, not the sensitive
Record who did what and whether it was allowed; never log passwords, tokens, or full card numbers. Ensure logs are tamper-resistant and retained long enough for investigations.
A10 — Server-Side Request Forgery (SSRF)
The server is coerced into making requests to unintended destinations — often internal services or the cloud metadata endpoint (169.254.169.254) — because it fetches a user-supplied URL without validation.
// SECURE: allowlist + block internal ranges + no redirects to internal
const ALLOWED_HOSTS = new Set(['images.example.com']);
const url = new URL(userUrl);
if (url.protocol !== 'https:' || !ALLOWED_HOSTS.has(url.hostname)) {
throw new Error('blocked');
}
// Resolve DNS and reject private/link-local IPs; disable redirects.
Fix: validate against a strict allowlist, block private/link-local/metadata ranges, disable unnecessary URL schemes and redirects, and enforce network egress controls (require IMDSv2 in AWS).
Summary: All 10 at a Glance
| Category | Typical cause | Primary fix |
|---|---|---|
| A01 Broken Access Control | Client-trusted IDs, no authz | Deny by default, server-side checks |
| A02 Cryptographic Failures | Weak/absent crypto | TLS 1.3, Argon2id, KMS |
| A03 Injection | Input parsed as code | Parameterized queries |
| A04 Insecure Design | No threat modeling | Design reviews, abuse cases |
| A05 Security Misconfiguration | Defaults, verbose errors | Hardened baseline, IaC |
| A06 Vulnerable Components | Outdated deps with CVEs | SBOM, dependency scanning |
| A07 Auth Failures | Weak creds, no brute-force limit | MFA, rate limiting |
| A08 Integrity Failures | Unsigned code, deserialization | Signatures, locked deps, no untrusted deser. |
| A09 Logging Failures | No visibility/alerting | Centralized logs + alerts |
| A10 SSRF | Unvalidated URL fetch | Allowlist, block internal ranges |
Practice Exercises
- Work through the access-control and injection lessons in WebGoat and the corresponding challenges in OWASP Juice Shop, then write the server-side fix for each.
- Take a deliberately vulnerable SQL query in a local test app and convert it to a parameterized query; confirm that classic
' OR '1'='1style input no longer alters results. - Add access-control tests (an owner and a non-owner user) to your test suite and verify a non-owner receives 403/404 for another user's resource (IDOR regression test).
- Run a dependency audit with
npm audit/osv-scanner, upgrade a flagged package, and re-run to confirm it is resolved. - Add security logging for login success/failure and access-control denials to a test app, then trigger events and confirm they appear with useful context (and no secrets).
- Complete the PortSwigger Web Security Academy SSRF labs, then implement an allowlist plus private-range blocking in a local URL-fetching endpoint and verify metadata-endpoint access is blocked.