OWASP Top 10 for Backend Engineers: Code-Level Prevention

The OWASP Top 10 is the most widely referenced list of critical web application security risks. Backend engineers are responsible for most of these vulnerabilities — they exist in code, not configurat

Introduction#

The OWASP Top 10 is the most widely referenced list of critical web application security risks. Backend engineers are responsible for most of these vulnerabilities — they exist in code, not configuration. This post covers the Top 10 with concrete examples of vulnerable code and their fixes.

A01: Broken Access Control#

The most common vulnerability. Failing to enforce authorization on every request.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# VULNERABLE: authorization check missing for sensitive operation
@app.get("/api/documents/{doc_id}")
async def get_document(doc_id: int, db: Session = Depends(get_db)):
    doc = db.query(Document).filter(Document.id == doc_id).first()
    return doc  # returns any document regardless of who's asking

# SECURE: always verify the requesting user owns the resource
@app.get("/api/documents/{doc_id}")
async def get_document(
    doc_id: int,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    doc = db.query(Document).filter(
        Document.id == doc_id,
        Document.owner_id == current_user.id  # enforce ownership
    ).first()

    if not doc:
        raise HTTPException(status_code=404)  # 404, not 403 (don't reveal existence)
    return doc

A02: Cryptographic Failures#

Sensitive data exposed due to weak or missing encryption.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import hashlib
import bcrypt
import secrets

# VULNERABLE: MD5 is broken, SHA1 is weak, plain SHA256 without salt is vulnerable to rainbow tables
def hash_password_wrong(password: str) -> str:
    return hashlib.md5(password.encode()).hexdigest()

# VULNERABLE: SHA256 without salt
def hash_password_still_wrong(password: str) -> str:
    return hashlib.sha256(password.encode()).hexdigest()

# SECURE: use bcrypt (or argon2, scrypt) — slow by design, includes salt
def hash_password(password: str) -> bytes:
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

def verify_password(password: str, hashed: bytes) -> bool:
    return bcrypt.checkpw(password.encode(), hashed)

# For tokens: use cryptographically secure random bytes
def generate_reset_token() -> str:
    return secrets.token_urlsafe(32)  # 256 bits of entropy

# Store sensitive data encrypted at rest
from cryptography.fernet import Fernet

KEY = Fernet.generate_key()  # store in secrets manager, not code
fernet = Fernet(KEY)

def encrypt(data: str) -> bytes:
    return fernet.encrypt(data.encode())

def decrypt(token: bytes) -> str:
    return fernet.decrypt(token).decode()

A03: Injection#

SQL, command, LDAP, and other injection vulnerabilities.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import subprocess
import sqlite3

# SQL INJECTION — VULNERABLE
def get_user_wrong(username: str):
    conn = sqlite3.connect("app.db")
    # Attacker sends: username = "admin' OR '1'='1"
    query = f"SELECT * FROM users WHERE username = '{username}'"
    return conn.execute(query).fetchone()

# SQL INJECTION — SECURE: parameterized queries
def get_user(username: str):
    conn = sqlite3.connect("app.db")
    return conn.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone()

# ORM also prevents injection:
def get_user_orm(username: str, db: Session):
    return db.query(User).filter(User.username == username).first()

# COMMAND INJECTION — VULNERABLE
def ping_host_wrong(host: str) -> str:
    result = subprocess.run(
        f"ping -c 1 {host}",  # attacker sends: "8.8.8.8; rm -rf /"
        shell=True, capture_output=True, text=True
    )
    return result.stdout

# COMMAND INJECTION — SECURE: use list form, never shell=True with user input
def ping_host(host: str) -> str:
    import re
    if not re.match(r'^[a-zA-Z0-9.\-]+$', host):
        raise ValueError("Invalid hostname")
    result = subprocess.run(
        ["ping", "-c", "1", host],  # list form, no shell interpretation
        capture_output=True, text=True, timeout=10
    )
    return result.stdout

A04: Insecure Design#

Missing threat modeling, no security requirements, unsafe defaults.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# VULNERABLE: password reset that reveals user existence
@app.post("/forgot-password")
async def forgot_password_wrong(email: str, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.email == email).first()
    if not user:
        raise HTTPException(status_code=404, detail="Email not found")  # reveals existence
    send_reset_email(user)
    return {"message": "Reset email sent"}

# SECURE: always return the same response regardless of whether user exists
@app.post("/forgot-password")
async def forgot_password(email: str, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.email == email).first()
    if user:  # silently skip if user not found
        token = secrets.token_urlsafe(32)
        store_reset_token(user.id, token, expires_in=3600)
        send_reset_email(user.email, token)
    return {"message": "If that email is registered, you'll receive a reset link"}

A05: Security Misconfiguration#

Default credentials, verbose error messages, unnecessary features enabled.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import logging

logger = logging.getLogger(__name__)
app = FastAPI()

# VULNERABLE: exposes internal details in error responses
@app.exception_handler(Exception)
async def generic_exception_handler_wrong(request: Request, exc: Exception):
    return JSONResponse(
        status_code=500,
        content={
            "error": str(exc),          # exposes exception message
            "traceback": traceback.format_exc(),  # exposes internals
        }
    )

# SECURE: log internally, return generic message externally
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
    logger.exception("Unhandled exception for %s %s", request.method, request.url)
    return JSONResponse(
        status_code=500,
        content={"error": "An internal error occurred"}
    )

# Security headers middleware
@app.middleware("http")
async def security_headers(request: Request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
    response.headers["Content-Security-Policy"] = "default-src 'self'"
    return response

A06: Vulnerable and Outdated Components#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Check for known vulnerabilities in Python dependencies
pip install pip-audit
pip-audit

# Check for known vulnerabilities in Node.js
npm audit
npm audit fix

# Use dependabot or renovate for automated updates
# .github/dependabot.yml
# version: 2
# updates:
#   - package-ecosystem: "pip"
#     directory: "/"
#     schedule:
#       interval: "weekly"

A07: Identification and Authentication Failures#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import time
from collections import defaultdict

# VULNERABLE: no brute force protection
@app.post("/login")
async def login_wrong(username: str, password: str, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.username == username).first()
    if user and verify_password(password, user.password_hash):
        return {"token": create_token(user.id)}
    raise HTTPException(status_code=401, detail="Invalid credentials")

# Track failed login attempts
login_attempts: dict[str, list[float]] = defaultdict(list)

WINDOW_SECONDS = 300  # 5 minutes
MAX_ATTEMPTS = 5

def check_rate_limit(identifier: str) -> None:
    now = time.time()
    attempts = [t for t in login_attempts[identifier] if now - t < WINDOW_SECONDS]
    login_attempts[identifier] = attempts

    if len(attempts) >= MAX_ATTEMPTS:
        raise HTTPException(
            status_code=429,
            detail="Too many failed attempts. Try again later.",
            headers={"Retry-After": str(WINDOW_SECONDS)},
        )

# SECURE: rate limit + consistent timing (prevent username enumeration)
@app.post("/login")
async def login(username: str, password: str, db: Session = Depends(get_db)):
    check_rate_limit(username)

    user = db.query(User).filter(User.username == username).first()

    # Always call verify_password to prevent timing attacks
    password_valid = user is not None and verify_password(password, user.password_hash)

    if not password_valid:
        login_attempts[username].append(time.time())
        raise HTTPException(status_code=401, detail="Invalid credentials")

    return {"token": create_token(user.id)}

A08: Software and Data Integrity Failures#

Deserialization of untrusted data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pickle
import json

# VULNERABLE: pickle deserializes arbitrary Python objects — remote code execution
@app.post("/load-session")
async def load_session_wrong(data: bytes):
    session = pickle.loads(data)  # NEVER deserialize user-controlled pickle data
    return session

# SECURE: use JSON or validated schemas
from pydantic import BaseModel

class SessionData(BaseModel):
    user_id: int
    role: str
    expires_at: int

@app.post("/load-session")
async def load_session(data: dict):
    session = SessionData(**data)  # validates and type-checks
    return session

A09: Security Logging and Monitoring Failures#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import logging
import uuid
from datetime import datetime

security_logger = logging.getLogger("security")

def log_security_event(
    event_type: str,
    user_id: int | None,
    ip_address: str,
    details: dict,
):
    security_logger.warning(
        "security_event",
        extra={
            "event_id": str(uuid.uuid4()),
            "event_type": event_type,
            "user_id": user_id,
            "ip_address": ip_address,
            "timestamp": datetime.utcnow().isoformat(),
            **details,
        }
    )

# Log security-relevant events
log_security_event("login_failure", None, request.client.host, {"username": username})
log_security_event("privilege_escalation_attempt", user.id, request.client.host, {})
log_security_event("mass_data_export", user.id, request.client.host, {"record_count": 50000})

A10: Server-Side Request Forgery (SSRF)#

Covered in depth in the dedicated SSRF post, but the key principle:

1
2
3
# Never fetch URLs provided by users without strict validation
# Use allowlists, resolve DNS, check against private IP ranges
# See the dedicated SSRF Prevention post for full implementation

Security Checklist#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Authentication and Authorization:
- [ ] All endpoints verify authentication
- [ ] All endpoints verify authorization (resource ownership)
- [ ] Passwords hashed with bcrypt/argon2
- [ ] Rate limiting on login and sensitive endpoints

Data Handling:
- [ ] SQL via parameterized queries or ORM only
- [ ] No shell=True with user-controlled input
- [ ] No pickle/eval on user-controlled data
- [ ] Input validated at API boundaries

Error Handling:
- [ ] Generic error messages in responses
- [ ] Detailed errors logged internally with correlation IDs
- [ ] Security events logged to tamper-resistant storage

Transport:
- [ ] TLS enforced (HSTS header set)
- [ ] Security headers set on all responses
- [ ] Sensitive data not logged or exposed in error messages

Conclusion#

Most OWASP Top 10 vulnerabilities are preventable with consistent engineering practices: parameterized queries, proper password hashing, enforced authorization on every request, input validation at boundaries, generic error responses, and security logging. Security is not a feature added at the end — it is built into every layer during development.

Contents