Skip to content
StackPractices
intermediate By Mathias Paulenko

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.

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

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 capabilities.

Prevention Checklist

  • Remove default accounts and credentials
  • Disable unnecessary capabilities, 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](/docs/templates/dependency-audit-template) 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.

How do I get started with this in an existing project?

Start with a small, isolated part of your codebase. Apply the concepts from this guide to one module or service. Measure the impact, then expand to other areas.

What tools do I need?

The tools mentioned throughout this guide are listed in each section. Most are open-source and widely adopted. Check the related resources for setup instructions.

How do I measure success after implementing this?

Define clear metrics before starting: performance benchmarks, error rates, or maintainability indicators. Compare before and after. Iterate based on the data, not on assumptions.