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)

A01
Broken Access Control

Users can act outside their intended permissions, accessing other users' data, modifying unauthorized resources.

A02
Cryptographic Failures

Sensitive data exposed due to weak/missing encryption, old algorithms, or poor key management.

A03
Injection

Hostile data sent to an interpreter as part of a command or query (SQL, OS, LDAP injection).

A04
Insecure Design

Missing or ineffective control design, security not built into the architecture from the start.

A05
Security Misconfiguration

Default credentials, verbose error messages, unnecessary features, missing security headers.

A06
Vulnerable & Outdated Components

Using components with known vulnerabilities; failure to patch libraries and frameworks.

A07
Identification & Auth Failures

Weak passwords, credential stuffing, missing MFA, poor session management.

A08
Software & Data Integrity Failures

Code and infrastructure that doesn't protect against integrity violations; insecure CI/CD pipelines.

A09
Security Logging & Monitoring Failures

Insufficient logging, monitoring, and alerting, attackers operate undetected for extended periods.

A10
Server-Side Request Forgery (SSRF)

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.system vs subprocess
  • Path traversal, unsafe file access vs safe path validation