OWASP Top 10
The ten most critical web application security risks — SQL injection, XSS, broken access control, cryptographic failures, and more — with real-world examples and prevention patterns every engineer must know.
OWASP Top 10
The ten most critical web application security risks — SQL injection, XSS, broken access control, cryptographic failures, and more — with real-world examples and prevention patterns every engineer must know.
Most web application breaches exploit a small set of well-understood, preventable vulnerability classes. Developers who do not know these vulnerabilities write code that contains them.
SQL injection, XSS, broken access control, and insecure deserialization are found in production applications every day — often years after they were introduced. These are not exotic vulnerabilities; they are standard bugs that should never ship.
Every engineer on your team understands the top vulnerability patterns, recognises insecure code in review, and knows which framework features prevent each class. Security becomes part of the development instinct, not a separate audit.
What you'll learn
- A01 Broken Access Control is the #1 risk — deny by default, check ownership at query level, test with unprivileged accounts.
- A02 Cryptographic Failures — use TLS everywhere, Argon2id for passwords, never MD5/SHA1 for passwords, never roll your own crypto.
- A03 Injection — use parameterised queries / ORMs, never interpolate user input into SQL/shell commands.
- A06 Vulnerable Components — use SCA scanning (Snyk/Dependabot) and enforce patch SLAs.
- A07 Auth Failures — enforce MFA, secure session management, protect against credential stuffing.
- A09 Logging Failures — log authentication events, access failures, admin actions; alert on anomalies; never log credentials.
- A10 SSRF — validate and allowlist URLs, block IMDS access, disable redirects, use IMDSv2.
- The OWASP Top 10 is the minimum security knowledge baseline for every engineer writing web application code.
Lesson outline
OWASP Top 10 (2021) — Overview
OWASP (Open Web Application Security Project) publishes the Top 10 most critical web application security risks every 3–4 years, based on data from thousands of organisations and security researchers.
| Rank | Vulnerability | Key Risk | Prevention |
|---|---|---|---|
| A01 | Broken Access Control | 94% of applications tested had some form of broken access control | Deny by default; enforce on server; test with unprivileged accounts |
| A02 | Cryptographic Failures | Data exposed in transit or at rest due to weak/missing encryption | TLS everywhere; strong algorithms; never roll your own crypto |
| A03 | Injection (SQL, LDAP, OS, NoSQL) | Untrusted data sent to an interpreter | Parameterised queries; ORM; input validation; allowlists |
| A04 | Insecure Design | Missing security controls at design phase; no threat modelling | Threat modelling; secure design patterns; security requirements |
| A05 | Security Misconfiguration | Unnecessary features enabled; default passwords; verbose errors | Hardening guides; automated config scanning; minimal surface area |
| A06 | Vulnerable and Outdated Components | Using dependencies with known CVEs | SCA scanning; automated updates; SBOM |
| A07 | Identification and Authentication Failures | Weak passwords; no MFA; session management flaws | MFA; secure session handling; credential stuffing protection |
| A08 | Software and Data Integrity Failures | Unsigned updates; deserialization of untrusted data; CI/CD integrity | Sign artifacts; verify checksums; avoid unsafe deserialization |
| A09 | Security Logging and Monitoring Failures | Breaches not detected due to missing logs or alerts | Log security events; alert on anomalies; test detection coverage |
| A10 | Server-Side Request Forgery (SSRF) | Server fetches attacker-controlled URL, accessing internal services | Validate and allowlist URLs; disable redirects; block IMDS |
A01 — Broken Access Control: The #1 Risk
Broken access control moved to #1 in 2021. It means users can act outside their intended permissions — accessing other users' data, calling admin functions, or modifying access controls.
Common broken access control patterns
- IDOR (Insecure Direct Object Reference) — Changing an ID in a URL/request accesses another user's data — the same as BOLA in API security
- Force browsing — Directly accessing /admin, /backup, /config that should require elevated roles
- Missing function-level access control — DELETE /api/users/:id works for regular users even though only admins should delete users
- CORS misconfiguration — API allows requests from any origin (Access-Control-Allow-Origin: *) — allows cross-site request forgery from malicious sites
- Privilege escalation via JWT tampering — JWT signed with "none" algorithm or weak secret — attacker modifies role claim
Deny by default — explicitly grant, never assume
Every endpoint and resource should deny access by default. Access is granted explicitly via role checks or ownership checks. Test access control by running your test suite with unprivileged accounts and verifying that restricted operations return 403/404.
1# A03: SQL Injection — vulnerable vs safe (Python / SQLAlchemy)23# ❌ VULNERABLE — string interpolation in SQLVULNERABLE — f-string or + concatenation in SQL is always wrong4def get_user_by_name(username: str):5query = f"SELECT * FROM users WHERE username = '{username}'"6# Attacker sends: username = "admin' OR '1'='1"7# Query becomes: SELECT * FROM users WHERE username = 'admin' OR '1'='1'8# Returns all users!9return db.execute(query).fetchall()1011# ❌ ALSO VULNERABLE — string formatting12def search_products(category: str):13return db.execute(14"SELECT * FROM products WHERE category = '" + category + "'"15).fetchall()1617# ✅ SAFE — parameterised query (positional placeholder)SAFE — ? placeholder; driver handles escaping18def get_user_by_name_safe(username: str):19return db.execute(20"SELECT * FROM users WHERE username = ?",21(username,) # Driver escapes the value — injection impossible22).fetchall()ORM uses parameterised queries automatically2324# ✅ SAFE — ORM with parameterised queries under the hood25def get_user_by_name_orm(username: str):26return User.query.filter_by(username=username).first()2728# ✅ SAFE — SQLAlchemy text() with bound parameters29from sqlalchemy import text30def search_safe(category: str, min_price: float):31return db.execute(32text("SELECT * FROM products WHERE category = :cat AND price >= :price"),33{"cat": category, "price": min_price}34).fetchall()3536# BONUS: stored procedure input validation37import re38def validate_username(username: str) -> bool:39return bool(re.match(r'^[a-zA-Z0-9_]{3,50}$', username))
A03 — Injection: SQL, NoSQL, Command, LDAP
Injection occurs when untrusted data is sent to an interpreter as part of a command or query. The interpreter cannot distinguish data from instructions.
| Injection Type | Example Sink | Prevention |
|---|---|---|
| SQL | db.query("SELECT ... WHERE id = " + userId) | Parameterised queries, ORM |
| NoSQL | db.users.find({ username: req.body.username }) | Schema validation, $where avoidance, allowlisted operators |
| OS Command | exec("ping " + userInput) | Avoid shell execution; use language APIs; allowlist args |
| LDAP | ldap.search("(uid=" + username + ")") | Escape special chars: ( ) * \ NUL / @ = < > |
| XPath | xpath.select("//user[name=" + input + "]") | Parameterised XPath queries |
| Template injection (SSTI) | template.render(userInput) | Never pass user input as template string; use context only |
| Log injection | logger.info("User: " + username) | Sanitise log entries; encode newlines; structured logging |
NoSQL injection is real and commonly overlooked
MongoDB queries accept operators like $where, $gt, $ne. If user input is inserted directly into a query object — db.users.find({username: req.body.username, password: req.body.password}) — an attacker can send {"username": "admin", "password": {"$ne": null}} to bypass password checking. Use schema validation (Joi/Zod) and avoid $where.
A02 — Cryptographic Failures
Cryptographic failures cover data exposed due to missing encryption, weak algorithms, improper key management, or cleartext transmission.
Common cryptographic failures
- HTTP instead of HTTPS — Data in transit is visible to network observers. Use HTTPS everywhere, HSTS with preloading.
- MD5 or SHA1 for password hashing — Reversible via rainbow tables and GPU cracking. Use bcrypt, scrypt, or Argon2 with work factor.
- Storing passwords in plaintext or reversible encryption — Any breach immediately exposes all passwords. Always use one-way hashing.
- Weak TLS configuration — TLS 1.0/1.1, RC4, DES, export ciphers. Require TLS 1.2+ with strong cipher suites.
- Rolling your own crypto — Never implement cryptographic algorithms yourself. Use established libraries (libsodium, OpenSSL, AWS KMS).
- Hardcoded or weak encryption keys — Keys must be randomly generated, stored in a secrets manager, and rotated.
Password hashing: Argon2id is the 2024 recommendation
Argon2id won the Password Hashing Competition and is the current OWASP recommendation. Bcrypt is acceptable for legacy systems. Never use MD5, SHA1, SHA256, or SHA512 for passwords — these are fast hashes designed for data integrity, not password security.
1# A07: Password hashing — wrong vs right23# ❌ WRONG — MD5 (reversible via rainbow tables in seconds)MD5 — cracked in milliseconds with GPU + rainbow tables4import hashlib5hashed = hashlib.md5(password.encode()).hexdigest()67# ❌ WRONG — SHA256 (fast — GPU can try billions/sec)8hashed = hashlib.sha256(password.encode()).hexdigest()910# ❌ WRONG — plaintext storage (any DB breach = all passwords exposed)11user.password = password1213# ✅ CORRECT — bcrypt (slow by design, includes salt)bcrypt with rounds=12 — takes ~250ms deliberately (tunable)14import bcrypt15hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12))16# Verify:17bcrypt.checkpw(password.encode('utf-8'), hashed)18Argon2id — memory-hard, GPU-resistant, OWASP recommended19# ✅ BEST — Argon2id (OWASP recommended, memory-hard)20from argon2 import PasswordHasher21ph = PasswordHasher(22time_cost=2, # Iterations23memory_cost=65536, # 64 MB24parallelism=2,25hash_len=32,26salt_len=16,27)28hashed = ph.hash(password)29# Verify:30ph.verify(hashed, password) # Raises VerifyMismatchError if wrong
A09 — Security Logging and Monitoring Failures
Without proper logging and monitoring, breaches go undetected for months. The median time to detect a breach is 207 days (IBM 2023). Logging failures mean you cannot detect, respond, or learn from attacks.
What to log for security
- Authentication events — All login attempts (success and failure), MFA prompts, password resets, session creation and termination
- Authorisation failures — Every 403/404 from an authenticated user — patterns indicate access control bypass attempts
- Input validation failures — Rejected inputs (SQL injection patterns, XSS payloads, oversized payloads) indicate active probing
- Administrative actions — User creation/deletion, role changes, configuration changes, data exports
- High-value transactions — Payments, data exports, bulk operations — log before and after state
- Rate limit breaches — Which account, which endpoint, what rate was hit — essential for abuse detection
Never log sensitive data
Passwords, credit card numbers, full SSNs, session tokens, and API keys must never appear in logs — even accidentally. Log that authentication occurred, not the credential used. Implement log scrubbing for known sensitive patterns. Treat logs as sensitive data requiring the same access controls as production data.
A10 — SSRF: Server-Side Request Forgery
SSRF allows an attacker to make the server issue HTTP requests to destinations they control — including internal services, cloud metadata endpoints, and localhost services not exposed externally.
AWS/GCP/Azure instance metadata is the primary SSRF target
The cloud instance metadata service (IMDS) at 169.254.169.254 (AWS/Azure) or 169.254.169.254/metadata.google.internal (GCP) provides IAM credentials, instance identity documents, and configuration data. An SSRF vulnerability can leak these credentials, giving an attacker full cloud API access. Capital One 2019 breach was caused by SSRF → IMDS credential theft.
SSRF prevention
01
Validate all user-supplied URLs against an allowlist of permitted domains and schemes
02
Block requests to private IP ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16
03
Disable redirects — an attacker can redirect from an allowed domain to an internal IP
04
Use IMDSv2 (AWS) which requires a PUT request with a session token before the GET — prevents simple SSRF credential theft
05
Run the fetching service with minimal network access (no internal network reachability)
06
Log all outbound HTTP requests made by your application
Validate all user-supplied URLs against an allowlist of permitted domains and schemes
Block requests to private IP ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16
Disable redirects — an attacker can redirect from an allowed domain to an internal IP
Use IMDSv2 (AWS) which requires a PUT request with a session token before the GET — prevents simple SSRF credential theft
Run the fetching service with minimal network access (no internal network reachability)
Log all outbound HTTP requests made by your application
How this might come up in interviews
OWASP Top 10 knowledge is tested in security-focused engineering interviews, secure code review assessments, and penetration testing interviews. Every backend engineer should be able to explain at least A01, A02, A03.
Common questions:
- What is SQL injection and how do you prevent it?
- Name 3 items from the OWASP Top 10 and explain them.
- What is the difference between XSS and CSRF?
- How would you prevent SSRF in an application that fetches URLs provided by users?
- What is broken access control and how do you test for it?
Strong answer: Can explain BOLA/IDOR as A01, knows parameterised queries for A03, mentions Argon2 for A02 passwords, and understands SSRF impact on cloud metadata endpoints.
Red flags: Not knowing what SQL injection is, thinking HTTPS alone prevents all attacks, or confusing XSS with CSRF.
Quick check · OWASP Top 10
1 / 3
Which OWASP Top 10 vulnerability was the root cause of the Equifax 2017 breach?
Key takeaways
- A01 Broken Access Control is the #1 risk — deny by default, check ownership at query level, test with unprivileged accounts.
- A02 Cryptographic Failures — use TLS everywhere, Argon2id for passwords, never MD5/SHA1 for passwords, never roll your own crypto.
- A03 Injection — use parameterised queries / ORMs, never interpolate user input into SQL/shell commands.
- A06 Vulnerable Components — use SCA scanning (Snyk/Dependabot) and enforce patch SLAs.
- A07 Auth Failures — enforce MFA, secure session management, protect against credential stuffing.
- A09 Logging Failures — log authentication events, access failures, admin actions; alert on anomalies; never log credentials.
- A10 SSRF — validate and allowlist URLs, block IMDS access, disable redirects, use IMDSv2.
- The OWASP Top 10 is the minimum security knowledge baseline for every engineer writing web application code.
Before you move on: can you answer these?
What is the difference between stored XSS, reflected XSS, and DOM-based XSS?
Stored XSS: malicious script is saved in the database and served to every user who views the content (e.g., a comment field that stores <script>). Reflected XSS: malicious script is in the URL/request, reflected back in the response immediately without storage (e.g., a search page that echoes the search term unescaped). DOM-based XSS: the vulnerability is in client-side JavaScript that reads from the URL/localStorage and writes it to the DOM without sanitisation — the server never sees the payload. Prevention: output encoding for stored/reflected; use textContent not innerHTML for DOM-based.
Why is using MD5 for password hashing dangerous even with a salt?
MD5 is a fast hash — modern GPUs can compute billions of MD5 hashes per second. A salt prevents rainbow table attacks (pre-computed hash lookups), but it does not prevent brute force. With a salt, an attacker must brute force each password individually — but with MD5 and a GPU, billions of guesses per second means an 8-character password is cracked in seconds. Argon2id and bcrypt are intentionally slow (configurable work factor) — they are designed to make brute force computationally infeasible even with GPU acceleration.
From the books
OWASP Testing Guide (owasp.org)
The definitive reference for web application security testing — covers test cases for every OWASP Top 10 category with step-by-step methodology.
The Web Application Hacker's Handbook (Stuttard & Pinto)
Comprehensive guide to web application attack techniques. Understanding attacks from the attacker's perspective is the most effective way to write secure code.
💡 Analogy
The OWASP Top 10 is the driver's test for web security
⚡ Core Idea
Just as every driver must know what a stop sign means, every engineer writing web code must know what SQL injection, broken access control, and XSS mean — and be able to recognise them in code they write and review. These are not advanced hacking techniques; they are basic hygiene that every engineer is expected to understand.
🎯 Why It Matters
The same 10 vulnerability classes appear in breaches year after year. Equifax (A06), Capital One (SSRF/A10), Optus (A01), Yahoo (A02) — these were preventable with knowledge that is freely available. The OWASP Top 10 is the minimum baseline for any engineer writing production code.
Ready to see how this works in the cloud?
Switch to Career Paths for structured paths (e.g. Developer, DevOps) and provider-specific lessons.
View role-based pathsSign in to track your progress and mark lessons complete.
Discussion
Questions? Discuss in the community or start a thread below.
Join DiscordIn-app Q&A
Sign in to start or join a thread.