Security & Auth
12 min read

OWASP Top 10: Web Application Security Risks

The OWASP Top 10 is a ranked list of the most critical web application security risks, derived from real-world vulnerability data. The 2025 edition analyzed 2.8 million applications with 589 CWE (Common Weakness Enumeration) mappings across 248 categories, making it the definitive baseline for application security priorities.

A06

Insecure Design

A07

Auth Failures

A08

Integrity Failures

A09

Logging Failures

A10

Exceptions

OWASP Top 10:2025

A01

Access Control

A02

Misconfiguration

A03

Supply Chain

A04

Crypto Failures

A05

Injection

Defense Layers

Edge/CDN

WAF, Rate Limiting, DDoS

Network

Firewalls, TLS 1.3, HSTS

Application

Input Validation, CSP, AuthZ

Data

Encryption at Rest, Access Control

Detection

Logging, SIEM, Alerting

Defense-in-depth model mapped to OWASP Top 10:2025 vulnerability categories

The OWASP Top 10 represents a data-driven consensus on application security priorities. The 2025 edition introduced two new categories (Software Supply Chain Failures, Mishandling of Exceptional Conditions), consolidated SSRF (Server-Side Request Forgery) into Broken Access Control, and elevated Security Misconfiguration to #2 based on 100% incidence rates.

Core mental model:

CategoryRoot CausePrimary Control
A01-A02Missing enforcement/configurationDeny-by-default, secure defaults
A03-A04Untrusted sources, weak cryptoSupply chain verification, modern algorithms
A05-A06Untrusted input, design flawsParameterized queries, threat modeling
A07-A08Identity/integrity assumptionsMFA, signed artifacts
A09-A10Insufficient visibility/resilienceCentralized logging, fail-closed

Key shift in 2025: Security Misconfiguration rose from #5 to #2, reflecting that 100% of tested applications had some form of misconfiguration. Supply chain attacks (SolarWinds, Log4Shell, npm worms) drove the addition of a dedicated category.

Version history: The 2021 edition introduced root-cause categorization (e.g., “Cryptographic Failures” instead of “Sensitive Data Exposure”). The 2025 edition expanded scope from ~400 to 589 CWEs and added community-driven categories for emerging threats.

Broken Access Control maintains #1 with 100% of applications tested showing some vulnerability. With 40 mapped CWEs, 1.8M+ occurrences, and 32,654 CVEs, it’s the most prevalent risk. SSRF was consolidated into this category since it fundamentally bypasses access boundaries.

Access control enforces policy so users cannot act outside their intended permissions. API-first architectures expanded attack surface—every endpoint is a potential authorization decision point. The consolidation of SSRF acknowledges that forging requests to internal resources is an access control bypass.

PatternExampleCWE
Privilege escalationRegular user accesses admin endpointsCWE-269
IDOR (Insecure Direct Object Reference)/api/users/123/api/users/456CWE-639
Missing function-level controlsPOST/PUT/DELETE endpoints lack authzCWE-285
CORS misconfigurationWildcard origin with credentialsCWE-942
SSRF (now consolidated here)http://169.254.169.254/ metadata accessCWE-918
Path traversal../../../etc/passwdCWE-22
access-control.ts
4 collapsed lines
import { Request, Response, NextFunction } from "express"
import { verify } from "jsonwebtoken"
import { hasPermission } from "./rbac"
// Middleware: deny-by-default, explicit allow
export function authorize(resource: string, action: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.split(" ")[1]
if (!token) return res.status(401).json({ error: "No token" })
const payload = verify(token, process.env.JWT_SECRET!)
const allowed = await hasPermission(payload.sub, resource, action)
if (!allowed) return res.status(403).json({ error: "Forbidden" })
next()
}
}
5 collapsed lines
// Route: explicit authorization on every endpoint
app.get("/api/users/:id", authorize("users", "read"), getUser)
app.put("/api/users/:id", authorize("users", "update"), updateUser)
app.delete("/api/users/:id", authorize("users", "delete"), deleteUser)

