Web Application Security (OWASP Top 10)
A developer-focused guide to the OWASP Top 10: injection, broken access control, XSS, insecure design, and how to prevent each vulnerability with code examples.
Web Application Security (OWASP Top 10)
Introduction
The OWASP Top 10 is a standard awareness document for web application security risks. This guide translates each risk into practical prevention techniques with code examples.
1. Broken Access Control
Risk: Users access resources or perform actions outside their permissions.
Prevention
# Bad: trusting a client-provided user ID
@app.route("/api/orders/<order_id>")
def get_order(order_id):
order = db.query(f"SELECT * FROM orders WHERE id = {order_id}")
return jsonify(order) # any user can see any order!
# Good: verify the authenticated user owns the resource
@app.route("/api/orders/<order_id>")
@login_required
def get_order(order_id):
order = Order.query.filter_by(
id=order_id,
user_id=current_user.id # enforce ownership
).first_or_404()
return jsonify(order.to_dict())
Checklist
- Deny by default — return 403 unless explicitly allowed
- Validate resource ownership on every request
- Disable directory listing and server-side path traversal
- Rate limit API access to prevent automated enumeration
2. Cryptographic Failures
Risk: Sensitive data is exposed through weak or missing encryption.
Prevention
# Bad: plaintext storage
user = {"ssn": "123-45-6789", "password": "abc123"}
# Good: hash passwords, encrypt PII
from bcrypt import hashpw, gensalt
from cryptography.fernet import Fernet
hashed_password = hashpw("user_password".encode(), gensalt())
cipher = Fernet(ENCRYPTION_KEY)
encrypted_ssn = cipher.encrypt("123-45-6789".encode())
Checklist
- Hash passwords with bcrypt, Argon2, or PBKDF2
- Encrypt sensitive data at rest (PII, health data, financial data)
- Use TLS 1.2+ for all data in transit
- Don’t cache sensitive data in client storage (localStorage for tokens)
3. Injection
Risk: Untrusted data is sent to an interpreter as part of a command or query.
SQL Injection Prevention
# Bad: string interpolation
query = f"SELECT * FROM users WHERE name = '{user_input}'"
# Good: parameterized queries
cursor.execute("SELECT * FROM users WHERE name = %s", (user_input,))
Command Injection Prevention
# Bad
os.system(f"convert {user_filename} output.png")
# Good: validate and whitelist
import subprocess
allowed_extensions = {".png", ".jpg", ".jpeg"}
if not any(user_filename.endswith(ext) for ext in allowed_extensions):
raise ValueError("Invalid file type")
subprocess.run(["convert", user_filename, "output.png"], check=True)
Checklist
- Use parameterized queries for all database access
- Escape special characters in LDAP, XML, and OS commands
- Validate and whitelist user input before using it in commands
4. Insecure Design
Risk: Missing or ineffective security controls in the application architecture.
Prevention
- Design with threat modeling from the start
- Validate business logic, not just syntax
- Implement anti-automation for sensitive flows (signup, password reset)
- Maintain a secure development lifecycle
# Anti-automation on password reset
def request_password_reset(email):
if rate_limiter.is_limited(f"reset:{email}"):
raise TooManyRequests("Try again later")
send_reset_email(email)
rate_limiter.increment(f"reset:{email}")
5. Security Misconfiguration
Risk: Incomplete or ad-hoc configurations, default accounts, unnecessary features.
Prevention Checklist
- Remove default accounts and credentials
- Disable unnecessary features, ports, and HTTP methods
- Send security headers (HSTS, X-Frame-Options, CSP)
- Keep all frameworks, libraries, and OS patches current
- Run in minimal privilege mode (not root)
6. Vulnerable and Outdated Components
Risk: Using components with known vulnerabilities.
Prevention
# Scan dependencies for known CVEs
npm audit
pip-audit
snyk test
# Pin versions and automate updates
# package.json
"dependencies": {
"express": "4.19.2" // exact version, not ^4.0.0
}
Checklist
- Maintain a software bill of materials (SBOM)
- Subscribe to security advisories for critical dependencies
- Remove unused dependencies (reduces attack surface)
- Test updates in staging before production
7. Identification and Authentication Failures
Risk: Authentication weaknesses allow credential stuffing, brute force, or session hijacking.
Prevention
# Multi-factor authentication
@app.route("/login", methods=["POST"])
def login():
user = authenticate(request.json)
if not user:
# Generic error to prevent user enumeration
raise Unauthorized("Invalid credentials")
if user.mfa_enabled:
session["pending_user_id"] = user.id
return {"mfa_required": True}
create_session(user)
return {"token": generate_jwt(user)}
Checklist
- Implement multi-factor authentication (MFA)
- Enforce strong password policies (length > 12)
- Rate limit login attempts
- Use secure session tokens (random, long, HttpOnly cookies)
- Invalidate sessions on password change
8. Software and Data Integrity Failures
Risk: Insecure deserialization, untrusted CI/CD pipelines, auto-updates without verification.
Prevention
# Bad: deserializing untrusted data with pickle
import pickle
data = pickle.loads(user_input) # arbitrary code execution!
# Good: use JSON with schema validation
import json
from marshmallow import Schema, fields
data = json.loads(user_input)
schema = UserSchema()
result = schema.load(data) # validates structure
Checklist
- Sign and verify serialized data integrity
- Verify CI/CD pipeline integrity (signed commits, immutable tags)
- Don’t auto-update without cryptographic verification
9. Security Logging and Monitoring Failures
Risk: Insufficient logging allows attackers to remain undetected.
Prevention
import logging
import structlog
logger = structlog.get_logger()
# Log security events with context
logger.info(
"user_login",
user_id=user.id,
ip_address=request.remote_addr,
user_agent=request.headers.get("User-Agent"),
success=True
)
Checklist
- Log all authentication events (success and failure)
- Log access control failures
- Log input validation errors
- Send alerts on suspicious patterns
- Ensure logs are tamper-resistant (append-only, centralized)
10. Server-Side Request Forgery (SSRF)
Risk: The server makes requests to unintended destinations based on user input.
Prevention
# Bad: user controls the URL
url = request.json.get("webhook_url")
requests.post(url, data=sensitive_data)
# Good: validate URL against allowlist
from urllib.parse import urlparse
ALLOWED_HOSTS = {"api.example.com", "hooks.slack.com"}
def safe_webhook_call(user_url, data):
parsed = urlparse(user_url)
if parsed.hostname not in ALLOWED_HOSTS:
raise ValueError("URL not in allowlist")
return requests.post(user_url, json=data)
Checklist
- Validate and whitelist outgoing request destinations
- Disable URL schemas you don’t need (file://, ftp://, gopher://)
- Use internal DNS resolvers that don’t expose internal services
Common Mistakes
- Thinking security is “done” after a single audit — it requires continuous effort
- Trusting user input for URL construction or file paths
- Storing secrets in source code or logs
- Ignoring security headers because “they’re just headers”
- Not logging authentication failures (missed brute-force detection)
Frequently Asked Questions
Q: Should I fix all OWASP Top 10 items before shipping? A: Address critical items (Access Control, Injection, Cryptographic Failures) before launch. Others can be phased in based on risk assessment.
Q: How often should I review the OWASP Top 10? A: The list updates every 3-4 years, but threats evolve continuously. Review your security posture quarterly.
Q: Is the OWASP Top 10 enough for compliance? A: It’s a starting point, not a complete security program. Add threat modeling, penetration testing, and secure coding training.