OWASP Top 10 Deep Dive
The 10 Most Critical Web Application Security Risks (2021)
Introduction
The OWASP Top 10 is the most widely referenced web application security standard. Published by the Open Web Application Security Project, it represents broad consensus about the most critical security risks to web applications. Security assessments, penetration tests, and secure code reviews all use the OWASP Top 10 as a baseline. This lesson covers each category with Python examples for the most impactful risks.
The OWASP Top 10 (2021)
Users can act outside their intended permissions, accessing other users' data, modifying unauthorized resources.
Sensitive data exposed due to weak/missing encryption, old algorithms, or poor key management.
Hostile data sent to an interpreter as part of a command or query (SQL, OS, LDAP injection).
Missing or ineffective control design, security not built into the architecture from the start.
Default credentials, verbose error messages, unnecessary features, missing security headers.
Using components with known vulnerabilities; failure to patch libraries and frameworks.
Weak passwords, credential stuffing, missing MFA, poor session management.
Code and infrastructure that doesn't protect against integrity violations; insecure CI/CD pipelines.
Insufficient logging, monitoring, and alerting, attackers operate undetected for extended periods.
Server fetches remote resources without validating user-supplied URLs, allows internal network access.
A01, Broken Access Control (Deep Dive)
Broken access control is the #1 risk. Users accessing resources they shouldn't, other users' data, admin endpoints, privileged actions, are all examples.
Install: pip install flask
# ❌ VULNERABLE, Insecure Direct Object Reference (IDOR)
# Flask route that trusts user-supplied IDs without authorization check
from flask import Flask, request, jsonify
app = Flask(__name__)
USERS = {
1: {"name": "Alice", "email": "alice@example.com", "salary": 95000},
2: {"name": "Bob", "email": "bob@example.com", "salary": 78000},
}
# VULNERABLE: Any authenticated user can access ANY user's data
@app.route("/api/user/<int:user_id>")
def get_user_vulnerable(user_id: int):
# Missing: check if current_user.id == user_id OR current_user.is_admin
user = USERS.get(user_id)
if not user:
return jsonify({"error": "Not found"}), 404
return jsonify(user) # ← Anyone can read anyone's salary!
# ✅ FIXED, Add authorization check
def get_current_user_id() -> int:
return int(request.headers.get("X-User-ID", 0)) # In reality: from JWT
@app.route("/api/user/<int:user_id>/secure")
def get_user_secure(user_id: int):
current_user_id = get_current_user_id()
# Only allow: accessing your own data OR admin role
is_admin = request.headers.get("X-User-Role") == "admin"
if current_user_id != user_id and not is_admin:
return jsonify({"error": "Forbidden"}), 403 # ← Explicit deny
user = USERS.get(user_id)
if not user:
return jsonify({"error": "Not found"}), 404
return jsonify(user)
print("IDOR fix: Always verify the current user owns (or can access) the requested resource")
print("Never trust user-supplied IDs directly, always enforce ownership checks")
A02, Cryptographic Failures (Deep Dive)
import hashlib
import bcrypt
from cryptography.fernet import Fernet
# ❌ WRONG, Plain MD5 password storage
def bad_store_password(password: str) -> str:
return hashlib.md5(password.encode()).hexdigest()
# 10 billion MD5 attempts/sec on modern GPU
# Rainbow tables exist for all common passwords
# Cracked in milliseconds
# ✅ CORRECT, bcrypt with proper cost factor
def good_store_password(password: str) -> bytes:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
# ~0.3s per attempt (300ms vs 0.0001ms for MD5)
# Built-in unique salt per hash
# GPU-resistant
bad = bad_store_password("password123")
good = good_store_password("password123")
print(f"MD5 (insecure): {bad}")
print(f"bcrypt (secure): {good.decode()[:40]}...")
# ❌ WRONG, Storing PII unencrypted in DB
bad_record = {"ssn": "123-45-6789", "user_id": 42} # plaintext
# ✅ CORRECT, Encrypt sensitive fields before storage
key = Fernet.generate_key()
f = Fernet(key)
good_record = {
"ssn_encrypted": f.encrypt(b"123-45-6789"),
"user_id": 42
}
print(f"\nPlain SSN: {bad_record['ssn']}")
print(f"Encrypted SSN: {good_record['ssn_encrypted'].decode()[:40]}...")
A05–A10, Quick Reference & Mitigations
A05Security Misconfiguration
- Change all default credentials before deploying
- Disable stack traces and verbose errors in production
- Remove unused features, endpoints, and services
- Add security headers: CSP, HSTS, X-Frame-Options
A06Vulnerable Components
- Run `pip audit` or `safety check` on every build
- Pin dependency versions in requirements.txt
- Subscribe to CVE alerts for critical packages
- Automate updates with Dependabot or Renovate
A07Auth Failures
- Enforce MFA for all admin and privileged accounts
- Rate-limit login attempts (max 5/min per IP)
- Use short-lived tokens with secure refresh flow
- Invalidate sessions on logout, password change, privilege change
A09Logging & Monitoring
- Log all authentication events (success + failure)
- Log all access control decisions
- Alert on repeated failures (brute force detection)
- Ensure logs are tamper-resistant and off-machine
A10SSRF
- Validate and allowlist URLs before fetching
- Block requests to 169.254.x.x (cloud metadata)
- Block requests to internal RFC1918 ranges
- Use network-level egress filtering
Python: Automated Security Checklist
# security_checklist.py, Automated OWASP compliance checker
import os
import subprocess
import sys
from dataclasses import dataclass
@dataclass
class CheckResult:
name: str
passed: bool
detail: str
def run_checks() -> list[CheckResult]:
results = []
# Check 1: No hardcoded secrets in env
dangerous_keys = ["PASSWORD", "SECRET", "API_KEY", "PRIVATE_KEY"]
found_secrets = [k for k in dangerous_keys if os.environ.get(k, "").startswith("hardcoded")]
results.append(CheckResult(
"No hardcoded secrets in ENV",
len(found_secrets) == 0,
f"Found: {found_secrets}" if found_secrets else "Clean",
))
# Check 2: DEBUG mode off
debug_mode = os.environ.get("DEBUG", "false").lower() == "true"
results.append(CheckResult(
"DEBUG mode disabled",
not debug_mode,
"DEBUG=true in production!" if debug_mode else "DEBUG=false",
))
# Check 3: requirements.txt exists
has_requirements = os.path.exists("requirements.txt")
results.append(CheckResult(
"requirements.txt present",
has_requirements,
"Found" if has_requirements else "Missing, cannot run `pip audit`",
))
# Check 4: Run safety/pip-audit if available
try:
result = subprocess.run(
["pip", "audit", "--format=json", "--quiet"],
capture_output=True, text=True, timeout=30
)
vuln_count = result.stdout.count('"id"')
results.append(CheckResult(
"No known vulnerable dependencies",
vuln_count == 0,
f"{vuln_count} vulnerabilities found" if vuln_count else "Clean",
))
except (FileNotFoundError, subprocess.TimeoutExpired):
results.append(CheckResult("pip audit", False, "pip-audit not installed, run: pip install pip-audit"))
return results
results = run_checks()
passed = sum(1 for r in results if r.passed)
print(f"Security Checklist: {passed}/{len(results)} checks passed\n")
for r in results:
status = "✅" if r.passed else "❌"
print(f" {status} {r.name}")
if not r.passed:
print(f" → {r.detail}")
Key Takeaways
- A01 Broken Access Control, always enforce ownership and role checks server-side; never trust user-supplied IDs
- A02 Cryptographic Failures, use bcrypt/Argon2 for passwords, Fernet/AES-GCM for data; never MD5/SHA-1
- A03 Injection is still #3, parameterize every query; never interpolate user input into SQL, OS commands, or LDAP
- Security is a design concern, A04 Insecure Design means you cannot patch your way out of an insecure architecture
- Run `pip audit` in CI/CD, catch vulnerable dependencies automatically before they reach production
- Log and alert on security events, if you can't detect a breach, you can't respond to it
What's Next?
Lesson 13 takes A03 (Injection) much deeper, SQL injection, NoSQL injection, command injection, and path traversal, each with vulnerable and fixed Python examples.
- SQL injection, vulnerable query vs parameterized fix
- Command injection,
os.systemvssubprocess - Path traversal, unsafe file access vs safe path validation