Critical controls:

  1. Deny by default—require explicit grants for every resource
  2. Server-side enforcement—never trust client-side state
  3. Record ownership validation—verify the requesting user owns the resource
  4. Centralized access control—one module, consistent enforcement
  5. SSRF prevention—allowlist outbound destinations, validate resolved IPs
  6. Rate limiting—slow automated attacks
ssrf-prevention.ts
5 collapsed lines
import { URL } from "url"
import dns from "dns/promises"
import ipaddr from "ipaddr.js"
const ALLOWED_HOSTS = ["api.example.com", "cdn.example.com"]
async function fetchSafely(userUrl: string): Promise<Response> {
const url = new URL(userUrl)
// Protocol allowlist (no file://, gopher://, etc.)
if (!["https:"].includes(url.protocol)) {
throw new Error("Only HTTPS allowed")
}
// DNS rebinding protection: resolve and check before fetch
const addresses = await dns.resolve4(url.hostname)
for (const addr of addresses) {
const parsed = ipaddr.parse(addr)
if (parsed.range() !== "unicast") {
6 collapsed lines
throw new Error("Private IP not allowed")
}
}
return fetch(url.toString(), { redirect: "error" })
}

Network controls: Disable cloud metadata endpoints (IMDSv2 or block 169.254.169.254), segment internal services from web tier.

Security Misconfiguration rose from #5 (2021) to #2 (2025), with 100% of tested applications showing some misconfiguration. The 27.70% max incidence rate and 16 mapped CWEs reflect that secure defaults are rare, and every stack layer can be misconfigured.

Cloud infrastructure complexity multiplied configuration surfaces. XXE (XML External Entities) remains consolidated here—it’s fundamentally a parser misconfiguration. The shift to infrastructure-as-code made misconfigurations reproducible at scale.

LayerMisconfigurationImpact
CloudS3 bucket public by defaultData breach
ContainerRoot user in DockerContainer escape
FrameworkDebug mode in productionStack traces leak
ServerDirectory listing enabledSource exposure
HeadersMissing security headersXSS, clickjacking
DefaultsUnchanged admin credentialsFull compromise
ParserXXE enabled in XML parserFile disclosure, SSRF
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}';
xml-parser.ts
2 collapsed lines
import { XMLParser } from "fast-xml-parser"
// SAFE: external entities disabled (default in modern parsers)
const parser = new XMLParser({
ignoreAttributes: false,
// These enable XXE - never enable without explicit need:
// processEntities: true,
// allowBooleanAttributes: true,
})

Hardening workflow:

  1. Minimal installation—remove sample apps, documentation, unused features
  2. Identical environments—dev/staging/prod differ only in secrets
  3. Automated verification—config checks in CI/CD pipeline
  4. Short-lived credentials—identity federation, not hardcoded secrets
  5. Regular audits—quarterly review of cloud IAM, firewall rules

New in 2025, Software Supply Chain Failures expanded from “Vulnerable and Outdated Components” to address the entire software supply chain. It ranked #1 in community surveys (50% of respondents), with a 5.72% average incidence rate—the highest of any category.

SolarWinds (18,000+ organizations compromised), Log4Shell (CVE-2021-44228 affecting millions of systems), and Shai-Hulud (2025’s first self-propagating npm worm harvesting tokens and auto-propagating to 500+ package versions) demonstrated that supply chain attacks scale catastrophically. Your application is only as secure as its least secure dependency.

