The OWASP Top 10 is the most referenced document in application security — cited by PCI DSS, ISO 27001, SOC 2, and virtually every enterprise security questionnaire. Our web application penetration testing tests against all OWASP Top 10 categories. Pair with a source code security review for maximum coverage. Most development teams have heard of it. Far fewer have actually read it carefully, understood the nuance of each category, or implemented the right defences.
This guide gives your engineering team the technical depth to understand each category, recognise the patterns in code review, and implement effective mitigations. Examples are language-agnostic where possible, with specific patterns called out for Python/Flask, Node.js/Express, and Java/Spring.
A01:2021 — Broken Access Control
Moved up to #1 from #5. 94% of applications tested had some form of broken access control. This is the most prevalent vulnerability in modern web applications.
What It Is
Broken access control occurs when users can act outside of their intended permissions. Common patterns:
- Insecure Direct Object References (IDOR) — modifying a user ID or record ID in a URL/request to access another user’s data
- Missing function-level access control — admin endpoints accessible to regular users
- Privilege escalation — a normal user performing admin actions
- CORS misconfiguration — allowing untrusted origins to make authenticated requests
- JWT token tampering — modifying claims to elevate privileges
Attack Example
GET /api/invoices/12345 ← returns your invoice
GET /api/invoices/12346 ← returns another customer's invoice (IDOR)
The application retrieves the invoice by ID without checking whether the authenticated user owns that record.
Prevention
Always enforce authorisation at the server side, not just in the UI:
# BAD — trusting client-supplied user ID
@app.route('/api/invoices/<invoice_id>')
def get_invoice(invoice_id):
return Invoice.get(invoice_id) # no ownership check
# GOOD — enforce ownership
@app.route('/api/invoices/<invoice_id>')
@login_required
def get_invoice(invoice_id):
invoice = Invoice.get(invoice_id)
if invoice.user_id != current_user.id:
abort(403)
return invoice
Key controls:
- Default deny — users must be explicitly granted access, not implicitly allowed
- Use UUIDs instead of sequential integer IDs to make enumeration harder (defence in depth, not a primary control)
- Log and alert on access control failures — they indicate active probing or insider threat
- Implement RBAC (Role-Based Access Control) or ABAC (Attribute-Based) consistently across all endpoints
A02:2021 — Cryptographic Failures
Formerly “Sensitive Data Exposure.” The focus has shifted from the symptom (data exposed) to the cause (weak or missing cryptography).
What It Is
- Transmitting sensitive data over HTTP (not HTTPS)
- Storing passwords with weak hashing (MD5, SHA1, unsalted SHA256)
- Using deprecated cryptographic algorithms (DES, RC4, weak RSA key lengths)
- Hard-coded cryptographic keys or weak key generation
- Missing encryption at rest for sensitive data stores
Attack Example
An attacker with database access finds passwords hashed with MD5. Using precomputed rainbow tables, they crack 60% of passwords in minutes — giving them access to user accounts and enabling credential stuffing against other services.
Prevention
Password storage — use an adaptive hashing algorithm:
# BAD
import hashlib
hashed = hashlib.md5(password.encode()).hexdigest()
# GOOD (Python)
from passlib.hash import argon2
hashed = argon2.hash(password)
verified = argon2.verify(password, hashed)
Use Argon2id (preferred), bcrypt, or scrypt. Never MD5, SHA1, or plain SHA256 for passwords.
Sensitive data at rest:
- Encrypt PII, payment data, health records with AES-256-GCM
- Use envelope encryption (data encrypted with DEK, DEK encrypted with KEK in KMS)
- Never store secrets in source code — use AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault
TLS configuration:
- Enforce TLS 1.2+ (disable TLS 1.0/1.1)
- Use HSTS with a long max-age:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload - Regularly scan your TLS config with SSL Labs
A03:2021 — Injection
Still critical. SQL injection was #1 for over a decade. The category now includes SQL, NoSQL, OS command, LDAP, and template injection.
SQL Injection
# VULNERABLE — string concatenation
query = "SELECT * FROM users WHERE email = '" + user_input + "'"
# SAFE — parameterised query
cursor.execute("SELECT * FROM users WHERE email = %s", (user_input,))
Input ' OR '1'='1 in the vulnerable version returns all users. '; DROP TABLE users; -- destroys your database.
Command Injection
# VULNERABLE
import subprocess
filename = request.args.get('file')
subprocess.run(f"cat /uploads/{filename}", shell=True)
# Input: ../../etc/passwd → reads sensitive files
# Input: file.txt; rm -rf / → catastrophic
# SAFE
import subprocess, shlex
filename = request.args.get('file')
# Validate filename is alphanumeric, no path traversal
if not re.match(r'^[a-zA-Z0-9._-]+$', filename):
abort(400)
subprocess.run(["cat", f"/uploads/{filename}"]) # no shell=True
Prevention
- Use parameterised queries / prepared statements — the primary defence for SQL injection
- Use ORM frameworks (SQLAlchemy, Hibernate, Eloquent) — they parameterise by default
- Never pass user input to shell commands with
shell=Trueor equivalent - Apply input validation (allowlist, not blocklist) at all entry points
- Use stored procedures carefully — they can still be vulnerable if constructed dynamically
- Apply least-privilege to database accounts — your app DB user shouldn’t have DROP TABLE permissions
A04:2021 — Insecure Design
New category. Distinguishes design flaws from implementation bugs. You can implement an insecurely designed system perfectly and still be vulnerable.
What It Is
Design-level failures that no amount of code quality can fix:
- No rate limiting on authentication endpoints → brute force possible
- Password reset via easily guessed security questions
- Single-factor authentication for high-risk operations (wire transfers, account deletion)
- Business logic flaws (applying a discount code unlimited times, skipping payment steps in checkout flow)
- No separation between admin and user API surfaces
Prevention
- Threat modelling — before writing code, identify what an attacker wants and how they’d get it. Use STRIDE methodology.
- Security requirements in user stories — “As a user, I should be rate-limited to 5 login attempts per minute”
- Security design review before significant features are built
- Separate high-risk operations with additional authentication (re-enter password, MFA challenge)
A05:2021 — Security Misconfiguration
Up from #6. With complex cloud and container deployments, misconfiguration is endemic.
Common Patterns
- Default credentials left unchanged (admin/admin, guest/guest)
- Debug mode enabled in production (
DEBUG=Truein Django/Flask) - Verbose error messages exposing stack traces, database errors, version information
- Unnecessary features, ports, services left enabled
- Missing security headers
- Cloud storage permissions too permissive
Prevention
Security headers (add to every response):
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=()
Test with securityheaders.com — aim for an A rating.
Error handling:
# BAD — exposes stack trace
@app.errorhandler(500)
def server_error(e):
return str(e), 500 # reveals internal details
# GOOD — generic error, log internally
@app.errorhandler(500)
def server_error(e):
app.logger.error(f"Internal error: {e}", exc_info=True)
return jsonify({"error": "Internal server error"}), 500
A06:2021 — Vulnerable and Outdated Components
Up from #9. Log4Shell (CVE-2021-44228) — one library, 100+ million vulnerable systems — made this undeniable.
What It Is
- Using libraries, frameworks, or OS components with known CVEs
- Not tracking which components are in use (no SBOM)
- Not subscribing to security advisories for dependencies
Prevention
- Software Composition Analysis (SCA): Snyk, Dependabot, OWASP Dependency-Check, Socket
- SBOM generation:
syft,cyclonedx-tool— document everything in your dependency tree - Automated dependency updates: Dependabot PRs, Renovate bot — keep patches flowing automatically
- Subscribe to CVE feeds for your critical libraries (GitHub Security Advisories, NIST NVD)
- Container image scanning: Trivy, Snyk Container, ECR image scanning — scan base images and rebuild when CVEs are found
# GitHub Actions — automated Dependabot
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
A07:2021 — Identification and Authentication Failures
Down from #2, renamed. Covers broken authentication, session management, and credential handling.
What It Is
- Allowing weak passwords (password123, qwerty)
- No brute-force protection on login
- Insecure session token generation (predictable, not enough entropy)
- Sessions not invalidated on logout
- Credentials in URL parameters (logged by proxies, browsers, servers)
- Not using MFA for privileged actions
Prevention
Rate limiting login:
from flask_limiter import Limiter
limiter = Limiter(app, key_func=get_remote_address)
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
# ...
Session management:
- Generate session tokens with cryptographically secure random number generators
- Set
HttpOnly,Secure,SameSite=Stricton session cookies - Invalidate sessions server-side on logout (not just delete the cookie)
- Implement session timeouts (idle + absolute)
- Implement MFA using TOTP (Google Authenticator) or WebAuthn (passkeys)
A08:2021 — Software and Data Integrity Failures
New, includes former “Insecure Deserialization.” Supply chain attacks made this critical.
What It Is
- Deserialising untrusted data without validation (PHP unserialize, Java ObjectInputStream, Python pickle)
- CI/CD pipelines that pull dependencies without integrity verification
- Auto-updates that don’t verify cryptographic signatures
- CDN-hosted scripts without Subresource Integrity (SRI)
Prevention
Subresource Integrity for external scripts:
<!-- BAD — trusts whatever the CDN serves -->
<script src="https://cdn.example.com/library.js"></script>
<!-- GOOD — verifies hash -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-..."
crossorigin="anonymous">
</script>
Avoid deserialising untrusted data. If you must:
- Use safer alternatives (JSON instead of pickle/Java serialisation)
- Implement strict type checking before deserialisation
- Sandbox deserialisation in isolated processes
CI/CD integrity:
- Pin GitHub Actions to specific commit SHAs, not tags
- Sign container images (Docker Content Trust, Sigstore/Cosign)
- Verify package signatures (npm audit, pip —require-hashes)
A09:2021 — Security Logging and Monitoring Failures
Up from #10. The Verizon DBIR consistently finds that breaches take months to detect. Logging is the difference between catching an attacker in hours vs. discovering the breach years later.
What to Log
- Authentication events (success, failure, lockout)
- Access control failures (403 responses, especially repeated ones)
- Input validation failures on security-sensitive fields
- High-value transactions (transfers, permission changes, data exports)
- Session management events (login, logout, timeout)
What Not to Log
- Passwords, credentials, session tokens (even on failure)
- Full credit card numbers, SSNs, health data
- Any data that would make your logs a breach target themselves
Prevention
import logging, json
from datetime import datetime
def log_security_event(event_type, user_id, ip_address, success, details=None):
event = {
"timestamp": datetime.utcnow().isoformat(),
"event_type": event_type,
"user_id": user_id,
"ip_address": ip_address,
"success": success,
"details": details or {}
}
security_logger.info(json.dumps(event))
# Usage
log_security_event("login_attempt", user_id, request.remote_addr, success=False)
Feed security events to a SIEM. Alert on:
- 5+ failed logins from the same IP in 5 minutes
- Successful login after N failed attempts
- Access control failures exceeding threshold
- Admin actions outside business hours
A10:2021 — Server-Side Request Forgery (SSRF)
New, added directly based on industry survey. With cloud metadata services and internal service meshes, SSRF is now a critical attack vector.
What It Is
An attacker tricks the server into making HTTP requests to an attacker-controlled destination — typically:
- The cloud metadata endpoint (
http://169.254.169.254/latest/meta-data/) to steal IAM credentials - Internal services not exposed to the internet
- Other servers on the internal network
Attack Example
POST /api/fetch-preview
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/EC2Role"}
# Response contains temporary AWS credentials → full cloud account compromise
Prevention
from urllib.parse import urlparse
import ipaddress
ALLOWED_SCHEMES = {'http', 'https'}
BLOCKED_HOSTS = {'169.254.169.254', 'metadata.google.internal', '100.100.100.200'}
def validate_url(url):
parsed = urlparse(url)
# Check scheme
if parsed.scheme not in ALLOWED_SCHEMES:
raise ValueError("Invalid URL scheme")
# Resolve hostname and check for private/loopback/link-local
import socket
try:
ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
except Exception:
raise ValueError("Cannot resolve hostname")
if ip.is_private or ip.is_loopback or ip.is_link_local:
raise ValueError("Private/internal URLs not allowed")
if parsed.hostname in BLOCKED_HOSTS:
raise ValueError("Blocked host")
return url
Also:
- Enable IMDSv2 on AWS EC2 (requires session token, blocks most SSRF exploitation)
- Restrict outbound traffic from application servers with egress firewall rules
- Use a dedicated HTTP fetching service with its own network controls
Building OWASP Defence Into Your Development Process
Knowing the Top 10 is the start. Preventing them requires embedding security into development:
| Stage | Activity |
|---|---|
| Design | Threat modelling, security requirements |
| Code | Secure coding training, IDE plugins (Snyk, SonarLint) |
| Commit | Pre-commit hooks with SAST (semgrep, bandit) |
| CI/CD | SAST, SCA, container scanning in pipeline |
| Pre-production | DAST scanning (OWASP ZAP, Burp Suite) |
| Production | WAF, runtime monitoring, penetration testing |
| Post-incident | Lessons learned, control improvements |
CyberneticsPlus provides web application penetration testing based on OWASP methodology, PTES, and custom techniques from real-world attack experience. Our testers hold OSCP, LPT, and CEH certifications. Contact us to schedule an assessment.