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:

  1. Fix broken access control by centralizing authorization logic and testing every endpoint.
  2. Use modern cryptography — bcrypt for passwords, TLS 1.3, AES-256 for data at rest.
  3. Parameterize everything that touches an interpreter.
  4. Automate dependency scanning — you cannot manually track CVEs across hundreds of packages.
  5. Log everything security-relevant and alert on anomalies.