RiskExampleControl
Known vulnerabilitiesLog4Shell (CVE-2021-44228)Dependency scanning
Typosquattinglodahs instead of lodashPackage verification
Maintainer compromiseevent-stream incident (2018)Audit upstream changes
Build system compromiseSolarWinds OrionSigned builds, reproducibility
Malicious packagesnpm worms harvesting tokensPrivate registry, allowlisting
package.json
5 collapsed lines
{
"name": "secure-app",
"version": "1.0.0",
"scripts": {
"audit": "npm audit --production",
"audit:fix": "npm audit fix",
"sbom": "npx @cyclonedx/cyclonedx-npm --output-file sbom.json"
},
"dependencies": {
"express": "^4.18.2"
},
"overrides": {
"lodash": "4.17.21"
}
1 collapsed line
}

Supply chain hygiene:

  1. SBOM (Software Bill of Materials)—generate with CycloneDX or SPDX
  2. Automated scanning—OWASP Dependency-Check, Snyk, npm audit in CI
  3. Version pinning—lock files, avoid floating versions in production
  4. Private registry—proxy public packages, scan before allowing
  5. SRI (Subresource Integrity)—hash verification for CDN scripts
  6. Staged rollouts—canary deployments, not simultaneous updates
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>

If the CDN script changes, the browser refuses execution.

Cryptographic Failures moved from #2 (2021) to #4 (2025), with 32 mapped CWEs and 1.6M+ occurrences. The category focuses on root causes—broken algorithms, weak keys, insufficient entropy—rather than the symptom of data exposure.

PatternWeaknessImpact
Cleartext transmissionNo TLS, HTTP fallbackMITM credential theft
Weak algorithmsMD5/SHA1 for passwords, DESOffline cracking
Insufficient entropyPredictable tokens, weak PRNGSession hijacking
Hardcoded secretsAPI keys in sourceCredential compromise
Missing authenticated encryptionAES-CBC without HMACPadding oracle
password.ts
3 collapsed lines
import { hash, verify } from "argon2"
// Argon2id: OWASP-recommended for password hashing
const PASSWORD_OPTIONS = {
type: 2, // argon2id (hybrid: side-channel + time-memory tradeoff resistant)
memoryCost: 65536, // 64MB
timeCost: 3,
parallelism: 4,
}
export async function hashPassword(password: string): Promise<string> {
return hash(password, PASSWORD_OPTIONS)
}
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
return verify(stored, password)
}

Why Argon2id: Memory-hard functions resist GPU/ASIC cracking. Argon2id combines side-channel resistance (Argon2i) with time-memory tradeoff resistance (Argon2d). NIST recommends preparation for post-quantum cryptography by 2030 for high-value systems.

  1. Identify sensitive data per PCI-DSS, GDPR, HIPAA
  2. Encrypt at rest using AES-256-GCM (authenticated)
  3. Encrypt in transit via TLS 1.3 with forward secrecy
  4. Key management: HSM or cloud KMS, never in source
  5. Rotate keys on schedule and after compromise

Injection dropped from #3 (2021) to #5 (2025), but remains critical with 37 mapped CWEs, 62,445 CVEs, and 100% of applications showing some vulnerability. XSS (Cross-Site Scripting) remains consolidated here—it’s HTML/JavaScript injection.

Injection TypeEntry PointPrevention
SQLDatabase queriesParameterized queries, ORM
XSSHTML outputOutput encoding, CSP
CommandOS shell callsAvoid shell; use safe APIs
NoSQLDocument queriesSchema validation
TemplateServer-side templatesSandbox, restricted syntax
LDAPDirectory queriesEscape special characters
query.ts
3 collapsed lines
import { Pool } from "pg"
const pool = new Pool()
// VULNERABLE: string concatenation
async function getUserUnsafe(id: string) {
// Attacker: "1; DROP TABLE users;--"
return pool.query(`SELECT * FROM accounts WHERE id = ${id}`)
}
// SAFE: parameterized query
async function getUserSafe(id: string) {
return pool.query("SELECT * FROM accounts WHERE id = $1", [id])
}
// SAFE: ORM with typed parameters
async function getUserORM(id: number) {
2 collapsed lines
return prisma.user.findUnique({ where: { id } })
}

