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.
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.
Lesson outline
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 |
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
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))
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.
Cryptographic failures cover data exposed due to missing encryption, weak algorithms, improper key management, or cleartext transmission.
Common cryptographic failures
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
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
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.
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
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:
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
Key takeaways
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.
Questions? Discuss in the community or start a thread below.
Join DiscordSign in to start or join a thread.