The OWASP Top 10 is the most widely referenced web application security standard. This guide translates each category into concrete code checks and fixes you can apply to your codebase today.
A01: Broken Access Control
The top-ranked category. Access control failures let users act outside their intended permissions.
Checklist:
- Authorization checks happen server-side on every request (not just at login)
- Horizontal access control: users can’t access other users’ resources by changing an ID
- Vertical access control: users can’t escalate to admin endpoints
- CORS policy explicitly allowlists origins — no
Access-Control-Allow-Origin: *on credentialed endpoints
# Vulnerable — trusts user-supplied ID
def get_document(doc_id):
return Document.objects.get(id=doc_id) # No ownership check
# Fixed — verify ownership
def get_document(doc_id, current_user):
doc = Document.objects.get(id=doc_id)
if doc.owner_id != current_user.id:
raise PermissionError("Access denied")
return doc
A02: Cryptographic Failures
Sensitive data exposed due to weak cryptography or missing encryption.
Checklist:
- Passwords hashed with bcrypt, Argon2, or scrypt (never MD5, SHA1, plain SHA256)
- TLS 1.2+ enforced; TLS 1.0/1.1 and SSL disabled
- Sensitive data encrypted at rest in the database
- No sensitive data in URL query parameters (ends up in logs and browser history)
# Vulnerable — SHA256 is too fast for passwords
import hashlib
hashed = hashlib.sha256(password.encode()).hexdigest()
# Fixed — bcrypt with work factor
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
A03: Injection
SQL, NoSQL, OS command, and LDAP injection when untrusted data reaches an interpreter.
Checklist:
- All SQL queries use parameterised statements
- No
os.system(),subprocess.call(shell=True)with user input - NoSQL queries use typed operators, not string interpolation
- HTML output is escaped (templating engine auto-escaping enabled)
// Vulnerable — shell injection
const { exec } = require('child_process');
exec(`convert ${userFile} output.pdf`); // userFile = "x; rm -rf /"
// Fixed — avoid shell, use array args
const { execFile } = require('child_process');
execFile('convert', [userFile, 'output.pdf']); // No shell expansion
A04: Insecure Design
Security flaws baked into the architecture rather than implementation bugs.
Checklist:
- Threat modeling performed before building new features
- Rate limiting on all authentication and sensitive endpoints
- Multi-factor authentication available for privileged actions
- Business logic validated server-side (e.g., can’t apply the same coupon twice)
# Rate limiting with Flask-Limiter
from flask_limiter import Limiter
@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
# ...
A05: Security Misconfiguration
Default credentials, overly verbose errors, unnecessary features enabled.
Checklist:
- Default credentials changed on all systems and services
- Production error responses don’t include stack traces
- Unnecessary HTTP methods disabled (TRACE, PUT if unused)
- Security headers set:
Content-Security-Policy,X-Frame-Options,X-Content-Type-Options
# Django production settings
DEBUG = False
ALLOWED_HOSTS = ['myapp.com']
# Security headers middleware
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
# ...
]
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
A06: Vulnerable and Outdated Components
Dependencies with known CVEs.
Checklist:
- Automated dependency scanning in CI (Dependabot, Snyk, OWASP Dependency-Check)
- Dependency lock files committed and used in production builds
- No EOL frameworks or runtimes in production
- Process for applying security patches within SLA (critical: 24h, high: 7 days)
# npm — audit and fix
npm audit
npm audit fix
# Python — check with safety
pip install safety
safety check -r requirements.txt
# GitHub Actions — automated PRs for updates
# .github/dependabot.yml
# version: 2
# updates:
# - package-ecosystem: "npm"
# directory: "/"
# schedule:
# interval: "weekly"
A07: Identification and Authentication Failures
Broken authentication, credential stuffing, weak session management.
Checklist:
- Account lockout or exponential backoff after failed logins
- Passwords checked against known breach lists (HaveIBeenPwned API)
- Session tokens are long, random, and invalidated on logout
- MFA available and enforced for admin accounts
# Check password against breach database
import hashlib
import requests
def is_pwned_password(password: str) -> bool:
sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
prefix, suffix = sha1[:5], sha1[5:]
response = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}")
return suffix in response.text
A08: Software and Data Integrity Failures
Insecure deserialization, unsigned updates, CI/CD pipeline integrity.
Checklist:
- No
pickle,marshal, or Java native serialization with untrusted data - Software update mechanisms verify cryptographic signatures
- CI/CD pipelines have integrity controls (pinned actions, artifact signing)
- Serialized objects passed in cookies are signed and verified
# Flask — use itsdangerous for signed cookies instead of pickle
from itsdangerous import URLSafeTimedSerializer
s = URLSafeTimedSerializer(app.secret_key)
# Sign
token = s.dumps({'user_id': 123}, salt='session')
# Verify — raises SignatureExpired or BadSignature on tampering
data = s.loads(token, salt='session', max_age=3600)
A09: Security Logging and Monitoring Failures
Attacks succeed because they go undetected for months.
Checklist:
- Authentication events logged (success, failure, lockout)
- Authorization failures logged with user context
- Log entries include timestamp, user ID, IP address, action
- Logs are immutable and shipped to external SIEM (attacker can’t delete them)
- Alerts configured for anomalous patterns (e.g., 100 failed logins/minute)
import logging
import structlog
logger = structlog.get_logger()
def login(username, password, ip_address):
user = authenticate(username, password)
if not user:
logger.warning(
"auth.failed",
username=username,
ip=ip_address,
reason="invalid_credentials"
)
return None
logger.info("auth.success", user_id=user.id, ip=ip_address)
return user
A10: Server-Side Request Forgery (SSRF)
The server fetches a URL supplied by the user, allowing access to internal services.
Checklist:
- URLs supplied by users are validated against an allowlist of schemes and hosts
- Internal IP ranges blocked in outbound request handlers
- Cloud metadata endpoints (169.254.169.254, fd00:ec2::254) explicitly denied
- Webhooks validated with signatures rather than IP allowlisting
import ipaddress
from urllib.parse import urlparse
BLOCKED_NETWORKS = [
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('169.254.0.0/16'), # Link-local / metadata
ipaddress.ip_network('127.0.0.0/8'),
]
def is_safe_url(url: str) -> bool:
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return False
try:
ip = ipaddress.ip_address(parsed.hostname)
return not any(ip in net for net in BLOCKED_NETWORKS)
except ValueError:
pass # Hostname, not IP — resolve and check
return True
Key Takeaways
The OWASP Top 10 is a starting point, not a complete security program. For each category:
- Fix broken access control by centralizing authorization logic and testing every endpoint.
- Use modern cryptography — bcrypt for passwords, TLS 1.3, AES-256 for data at rest.
- Parameterize everything that touches an interpreter.
- Automate dependency scanning — you cannot manually track CVEs across hundreds of packages.
- Log everything security-relevant and alert on anomalies.