CSP is the primary XSS defense—it tells browsers which sources can execute scripts:

Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';

CSP deployment: Start with Content-Security-Policy-Report-Only, analyze violations, then enforce.

Insecure Design dropped from #4 (2021) to #6 (2025), with 39 mapped CWEs. It targets architectural flaws that cannot be fixed by secure implementation—an insecure design cannot be saved by secure code.

Insecure DesignImplementation Bug
No rate limiting on password resetRate limit bypassed via header
Security questions for recoverySQL injection in security handler
Trust boundary violationJWT validation missing on one endpoint
No business logic validationOff-by-one in quantity check
  1. Secure development lifecycle with threat modeling
  2. Security requirements in user stories
  3. Design pattern library for authentication, authorization
  4. Plausibility checks at all tiers (can one user really order 10,000 items?)
  5. Rate limits on expensive operations

A cinema allowed unlimited seat reservations. Attackers reserved all seats, preventing legitimate sales, then released them last-minute.

Design fix: Maximum seats per session, escalating hold costs, bot detection.

Authentication Failures maintains #7 with 36 mapped CWEs, 1.1M+ occurrences, and 7,147 CVEs. The name shortened from “Identification and Authentication Failures” to emphasize the authentication focus.

WeaknessExploitationControl
Credential stuffingAutomated login with breached passwordsMFA, rate limiting
Weak passwordsDictionary attacksMin 12 chars, breach check
Session fixationAttacker sets session ID pre-loginRegenerate on auth
Missing logoutSessions persist indefinitelyServer-side invalidation
Exposed session IDsIDs in URLsCookie-only, HttpOnly
session.ts
5 collapsed lines
import session from "express-session"
import RedisStore from "connect-redis"
import { createClient } from "redis"
const redisClient = createClient()
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!,
name: "__Host-session", // Cookie prefix binding
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JS access
sameSite: "strict", // CSRF protection
maxAge: 15 * 60 * 1000, // 15 minutes
},
}),
)
7 collapsed lines
// Regenerate session on login (prevent fixation)
app.post("/login", async (req, res) => {
const user = await authenticate(req.body)
req.session.regenerate(() => {
req.session.userId = user.id
res.json({ success: true })
})
})

NIST reversed decades of password theater:

  • No complexity requirements—users create worse passwords with forced symbols
  • No periodic rotation—change only on evidence of compromise
  • Minimum 8 characters (12+ recommended), max at least 64
  • Check against breached passwords—haveibeenpwned integration
  • Allow paste—password managers are the primary defense

Software and Data Integrity Failures remains #8, addressing assumptions about updates, critical data, and CI/CD pipelines without verifying integrity. Distinct from A03: this category focuses on runtime integrity checks (deserialization, auto-updates), while A03 focuses on supply chain provenance.

PatternExampleImpact
Unsigned updatesAuto-update without verificationMalicious code
Insecure deserializationUntrusted data → object instantiationRCE
CI/CD compromiseBuild server accessSupply chain attack
CDN modificationJavaScript changed at CDNClient attacks
deserialize.ts
3 collapsed lines
import Ajv from "ajv"
// SAFE: schema validation before use
const ajv = new Ajv()
const schema = {
type: "object",
properties: {
userId: { type: "integer" },
action: { type: "string", enum: ["read", "write"] },
},
required: ["userId", "action"],
additionalProperties: false,
}
const validate = ajv.compile(schema)
function processInput(untrusted: string) {
const data = JSON.parse(untrusted)
if (!validate(data)) {
throw new Error("Invalid input schema")
}
// Now safe to use data.userId, data.action
}
  1. Signed commits—GPG signatures on protected branches
  2. Reproducible builds—same source → same binary
  3. Artifact signing—verify packages before deployment
  4. Least privilege runners—CI jobs run with minimal permissions
  5. Secret scanning—no credentials in logs or artifacts

