Most web application vulnerabilities are not sophisticated. They are the same classes of mistake — SQL injection, broken access control, security misconfiguration — appearing in new codebases written by developers who learned to build features before they learned to build secure features.
The OWASP Top 10 documents these classes. It has been maintained since 2003 and updated regularly to reflect how the threat landscape and development practices change. Understanding it is not optional for any developer who ships code to users.
This guide explains each of the 10 categories in terms developers can act on: what the vulnerability is, what it looks like in real code, and what the correct implementation is.
The OWASP Top 10 (2021)
| Rank | Category | Core risk |
|---|---|---|
| A01 | Broken Access Control | Users access data or functions they should not |
| A02 | Cryptographic Failures | Sensitive data not protected in transit or at rest |
| A03 | Injection | User input alters the logic of queries or commands |
| A04 | Insecure Design | Security not considered in the design phase |
| A05 | Security Misconfiguration | Insecure defaults, unnecessary features, verbose errors |
| A06 | Vulnerable and Outdated Components | Dependencies with known CVEs in production |
| A07 | Identification and Authentication Failures | Weak session management, no MFA, credential stuffing |
| A08 | Software and Data Integrity Failures | Insecure deserialization, unsigned CI/CD pipelines |
| A09 | Security Logging and Monitoring Failures | No visibility into attacks in progress |
| A10 | Server-Side Request Forgery (SSRF) | Server fetches attacker-controlled URLs |
A01: Broken Access Control
Broken Access Control jumped to #1 in the 2021 update because it is now the most common finding in real penetration tests — present in 94% of applications tested.
What it looks like
// Vulnerable: fetches whatever order ID is in the URL
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order);
});
// Attacker changes /api/orders/1001 to /api/orders/1002
// and gets another user's order details
The fix
// Correct: scopes the query to the authenticated user
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id // ← mandatory ownership check
});
if (!order) return res.status(404).json({ error: 'Not found' });
res.json(order);
});
The check must happen at the data layer on every query that references a user-owned resource. There is no framework default that handles this — it requires deliberate implementation on every endpoint.
A02: Cryptographic Failures
Passwords in plaintext or with weak hashing
# Wrong: MD5 is fast to brute-force
import hashlib
hashed = hashlib.md5(password.encode()).hexdigest()
# Wrong: SHA-256 is also fast to brute-force
hashed = hashlib.sha256(password.encode()).hexdigest()
# Correct: Argon2id is slow and designed for passwords
from argon2 import PasswordHasher
ph = PasswordHasher()
hashed = ph.hash(password)
Sensitive data in transit without HTTPS
Every request that carries credentials, session tokens, personal data, or any sensitive value must use HTTPS. Redirect HTTP to HTTPS. Set HSTS with a long max-age. Test with curl -I https://yourdomain.com to confirm headers.
Sensitive data in logs
Never log passwords, tokens, credit card numbers, or government identifiers — even to internal systems. An internal log pipeline is not a security boundary.
A03: Injection
SQL injection remains the most severe injection class.
# Vulnerable: string concatenation with user input
query = f"SELECT * FROM users WHERE email = '{email}'"
cursor.execute(query)
# Attacker sends: email = "' OR '1'='1"
# Resulting query: SELECT * FROM users WHERE email = '' OR '1'='1'
# Returns all users
# Correct: parameterised query
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
The same principle applies to every system that executes instructions based on user input: LDAP queries, OS commands, XML parsers (XXE), and template engines (server-side template injection).
XSS (Stored and Reflected):
// Vulnerable in React: bypasses the framework's auto-escaping
<div dangerouslySetInnerHTML={{ __html: userComment }} />
// Safe: React's default rendering escapes HTML automatically
<div>{userComment}</div>
A04: Insecure Design
Insecure design is the only OWASP category that cannot be fixed by adding a security control after the fact — it requires revisiting the architecture.
Examples of design-level security failures:
- An API that signs requests client-side with a key embedded in the binary (the entire key can be extracted — see the Flutter reverse engineering research)
- A password reset flow that sends the new password in the email rather than a reset link
- An admin panel accessible from the same domain and port as the user-facing application
- A multi-tenant SaaS where tenant isolation is enforced only in the application layer, not at the database level
The fix is threat modelling: systematically identifying what can go wrong in a design before it is built, and making design choices that limit the blast radius of any single component being compromised.
A05: Security Misconfiguration
Common misconfiguration patterns
# Wrong: directory listing enabled
autoindex on;
# Wrong: verbose error pages showing stack traces
error_page 500 /internal-error.html; # served with full trace
# Wrong: default credentials unchanged
admin:admin, admin:password, root:root
Cloud misconfiguration is the most costly form in 2026: public S3 buckets, overly permissive IAM policies, unrestricted security groups, and unencrypted databases are responsible for the majority of large cloud data breaches.
Tools that detect misconfiguration automatically: AWS Security Hub, Azure Defender for Cloud, GCP Security Command Center for cloud resources; OWASP ZAP, Nikto for web application configuration.
A06: Vulnerable and Outdated Components
A single critical CVE in a widely used dependency can affect thousands of applications simultaneously. The Equifax breach (147 million records) was caused by an unpatched Apache Struts vulnerability that had a fix available for two months before the breach.
Automation is the only sustainable approach:
# GitHub Dependabot configuration
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
Prioritise CVSS Critical (9.0–10.0) and High (7.0–8.9) CVEs. Patch or mitigate within 24–72 hours for Critical, within one sprint for High.
A07: Identification and Authentication Failures
Secure session management
// Correct session cookie configuration (Express.js)
app.use(session({
secret: process.env.SESSION_SECRET, // long, random, from env
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // not accessible from JavaScript
sameSite: 'strict', // no cross-site requests
maxAge: 30 * 60 * 1000 // 30 minute expiry
}
}));
Rate limiting authentication endpoints
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per window
message: 'Too many login attempts',
standardHeaders: true,
});
app.post('/login', loginLimiter, loginHandler);
Secure password reset
The reset flow must: generate a cryptographically random token (not a predictable value like a timestamp), store its hash (not the token itself), set a short expiry (15–60 minutes), invalidate the token after single use, and send the token in the URL — never send a new password in the email.
A08: Software and Data Integrity Failures
This category covers two distinct risks:
Insecure deserialization: Untrusted data passed to a deserializer can execute arbitrary code if the deserializer is vulnerable. Never deserialise untrusted data with Java's native serialization, Python's pickle, or Ruby's Marshal. Use JSON with schema validation instead.
Unsigned CI/CD pipeline artifacts: If your build pipeline can be manipulated to include malicious code — through a compromised dependency, a tampered build server, or an unsigned artifact — the malicious code ships to production. Controls: verify dependency integrity (npm package-lock.json, pip hash verification), sign build artifacts, and use SLSA (Supply-chain Levels for Software Artifacts) framework controls.
A09: Security Logging and Monitoring Failures
Attacks that are not detected cannot be stopped. Logging must capture:
- Authentication events (success and failure) with user ID, IP, and timestamp
- Access control failures (403 responses with the resource attempted)
- Input validation failures (potentially malicious inputs)
- High-value transactions (payments, account changes, privilege escalations)
What not to log:
- Passwords, tokens, or secrets (even failed attempts — the attacker may have entered the correct value)
- Full credit card numbers or government identifiers
Set alerts for: > 10 failed authentication attempts per minute per IP, access control failures in bulk (IDOR probing pattern), and unusual spikes in 5xx error rates.
A10: Server-Side Request Forgery (SSRF)
SSRF occurs when a server makes an HTTP request to a URL supplied by the user — allowing an attacker to make the server fetch internal resources.
Attacker sends: POST /fetch-image
body: {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
Server fetches AWS instance metadata → attacker gets IAM credentials
Prevention:
- Validate and allowlist URLs before making server-side requests
- Block requests to private IP ranges (10.x.x.x, 172.16.x.x–172.31.x.x, 192.168.x.x, 169.254.x.x, 127.x.x.x)
- Use a dedicated egress proxy that enforces allowlisted domains
- Disable URL schemes other than HTTPS (
file://,gopher://,ftp://)
Security as a Development Practice
The OWASP Top 10 is not a one-time checklist. It is a framework for building security thinking into development practice:
- Threat model new features before they are built — what can go wrong?
- Include security requirements alongside functional requirements
- Run SAST (static analysis) on every commit
- Conduct access control reviews on every endpoint that handles user data
- Penetration test before major releases or new attack surface is exposed
For teams that want a professional security assessment — including active testing against the OWASP Top 10 — get in touch or explore our Mobile Security & Reverse Engineering service. Our API Security Best Practices guide covers the server-side API layer in detail.