OWASP Top 10 (2021) -- Web Application Security Checklist with Code Examples

Type: Software Reference Confidence: 0.92 Sources: 7 Verified: 2026-02-24 Freshness: annual

TL;DR

Constraints

Quick Reference

#VulnerabilityRiskVulnerable CodeSecure Code
A01Broken Access ControlCriticalif (user.role === 'admin') // client-side onlyauthorize(req.user, resource, 'write') // server-side RBAC
A02Cryptographic FailuresHighmd5(password) or http:// for sensitive databcrypt.hash(password, 12) + TLS 1.2+ everywhere
A03Injection (SQL, XSS, CMD)Criticaldb.query("SELECT * FROM users WHERE id=" + id)db.query("SELECT * FROM users WHERE id=$1", [id])
A04Insecure DesignHighNo rate limiting on login; no threat modelThreat model in design phase; rate limit + account lockout
A05Security MisconfigurationHighDefault admin credentials; verbose errors in prodHarden configs; disable stack traces; remove default accounts
A06Vulnerable ComponentsHigh"lodash": "^3.0.0" (unpinned, known CVE)npm audit fix; pin versions; Dependabot enabled
A07Auth FailuresHighsession.maxAge = null (no expiry)Short-lived sessions + MFA + bcrypt + constant-time compare
A08Software/Data IntegrityHigh<script src="cdn/lib.js"> (no SRI)<script src="..." integrity="sha384-..." crossorigin>
A09Logging FailuresMediumNo logging of failed logins or access denialsStructured logging of auth events with alerting
A10SSRFHighfetch(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

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

VersionStatusBreaking ChangesMigration Notes
OWASP Top 10:2021Current3 new categories; 4 renamed; XSS merged into A03Map 2017 A4 (XXE) to 2021 A05; A8 to A08; A10 to A09
OWASP Top 10:2017SupersededAdded Insecure Deserialization (A8), Insufficient Logging (A10)XSS was standalone A7; now part of Injection A03
OWASP Top 10:2013Legacy--First to include Components with Known Vulnerabilities
OWASP Top 10:2025 (Draft)DraftProposed AI/LLM risk updatesNot ratified -- do not use as authoritative yet

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Starting a new web app security programBuilding mobile-only applicationsOWASP Mobile Top 10 (2024)
Training developers on common vulnerabilitiesSecuring REST/GraphQL APIsOWASP API Security Top 10 (2023)
Performing security code reviewsMeeting specific compliance requirementsPCI DSS Req 6, ISO 27001 A.14
Setting up security scanning in CI/CDEvaluating cloud infrastructureOWASP Cloud-Native Top 10
Threat modeling during design phaseNetwork-level penetration testingOWASP Testing Guide

Important Caveats

Related Units