Renamed from “Security Logging and Monitoring Failures” to emphasize alerting—logging without alerting provides minimal value. With only 5 mapped CWEs, it’s community-driven rather than data-driven, reflecting that breaches averaging 287 days undetected (IBM 2021) demonstrate detection failures.

EventRequired FieldsAlert Threshold
Login failureUser, IP, timestamp, user-agent5 failures/minute
Access deniedUser, resource, actionAny sensitive resource
Input validation failureInput (sanitized), endpoint10/minute same IP
Admin actionUser, action, targetAll
Rate limit triggeredIP, endpoint, countAll
logging.ts
3 collapsed lines
import pino from "pino"
const logger = pino({
formatters: {
level: (label) => ({ level: label }),
},
})
// SAFE: structured logging (not string concatenation)
logger.info({ userId: user.id, action: "login" }, "User authenticated")
// UNSAFE: user input in log string enables injection
// logger.info(`User ${username} logged in`);
// username = "admin\n[ALERT] Breach"
  1. Real-time alerts for auth failures, privilege changes
  2. Anomaly detection for unusual access patterns
  3. Tamper-evident storage for logs
  4. Incident response playbook per NIST 800-61r2
  5. Regular testing of detection capabilities

New in 2025, this category addresses failures to prevent, detect, and respond to unusual situations—crashes, unexpected behavior, and exploitable states. With 24 mapped CWEs and 3,416 CVEs, it replaces vague “code quality” categories with specific guidance.

FailureExampleImpact
Resource exhaustionUnhandled upload exceptionDoS
Data exposureDatabase error reveals internalsReconnaissance
Fail-openException bypasses auth checkUnauthorized access
Incomplete transactionTransfer interrupted mid-processFinancial fraud
error-handling.ts
3 collapsed lines
import { Transaction } from "sequelize"
// SAFE: transactional rollback on any failure
async function transferFunds(from: number, to: number, amount: number) {
const t = await sequelize.transaction()
try {
await Account.decrement("balance", { by: amount, where: { id: from }, transaction: t })
await Account.increment("balance", { by: amount, where: { id: to }, transaction: t })
await t.commit()
} catch (error) {
await t.rollback() // Fail closed: no partial state
logger.error({ from, to, amount, error: error.message }, "Transfer failed")
throw new Error("Transfer failed") // Don't leak internals
}
}

Key controls:

  1. Local error handling—catch at occurrence point with meaningful recovery
  2. Global exception handler—backup for uncaught exceptions
  3. Fail closed—transactional rollback, deny access on error
  4. Resource quotas—rate limiting, memory limits
  5. Sanitized error messages—no stack traces to users

The OWASP Top 10:2025 reflects the evolved threat landscape based on data from 2.8M applications:

  1. Access control remains #1—SSRF consolidated here, every endpoint needs explicit authz
  2. Misconfiguration rose to #2—100% incidence, secure defaults are rare
  3. Supply chain is critical—new category driven by SolarWinds, Log4Shell, npm worms
  4. Exception handling matters—new category for fail-closed patterns
  5. Detection enables response—logging without alerting is ineffective

Use the Top 10 as a baseline for threat modeling, code review checklists, and security testing. For high-assurance applications, ASVS (Application Security Verification Standard) provides deeper coverage.

  • HTTP request/response model
  • Web application architecture (client/server, APIs)
  • Authentication flows (sessions, tokens)
  • A01-A02 (Access Control, Misconfiguration): Deny-by-default, secure defaults
  • A03-A04 (Supply Chain, Crypto): Verify dependencies, use modern algorithms
  • A05-A06 (Injection, Design): Parameterized queries, threat modeling
  • A07-A08 (Auth, Integrity): MFA, signed artifacts
  • A09-A10 (Logging, Exceptions): Centralized alerting, fail-closed
  • 2025 added Supply Chain Failures and Mishandling Exceptional Conditions
  • SSRF consolidated into Broken Access Control

Read more