OWASP Top 10 (2021) -- Web Application Security Checklist with Code Examples
What is the OWASP Top 10 checklist with code examples?
TL;DR
- Bottom line: A01 Broken Access Control and A03 Injection are the most common web application vulnerabilities; the 2021 list introduced 3 new categories (Insecure Design, Software Integrity Failures, SSRF) and promoted access control flaws to #1.
- Key tool/command:
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t https://your-app.com - Watch out for: Treating OWASP Top 10 as a compliance checklist rather than a risk-awareness document -- passing all 10 does not guarantee a secure application.
- Works with: Any web application stack (Node.js, Python, Java, .NET, Go, PHP, Ruby); language-specific mitigations vary but principles are universal.
Constraints
- The OWASP Top 10 is a risk-awareness document, not a compliance standard -- passing all 10 does not mean your application is secure
- The current official version is OWASP Top 10:2021 (released September 2021); a 2025 draft exists but is not yet ratified
- OWASP Top 10 focuses on web applications only -- mobile, API, and cloud have separate OWASP projects
- Automated scanners (ZAP, Burp) only detect a subset of the Top 10 -- A04 Insecure Design and A08 Integrity Failures require manual review
- Never rely solely on client-side validation for any OWASP category -- all security controls must be enforced server-side
Quick Reference
| # | Vulnerability | Risk | Vulnerable Code | Secure Code |
|---|---|---|---|---|
| A01 | Broken Access Control | Critical | if (user.role === 'admin') // client-side only | authorize(req.user, resource, 'write') // server-side RBAC |
| A02 | Cryptographic Failures | High | md5(password) or http:// for sensitive data | bcrypt.hash(password, 12) + TLS 1.2+ everywhere |
| A03 | Injection (SQL, XSS, CMD) | Critical | db.query("SELECT * FROM users WHERE id=" + id) | db.query("SELECT * FROM users WHERE id=$1", [id]) |
| A04 | Insecure Design | High | No rate limiting on login; no threat model | Threat model in design phase; rate limit + account lockout |
| A05 | Security Misconfiguration | High | Default admin credentials; verbose errors in prod | Harden configs; disable stack traces; remove default accounts |
| A06 | Vulnerable Components | High | "lodash": "^3.0.0" (unpinned, known CVE) | npm audit fix; pin versions; Dependabot enabled |
| A07 | Auth Failures | High | session.maxAge = null (no expiry) | Short-lived sessions + MFA + bcrypt + constant-time compare |
| A08 | Software/Data Integrity | High | <script src="cdn/lib.js"> (no SRI) | <script src="..." integrity="sha384-..." crossorigin> |
| A09 | Logging Failures | Medium | No logging of failed logins or access denials | Structured logging of auth events with alerting |
| A10 | SSRF | High | fetch(req.body.url) (unvalidated) | Allowlist URLs; block private IP ranges; DNS checks |
Decision Tree
START
|-- Is the app handling user authentication/sessions?
| |-- YES --> Check A07 (Auth Failures) + A01 (Broken Access Control) first
| +-- NO (static site / read-only API) --> Skip A07, focus on A03 + A05
|
|-- Does the app accept user input (forms, APIs, file uploads)?
| |-- YES --> Prioritize A03 (Injection) + A01 (Access Control)
| +-- NO --> Focus on A02 (Crypto) + A05 (Misconfiguration)
|
|-- Does the app fetch external resources or URLs?
| |-- YES --> Check A10 (SSRF) + A08 (Integrity Failures)
| +-- NO --> Skip A10
|
|-- Does the app use third-party packages/libraries?
| |-- YES --> Check A06 (Vulnerable Components) -- run npm audit / pip audit
| +-- NO --> Skip A06
|
+-- DEFAULT --> Run full OWASP ZAP baseline scan + manual code review
Step-by-Step Guide
1. Run an automated baseline scan
Start with OWASP ZAP to identify low-hanging fruit across all 10 categories. [src4]
# Run ZAP baseline scan against your application
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
-t https://your-app.com \
-r report.html
Verify: Open report.html in a browser -- expected: HTML report with findings categorized by risk level.
2. Audit access control (A01)
Review every endpoint for authorization checks. Ensure deny-by-default at the middleware level. [src1]
// Express.js: middleware-level authorization
function authorize(requiredRole) {
return (req, res, next) => {
if (!req.user || !req.user.roles.includes(requiredRole)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.delete('/api/users/:id', authorize('admin'), deleteUser);
Verify: curl -X DELETE /api/users/1 -H "Authorization: Bearer <non-admin>" -- expected: 403 Forbidden.
3. Eliminate injection vectors (A03)
Replace all string-concatenated queries with parameterized queries. Run SAST to find remaining instances. [src2]
# Find potential SQL injection in Node.js codebase
grep -rn "query\s*(" --include="*.js" --include="*.ts" | grep -v "\$\|?\|@"
# Run Semgrep with OWASP rules
npx semgrep --config=p/owasp-top-ten .
Verify: npx semgrep --config=p/owasp-top-ten . -- expected: no critical injection findings.
4. Scan dependencies for known vulnerabilities (A06)
Run dependency audits in your CI/CD pipeline to catch vulnerable components before deployment. [src2]
# Node.js
npm audit --audit-level=high
# Python
pip install pip-audit && pip-audit
# Java/Maven
mvn org.owasp:dependency-check-maven:check
Verify: npm audit -- expected: found 0 vulnerabilities.
5. Review cryptographic practices (A02)
Ensure passwords use bcrypt/scrypt/argon2, all traffic uses TLS 1.2+, and no sensitive data is stored in plaintext. [src1]
# Check TLS configuration
nmap --script ssl-enum-ciphers -p 443 your-app.com
Verify: Output shows only TLS 1.2+ with AES-GCM or ChaCha20 cipher suites.
6. Implement security logging and monitoring (A09)
Configure structured logging for all authentication events, access denials, and input validation failures. [src2]
// Structured security event logging
const securityLog = {
event: 'AUTH_FAILURE',
timestamp: new Date().toISOString(),
ip: req.ip,
userAgent: req.headers['user-agent'],
username: req.body.username, // never log passwords
reason: 'invalid_credentials'
};
logger.warn(securityLog);
Verify: grep "AUTH_FAILURE" /var/log/app.log | tail -5 -- expected: structured JSON entries for each failed login.
Code Examples
Node.js/Express: Broken Access Control (A01) -- Vulnerable vs Secure
// VULNERABLE: No ownership check -- any user can read any doc
app.get('/api/documents/:id', async (req, res) => {
const doc = await db.query('SELECT * FROM docs WHERE id=$1', [req.params.id]);
res.json(doc.rows[0]);
});
// SECURE: Server-side ownership verification
app.get('/api/documents/:id', authenticate, async (req, res) => {
const doc = await db.query(
'SELECT * FROM docs WHERE id=$1 AND owner_id=$2',
[req.params.id, req.user.id]
);
if (!doc.rows[0]) return res.status(404).json({ error: 'Not found' });
res.json(doc.rows[0]);
});
Node.js/Express: Injection (A03) -- Vulnerable vs Secure
// VULNERABLE: String concatenation in SQL query
const result = await db.query(
`SELECT * FROM users WHERE name = '${req.query.name}'`
);
// SECURE: Parameterized query
const result = await db.query(
'SELECT * FROM users WHERE name = $1',
[req.query.name]
);
Node.js/Express: Authentication Failures (A07) -- Vulnerable vs Secure
// VULNERABLE: Weak session config
app.use(session({
secret: 'keyboard cat',
cookie: { secure: false },
resave: true
}));
// SECURE: Hardened session config
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: { secure: true, httpOnly: true, sameSite: 'strict', maxAge: 1800000 },
resave: false,
saveUninitialized: false
}));
Python/Flask: Access Control (A01) -- Vulnerable vs Secure
# VULNERABLE: No authorization check
@app.route('/admin/users', methods=['DELETE'])
def delete_user():
user_id = request.args.get('id')
db.execute('DELETE FROM users WHERE id = %s', (user_id,))
return jsonify(success=True)
# SECURE: Role-based authorization decorator
@app.route('/admin/users', methods=['DELETE'])
@login_required
@require_role('admin')
def delete_user():
user_id = request.args.get('id')
db.execute('DELETE FROM users WHERE id = %s', (user_id,))
return jsonify(success=True)
Python/Flask: SSRF Prevention (A10) -- Vulnerable vs Secure
# VULNERABLE: Unvalidated URL fetch
@app.route('/fetch-url')
def fetch_url():
url = request.args.get('url')
response = requests.get(url) # Can access internal services!
return response.text
# SECURE: URL allowlisting + IP validation
import ipaddress, urllib.parse, socket
ALLOWED_HOSTS = {'api.example.com', 'cdn.example.com'}
@app.route('/fetch-url')
def fetch_url():
url = request.args.get('url')
parsed = urllib.parse.urlparse(url)
if parsed.hostname not in ALLOWED_HOSTS: abort(400)
if parsed.scheme != 'https': abort(400)
ip = socket.gethostbyname(parsed.hostname)
if ipaddress.ip_address(ip).is_private: abort(400)
return requests.get(url, timeout=5).text
Anti-Patterns
Wrong: Client-side access control
// BAD -- authorization check in the browser; user can edit localStorage
if (localStorage.getItem('role') === 'admin') {
showAdminPanel();
}
Correct: Server-side access control
// GOOD -- server enforces authorization on every request
app.get('/admin', authorize('admin'), (req, res) => {
res.json(getAdminData());
});
Wrong: Storing passwords with MD5/SHA
# BAD -- fast hash, trivially reversible with rainbow tables
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()
Correct: Using bcrypt with cost factor
# GOOD -- slow adaptive hash, salt built-in
import bcrypt
password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
Wrong: Trusting user-supplied URLs without validation
// BAD -- allows SSRF to internal services
const response = await fetch(req.body.url);
Correct: URL allowlisting with DNS verification
// GOOD -- validate scheme, host, and resolved IP
const url = new URL(req.body.url);
if (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error('Blocked');
if (url.protocol !== 'https:') throw new Error('HTTPS required');
const { address } = await dns.resolve4(url.hostname);
if (isPrivateIP(address)) throw new Error('Private IP blocked');
Wrong: Inline scripts without integrity checks
<!-- BAD -- no integrity verification -->
<script src="https://cdn.example.com/lib.js"></script>
Correct: Subresource Integrity (SRI)
<!-- GOOD -- browser verifies hash before executing -->
<script src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/..."
crossorigin="anonymous"></script>
Common Pitfalls
- Treating OWASP Top 10 as compliance: Organizations "pass" an assessment but miss application-specific risks. Fix: Use as a starting point, then perform threat modeling. [src1]
- Only running automated scans: ZAP finds injection and misconfiguration but misses insecure design entirely. Fix: Combine automated scanning with manual code review. [src4]
- Ignoring A06 in CI/CD: Teams add
npm auditbut ignore warnings or misconfigure flags. Fix:npm audit --audit-level=high && exit 1in CI. [src2] - Logging passwords and tokens: Security logging accidentally includes sensitive data. Fix: Exclude
password,token,cookiefields from log serialization. [src2] - DNS rebinding bypass for SSRF: Validating hostname but not resolved IP allows rebinding attacks. Fix: Resolve DNS first, validate IP, then connect using resolved IP. [src3]
- Applying web Top 10 to APIs: Misses API-specific risks like BOLA and mass assignment. Fix: Use OWASP API Security Top 10 (2023) for REST/GraphQL. [src1]
Diagnostic Commands
# Run OWASP ZAP baseline scan (Docker)
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t https://your-app.com
# Scan Node.js dependencies for known CVEs (A06)
npm audit --audit-level=high
# Scan Python dependencies (A06)
pip-audit --strict
# Run Semgrep with OWASP rules (A03 Injection + more)
npx semgrep --config=p/owasp-top-ten .
# Scan Docker images for vulnerabilities (A06)
docker scout cves your-image:latest
# Check TLS configuration (A02)
nmap --script ssl-enum-ciphers -p 443 your-app.com
# Scan with Snyk (A06 + A03)
npx snyk test --severity-threshold=high
# Check HTTP security headers (A05)
curl -sI https://your-app.com | grep -iE "strict-transport|content-security|x-frame|x-content-type"
Version History & Compatibility
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| OWASP Top 10:2021 | Current | 3 new categories; 4 renamed; XSS merged into A03 | Map 2017 A4 (XXE) to 2021 A05; A8 to A08; A10 to A09 |
| OWASP Top 10:2017 | Superseded | Added Insecure Deserialization (A8), Insufficient Logging (A10) | XSS was standalone A7; now part of Injection A03 |
| OWASP Top 10:2013 | Legacy | -- | First to include Components with Known Vulnerabilities |
| OWASP Top 10:2025 (Draft) | Draft | Proposed AI/LLM risk updates | Not ratified -- do not use as authoritative yet |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Starting a new web app security program | Building mobile-only applications | OWASP Mobile Top 10 (2024) |
| Training developers on common vulnerabilities | Securing REST/GraphQL APIs | OWASP API Security Top 10 (2023) |
| Performing security code reviews | Meeting specific compliance requirements | PCI DSS Req 6, ISO 27001 A.14 |
| Setting up security scanning in CI/CD | Evaluating cloud infrastructure | OWASP Cloud-Native Top 10 |
| Threat modeling during design phase | Network-level penetration testing | OWASP Testing Guide |
Important Caveats
- The OWASP Top 10 is updated roughly every 3-4 years; the 2021 edition is current as of February 2026 but a 2025 draft is under community review. Always check owasp.org for the latest official version.
- Vulnerability prevalence varies by industry and tech stack. Financial services see more A01 (Access Control) issues while e-commerce sees more A03 (Injection). Prioritize based on your specific risk profile.
- The Top 10 categories overlap significantly: SQL injection (A03) often exploits broken access control (A01) and may be enabled by security misconfiguration (A05). Treat categories as perspectives, not independent checklists.
- Code examples use Node.js/Express and Python/Flask for illustration. The same principles apply to all frameworks; consult the OWASP Cheat Sheet Series for framework-specific guidance.