Blog

OWASP Top 10 in Practice: Code Examples, Real Breaches, and Free Tools for Every Category

Every OWASP Top 10 2025 category broken down with vulnerable code, fixed code, real breach examples, and free tools that catch each vulnerability type.

The OWASP Top 10 is the most referenced list in application security. It shapes compliance requirements, drives penetration testing methodologies, and shows up in every security questionnaire you will ever fill out. But most coverage of it reads like a textbook: abstract descriptions, vague recommendations, and zero code.

This post is different. For every category in the 2025 edition, I am going to show you what the vulnerability looks like in your codebase, what the fix looks like, which real company got breached because of it, and which free tools catch it before attackers do.

The 2025 edition is a significant update. OWASP analyzed over 175,000 CVE records mapped to 643 unique CWEs, up from 241 in 2021. Two new categories were added, one was consolidated, and the rankings shifted to reflect where the real damage is happening now. CISA added 245 vulnerabilities to its Known Exploited Vulnerabilities catalog in 2025 alone, a 20% increase over the prior year. A record-breaking 40,009 CVEs were published in 2024. The threat landscape is not slowing down, and the Top 10 reflects that.

If you are building with a security-first mindset, understanding these categories at the code level is non-negotiable. Let's get into it.

The 2025 List at a Glance

Before diving into each category, here is the complete OWASP Top 10 2025 with key statistics and what changed from 2021.

Rank Category CWEs Avg Incidence Change from 2021
A01 Broken Access Control 40 3.73% Stayed #1 (absorbed SSRF)
A02 Security Misconfiguration 16 3.00% Up from #5
A03 Software Supply Chain Failures 5 5.19% NEW (expanded from Vulnerable Components)
A04 Cryptographic Failures 32 3.80% Down from #2
A05 Injection 38 Highest CVE count Down from #3
A06 Insecure Design -- Max 11.33% Down from #4
A07 Authentication Failures 36 Max 15.80% Stayed ~#7 (renamed)
A08 Software or Data Integrity Failures -- -- Stayed #8
A09 Security Logging and Alerting Failures 5 -- Stayed #9 (renamed)
A10 Mishandling of Exceptional Conditions 24 -- NEW

Two new categories entered the list: Software Supply Chain Failures (A03), which expands the old "Vulnerable and Outdated Components" to cover the entire dependency and build ecosystem, and Mishandling of Exceptional Conditions (A10), covering error handling failures that lead to security vulnerabilities. SSRF was consolidated into Broken Access Control (A01). Security Misconfiguration jumped from #5 to #2, reflecting how pervasive cloud-era misconfigurations have become.

A01: Broken Access Control

The problem: Your application lets users access or modify resources they should not be able to reach. This includes IDOR (Insecure Direct Object References), privilege escalation, SSRF, and missing function-level access controls.

How common: 3.73% of tested applications. Number one for two consecutive editions. 40 mapped CWEs.

Real breaches:

  • Optus (2022): A coding error left an API endpoint without access control, exposing personal data of roughly 10 million customers.
  • Kia Connected Cars (2024): Researchers demonstrated they could take over connected-car functions using only a license plate number, exploiting weak ownership verification on Kia's web portal.
  • Capital One (2019): An SSRF attack via a misconfigured WAF let an attacker reach the AWS metadata service at 169.254.169.254, obtain temporary IAM credentials, and exfiltrate nearly 30 GB of credit application data.

Vulnerable code: IDOR in JavaScript

// VULNERABLE: No authorization check -- any authenticated user
// can view any other user's profile by changing the ID
app.get('/api/users/:userId/profile', (req, res) => {
  const { userId } = req.params;
  db.users.findById(userId).then(user => {
    res.json(user);
  });
});

Fixed code: IDOR prevention

// SECURE: Verify the requester owns the resource
app.get('/api/users/:userId/profile', authenticate, (req, res) => {
  const { userId } = req.params;
  if (req.user.id !== userId && !req.user.roles.includes('admin')) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  db.users.findById(userId).then(user => {
    res.json(user);
  });
});

Vulnerable code: SSRF in JavaScript

// VULNERABLE: User controls the URL the server fetches
app.get('/fetch-url', async (req, res) => {
  const { url } = req.query;
  // Attacker sends: http://169.254.169.254/latest/meta-data/
  const response = await fetch(url);
  const data = await response.text();
  res.send(data);
});

Fixed code: SSRF prevention

import { URL } from 'url';

const ALLOWED_HOSTS = new Set(['api.example.com', 'cdn.example.com']);

app.get('/fetch-url', async (req, res) => {
  const { url } = req.query;
  try {
    const parsed = new URL(url);
    if (!ALLOWED_HOSTS.has(parsed.hostname)) {
      return res.status(400).json({ error: 'Host not allowed' });
    }
    const response = await fetch(url);
    res.send(await response.text());
  } catch {
    res.status(400).json({ error: 'Invalid URL' });
  }
});

Free tools: Semgrep (SAST rules for IDOR patterns), OWASP ZAP (DAST for access control testing), Burp Suite Community Edition (manual testing).

A02: Security Misconfiguration

The problem: Default credentials, overly permissive CORS, debug mode in production, exposed error messages with stack traces, missing security headers, open cloud storage buckets.

How common: 3.00% of tested applications. Jumped from #5 to #2 in the 2025 edition because cloud misconfigurations have become epidemic.

Real breaches:

  • AWS S3 Bucket Mega-Breach (2024): Hackers scanned millions of sites and exploited misconfigured systems to steal over 2 TB of data, including AWS access keys, GitHub API keys, SMTP credentials, and source code. In an ironic twist, the attackers stored their stolen data in their own misconfigured S3 bucket.
  • Microsoft Sentinel (September 2024): A bug in internal monitoring agents disrupted log collection for critical services for nearly three weeks, creating blind spots in threat detection for customers.

Vulnerable code: Exposed error details in JavaScript

// VULNERABLE: Leaks internal paths, database schema, credentials
app.use((err, req, res, next) => {
  res.status(500).json({
    error: err.message,
    stack: err.stack,
    query: err.sql,
    connectionString: err.config
  });
});

Fixed code: Safe error handling

// SECURE: Generic errors in production, detailed logs internally
app.use((err, req, res, next) => {
  const errorId = crypto.randomUUID();
  logger.error({ errorId, err, url: req.url, method: req.method });

  if (process.env.NODE_ENV === 'production') {
    res.status(500).json({ error: 'Internal server error', errorId });
  } else {
    res.status(500).json({ error: err.message, stack: err.stack });
  }
});

Vulnerable code: Permissive defaults in Python

# VULNERABLE: Debug mode, default secret, wide-open CORS
app = Flask(__name__)
app.config['SECRET_KEY'] = 'changeme'
app.config['DEBUG'] = True
CORS(app, origins="*", supports_credentials=True)

Fixed code: Hardened configuration

import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ['SECRET_KEY']
app.config['DEBUG'] = False
CORS(app, origins=["https://app.example.com"], supports_credentials=True)

@app.after_request
def set_headers(response):
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['Strict-Transport-Security'] = (
        'max-age=31536000; includeSubDomains'
    )
    return response

Free tools: ScoutSuite (cloud misconfiguration scanning), Trivy (infrastructure-as-code scanning), Checkov (IaC policy checks), Mozilla Observatory (HTTP security headers).

A03: Software Supply Chain Failures (New in 2025)

The problem: Compromises anywhere in the software dependency chain: malicious packages, hijacked maintainer accounts, poisoned build pipelines, unverified downloads. This category expanded from the old "Vulnerable and Outdated Components" to cover the entire ecosystem.

How common: 5.19% average incidence rate. The community survey overwhelmingly voted this the top concern.

Real breaches:

  • Chalk and Debug npm Compromise (September 2025): A maintainer was phished, and attackers published poisoned versions of libraries with over 2 billion aggregate weekly downloads.
  • Ultralytics PyPI Attack (December 2024): The popular ML library was compromised through an attack on its build pipeline.
  • dYdX Protocol Compromise (February 2026): Legitimate npm and PyPI packages were hijacked to deploy cryptocurrency wallet stealers and remote access trojans.

Vulnerable pattern: No lockfile integrity

// package.json -- VULNERABLE: loose version ranges
{
  "dependencies": {
    "lodash": "^4.0.0",
    "event-stream": "*"
  }
}
// No package-lock.json committed
// No integrity checks in CI

Fixed pattern: Pinned versions with integrity verification

// package.json -- SECURE: pinned versions
{
  "dependencies": {
    "lodash": "4.17.21"
  }
}

// In CI pipeline:
// npm ci --ignore-scripts  (uses lockfile exactly, fails if mismatched)
// npm audit --audit-level=high

Vulnerable pattern: Python dependencies without hashes

# requirements.txt -- VULNERABLE
requests
flask
# No version pins, no hashes

Fixed pattern: Pinned with hash verification

# requirements.txt -- SECURE
requests==2.31.0 \
  --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7f
flask==3.0.0 \
  --hash=sha256:21128f47e4e3b9d29131f7f3a70d56c5a

# Install with:
# pip install --require-hashes -r requirements.txt

Free tools: Socket.dev (supply chain analysis), npm audit / pip-audit, OWASP Dependency-Check, Trivy (SCA mode), Snyk Open Source (free tier), Renovate (automated dependency updates).

A04: Cryptographic Failures

The problem: Weak hashing algorithms, plaintext password storage, hardcoded encryption keys, missing encryption in transit, broken cryptographic implementations.

How common: 3.80% of tested applications. 32 mapped CWEs.

Real breaches:

  • Facebook (2019): 600 million user passwords stored in plaintext internally for months.
  • Toyota Subcontractor (2022): Private encryption keys and access tokens pushed to a public GitHub repository.
  • RockYou2021: A compilation of 8.4 billion passwords in plaintext, aggregated from years of breaches where companies used weak or no hashing.

Vulnerable code: Weak password hashing in JavaScript

const crypto = require('crypto');

// VULNERABLE: MD5 is broken for password hashing, no salt
function hashPassword(password) {
  return crypto.createHash('md5').update(password).digest('hex');
}

// VULNERABLE: Hardcoded key, ECB mode
const ENCRYPTION_KEY = 'mySecretKey123';
function encrypt(text) {
  const cipher = crypto.createCipher('aes-128-ecb', ENCRYPTION_KEY);
  return cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
}

Fixed code: Proper cryptography

const bcrypt = require('bcrypt');
const crypto = require('crypto');

// SECURE: bcrypt with proper cost factor
async function hashPassword(password) {
  return bcrypt.hash(password, 12);
}

// SECURE: AES-256-GCM with random IV, key from environment
function encrypt(text) {
  const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
  const encrypted = Buffer.concat([
    cipher.update(text, 'utf8'),
    cipher.final()
  ]);
  const tag = cipher.getAuthTag();
  return Buffer.concat([iv, tag, encrypted]).toString('base64');
}

Vulnerable code: Weak hashing in Python

import hashlib

# VULNERABLE: SHA-1 without salt
def hash_password(password):
    return hashlib.sha1(password.encode()).hexdigest()

# VULNERABLE: Secrets in code
DATABASE_URL = "postgresql://admin:password123@prod-db.example.com/main"

Fixed code: Proper Python cryptography

import bcrypt
import os

# SECURE: bcrypt with automatic salting
def hash_password(password: str) -> bytes:
    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12))

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

# Secrets from environment
DATABASE_URL = os.environ['DATABASE_URL']

Free tools: Semgrep (weak crypto rules, hardcoded secrets), Gitleaks / TruffleHog (secrets detection in git history), Bandit (Python-specific crypto checks).

A05: Injection

The problem: SQL injection, cross-site scripting (XSS), OS command injection, and other forms of untrusted data being interpreted as code. This remains one of the most tested categories, with the largest number of CVEs across all OWASP Top 10 entries.

How common: SQL injection CVEs rose from 2,264 in 2023 to 2,400 in 2024. CWE-78 (OS Command Injection) accounted for 18 of CISA's 245 KEV additions in 2025, making it the most common weakness in actively exploited vulnerabilities.

Real breaches:

  • ResumeLooters (2023-2024): Stole 2.2 million records from over 65 job websites using SQL injection and XSS, exfiltrating over 2 million unique email addresses.
  • MOVEit Transfer (2023): SQL injection vulnerability CVE-2023-34362, exploited by the Cl0p ransomware group, affected hundreds of organizations including Shell, BBC, and the US Department of Energy.

Vulnerable code: SQL injection in JavaScript

// VULNERABLE: String interpolation in SQL
app.get('/api/users', async (req, res) => {
  const { search } = req.query;
  const result = await db.query(
    `SELECT * FROM users WHERE name = '${search}'`
    // Attacker sends: ' OR '1'='1' --
  );
  res.json(result.rows);
});

Fixed code: Parameterized queries

// SECURE: Parameterized query
app.get('/api/users', async (req, res) => {
  const { search } = req.query;
  const result = await db.query(
    'SELECT * FROM users WHERE name = $1',
    [search]
  );
  res.json(result.rows);
});

// SECURE: Using an ORM (Prisma)
const users = await prisma.user.findMany({
  where: { name: search }
});

Vulnerable code: XSS in React

// VULNERABLE: Rendering unsanitized HTML
function Comment({ body }) {
  return <div dangerouslySetInnerHTML={{ __html: body }} />;
  // Attacker submits: <img src=x onerror="steal(document.cookie)">
}

Fixed code: XSS prevention

import DOMPurify from 'dompurify';

// SECURE: Sanitize before rendering
function Comment({ body }) {
  const sanitized = DOMPurify.sanitize(body);
  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

// BEST: Avoid dangerouslySetInnerHTML entirely
function Comment({ body }) {
  return <div>{body}</div>; // React auto-escapes by default
}

Vulnerable code: SQL injection in Python

# VULNERABLE: f-string in SQL
@app.route('/api/users')
def search_users():
    search = request.args.get('search')
    cursor.execute(f"SELECT * FROM users WHERE name = '{search}'")
    return jsonify(cursor.fetchall())

Fixed code: Parameterized Python queries

# SECURE: Parameterized query
@app.route('/api/users')
def search_users():
    search = request.args.get('search')
    cursor.execute("SELECT * FROM users WHERE name = %s", (search,))
    return jsonify(cursor.fetchall())

# SECURE: Using an ORM (SQLAlchemy)
users = User.query.filter_by(name=search).all()

Free tools: SQLMap (SQL injection testing), Semgrep (injection rules), OWASP ZAP (automated XSS and SQLi scanning), Bandit (Python injection detection), ESLint security plugins.

A06: Insecure Design

The problem: Architectural and business logic flaws that cannot be fixed with better implementation. Missing rate limiting, no fraud controls, trust boundary violations, and absent threat modeling.

How common: Maximum incidence rate of 11.33%. Business logic flaws totaled 2% of all vulnerabilities with a 5% year-over-year increase. The crypto and blockchain industries saw a 37% increase in business logic flaw exploitation from 2023 to 2024.

Real breaches:

  • Equifax (2017): Exposed PII of 147 million people. Root causes included insecure design, poor patch management, and fundamentally inadequate security architecture.
  • Crypto/Blockchain exploits (2023-2024): Business logic flaws in smart contracts and DeFi protocols accounted for billions in losses.

Vulnerable code: No rate limiting, no business constraints

// VULNERABLE: Unlimited login attempts
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findByEmail(email);
  if (user && await bcrypt.compare(password, user.passwordHash)) {
    return res.json({ token: jwt.sign({ id: user.id }, SECRET) });
  }
  res.status(401).json({ error: 'Invalid credentials' });
});

// VULNERABLE: No purchase limits (enables bot scalping)
app.post('/api/purchase', authenticate, async (req, res) => {
  const { itemId, quantity } = req.body;
  await createOrder(req.user.id, itemId, quantity);
  res.json({ success: true });
});

Fixed code: Design-level security controls

import rateLimit from 'express-rate-limit';

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true,
  message: { error: 'Too many login attempts. Try again later.' }
});

app.post('/api/login', loginLimiter, async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findByEmail(email);
  if (user && await bcrypt.compare(password, user.passwordHash)) {
    return res.json({
      token: jwt.sign({ id: user.id }, SECRET, { expiresIn: '1h' })
    });
  }
  // Generic message: don't reveal if email exists
  res.status(401).json({ error: 'Invalid email or password' });
});

// Business logic constraints
app.post('/api/purchase', authenticate, purchaseLimiter, async (req, res) => {
  const { itemId, quantity } = req.body;
  if (quantity < 1 || quantity > 5) {
    return res.status(400).json({ error: 'Quantity must be 1-5' });
  }
  const recentPurchases = await getRecentPurchases(req.user.id, itemId);
  if (recentPurchases + quantity > 10) {
    return res.status(429).json({ error: 'Purchase limit exceeded' });
  }
  await createOrder(req.user.id, itemId, quantity);
  res.json({ success: true });
});

Free tools: OWASP Threat Dragon (threat modeling), Semgrep (custom business logic rules), manual code review.

A07: Authentication Failures

The problem: Weak session management, credential stuffing, missing MFA, predictable tokens, password reset flaws, JWT misconfigurations.

How common: Maximum incidence rate of 15.80%. 36 mapped CWEs.

Real breaches:

  • Okta Credential Stuffing (April 2024): An unprecedented surge in proxy-driven credential stuffing attacks using residential proxies and combo lists. Customers affected included 23andMe, Roku, and Hot Topic.
  • 23andMe (2023): Credential stuffing exposed genetic data of 6.9 million users. Attackers used passwords from prior breaches, then scraped connected relatives' data.
  • Snowflake Platform (2024): Attackers exploited privileged accounts without MFA, moving laterally and exfiltrating data from customers including Ticketmaster and AT&T.

Vulnerable code: Weak session management in JavaScript

// VULNERABLE: Predictable tokens, no expiry, no cookie flags
app.post('/api/login', async (req, res) => {
  if (await validateUser(req.body.username, req.body.password)) {
    const sessionId = req.body.username + '_' + Date.now();
    sessions[sessionId] = { username: req.body.username };
    res.cookie('session', sessionId); // No httpOnly, no secure flag
    res.json({ success: true });
  }
});

Fixed code: Secure session management

import session from 'express-session';

app.use(session({
  secret: process.env.SESSION_SECRET,
  name: '__Host-sid',
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 30 * 60 * 1000
  },
  resave: false,
  saveUninitialized: false,
  store: new RedisStore({ client: redisClient })
}));

Vulnerable code: Weak JWT in Python

import jwt

# VULNERABLE: No expiry, weak secret, no claims validation
def create_token(user_id):
    return jwt.encode({'user_id': user_id}, 'secret123', algorithm='HS256')

Fixed code: Secure JWT in Python

import jwt
import os
from datetime import datetime, timedelta, timezone

def create_token(user_id: int) -> str:
    payload = {
        'sub': user_id,
        'iat': datetime.now(timezone.utc),
        'exp': datetime.now(timezone.utc) + timedelta(hours=1),
        'iss': 'myapp',
        'aud': 'myapp-api'
    }
    return jwt.encode(payload, os.environ['JWT_SECRET'], algorithm='HS256')

def verify_token(token: str) -> dict:
    return jwt.decode(
        token,
        os.environ['JWT_SECRET'],
        algorithms=['HS256'],
        issuer='myapp',
        audience='myapp-api'
    )

Free tools: OWASP ZAP (session management testing), Burp Suite Community (manual auth testing), Hydra (credential testing), Semgrep (JWT and session rules).

A08: Software or Data Integrity Failures

The problem: Software updates, critical data, or CI/CD pipelines that are not protected against integrity violations. This includes insecure deserialization, unsigned artifacts, and compromised build systems.

How common: CWE-502 (Deserialization of Untrusted Data) was a factor in 14 of CISA's 245 KEV additions in 2025, making it the second most common weakness in actively exploited vulnerabilities.

Real breaches:

  • SolarWinds Orion (2020): Attackers injected malicious code into the CI/CD build pipeline, distributing a backdoored update to over 18,000 customers including US government agencies and Fortune 500 companies.

Vulnerable code: Unsafe deserialization in JavaScript

const serialize = require('node-serialize');

// VULNERABLE: Deserializing user-controlled data enables RCE
app.post('/api/session', (req, res) => {
  const sessionData = Buffer.from(
    req.cookies.session, 'base64'
  ).toString();
  const obj = serialize.unserialize(sessionData);
  res.json(obj);
});

Fixed code: Safe parsing with validation

// SECURE: JSON.parse with schema validation
app.post('/api/session', (req, res) => {
  try {
    const sessionData = Buffer.from(
      req.cookies.session, 'base64'
    ).toString();
    const obj = JSON.parse(sessionData);
    const validated = sessionSchema.parse(obj); // zod validation
    res.json(validated);
  } catch {
    res.status(400).json({ error: 'Invalid session' });
  }
});

Vulnerable code: Pickle deserialization in Python

import pickle, base64

# VULNERABLE: pickle can execute arbitrary code on load
@app.route('/api/load-state', methods=['POST'])
def load_state():
    data = base64.b64decode(request.data)
    state = pickle.loads(data)  # Arbitrary code execution
    return jsonify(state)

Fixed code: JSON with schema validation

from pydantic import BaseModel

class AppState(BaseModel):
    user_id: int
    preferences: dict

# SECURE: JSON with Pydantic validation
@app.route('/api/load-state', methods=['POST'])
def load_state():
    try:
        data = request.get_json()
        state = AppState(**data)
        return jsonify(state.dict())
    except Exception:
        abort(400)

Free tools: Semgrep (deserialization rules), Bandit (Python pickle.loads detection), npm audit, Sigstore/Cosign (artifact signing and verification).

A09: Security Logging and Alerting Failures

The problem: Missing audit logs for security-relevant events, insufficient monitoring, no alerting on suspicious activity. Without logging, breaches go undetected for months.

How common: Organizations took an average of 194 days to identify a data breach in 2024. That is over six months of attackers operating undetected.

Real breaches:

  • Snowflake Platform (2024): Insufficient monitoring of privileged accounts allowed attackers to move laterally and exfiltrate data over an extended period. Inadequate logging complicated forensic investigation.
  • Microsoft Sentinel (September 2024): A monitoring agent bug disrupted log collection for nearly three weeks, creating blind spots in threat detection.
  • Cloudflare Logging Pipeline (November 2024): A misconfiguration caused a cascading failure that eliminated approximately 55% of customer logs over a 3.5-hour window.

Vulnerable code: No audit logging

// VULNERABLE: Security-relevant events are invisible
app.post('/api/login', async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  if (user) {
    res.json({ token: createToken(user) });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
    // Failed login: no log, no alert, no trail
  }
});

app.delete('/api/users/:id', adminOnly, async (req, res) => {
  await User.destroy(req.params.id);
  res.json({ success: true });
  // Admin deleting a user with zero audit trail
});

Fixed code: Structured audit logging

import winston from 'winston';

const auditLogger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  defaultMeta: { service: 'auth-service' },
  transports: [
    new winston.transports.File({ filename: 'audit.log' })
  ]
});

app.post('/api/login', async (req, res) => {
  const { email } = req.body;
  const user = await authenticate(email, req.body.password);

  if (user) {
    auditLogger.info('login_success', {
      userId: user.id,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
      timestamp: new Date().toISOString()
    });
    res.json({ token: createToken(user) });
  } else {
    auditLogger.warn('login_failure', {
      email,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
      timestamp: new Date().toISOString()
    });
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

Free tools: ELK Stack (Elasticsearch, Logstash, Kibana), Grafana Loki, Falco (runtime security monitoring), OWASP Logging Cheat Sheet.

A10: Mishandling of Exceptional Conditions (New in 2025)

The problem: Programs that fail to prevent, detect, and respond to unusual conditions: improper error handling, failing open instead of closed, resource leaks in error paths, null pointer dereferences, and swallowed exceptions that hide security problems.

How common: 24 mapped CWEs including CWE-209 (Info Exposure via Error Messages), CWE-476 (NULL Pointer Dereference), CWE-636 (Not Failing Securely), and CWE-391 (Unchecked Error Condition).

Real-world scenarios (from OWASP documentation):

  • Resource exhaustion via exception-triggered resource leaks leading to denial of service.
  • Sensitive data exposure via database error messages that reveal schema, enabling SQL injection refinement.
  • Financial transaction corruption via interrupted multi-step processes that fail open.

Vulnerable code: Fail-open authorization

// VULNERABLE: Grants access when auth service is down
async function checkPermission(userId, resource) {
  try {
    const response = await fetch(`${AUTH_SERVICE}/check`, {
      body: JSON.stringify({ userId, resource })
    });
    const { allowed } = await response.json();
    return allowed;
  } catch (error) {
    return true; // FAIL OPEN -- auth service down means everyone gets in
  }
}

Fixed code: Fail-closed with proper error handling

// SECURE: Denies access when auth service is unavailable
async function checkPermission(userId, resource) {
  try {
    const response = await fetch(`${AUTH_SERVICE}/check`, {
      body: JSON.stringify({ userId, resource }),
      signal: AbortSignal.timeout(5000)
    });
    if (!response.ok) {
      throw new Error(`Auth service returned ${response.status}`);
    }
    const { allowed } = await response.json();
    return allowed;
  } catch (error) {
    logger.error('auth_check_failed', {
      userId, resource, error: error.message
    });
    return false; // FAIL CLOSED -- deny by default
  }
}

Vulnerable code: Swallowed exceptions in Python

# VULNERABLE: Silently swallows all errors
def transfer_funds(from_account, to_account, amount):
    try:
        debit(from_account, amount)
        credit(to_account, amount)
    except Exception:
        pass  # Money debited but never credited?

Fixed code: Proper exception handling with transactions

import logging

logger = logging.getLogger(__name__)

def transfer_funds(from_account: str, to_account: str, amount: float):
    try:
        with db.transaction():  # Atomic: both succeed or both roll back
            debit(from_account, amount)
            credit(to_account, amount)
    except InsufficientFundsError:
        logger.warning("Insufficient funds",
                       extra={"from": from_account, "amount": amount})
        raise
    except DatabaseError as e:
        logger.error("Transfer failed",
                     extra={"from": from_account, "to": to_account})
        raise TransferFailedError("Transfer could not be completed") from e

Free tools: Semgrep (bare except, empty catch rules), ESLint (no-empty rule), Pylint, SonarQube Community Edition.

Free Tools Summary: What Catches What

Here is a consolidated view of which free tools map to which OWASP categories. This is the toolstack I recommend in the free security toolstack guide.

Tool Type Categories Covered
Semgrep SAST A01, A04, A05, A07, A08, A10
OWASP ZAP DAST A01, A02, A05, A07
Trivy SCA + IaC A02, A03
OWASP Dependency-Check SCA A03
Bandit SAST (Python) A04, A05, A08
ESLint Security Plugins SAST (JS) A05, A10
Gitleaks / TruffleHog Secrets A02, A04
Socket.dev Supply Chain A03
SonarQube Community SAST A01 through A10
ScoutSuite Cloud A02
Grafana Loki / ELK Logging A09
OWASP Threat Dragon Threat Modeling A06

No single tool covers everything. The combination of Semgrep (SAST) plus Trivy (SCA) plus OWASP ZAP (DAST) gives you broad coverage across most categories for zero dollars. If you are building a compliance-ready SaaS, this trio should be in your CI/CD pipeline from day one.

The OWASP API Security Top 10 (2023)

APIs have their own threat landscape. The OWASP API Security Top 10 was last updated in 2023 and is still the current edition. It focuses on API-specific attack patterns that the main Top 10 does not fully address.

Rank Category Description
API1 Broken Object Level Authorization Manipulating object IDs to access other users' data
API2 Broken Authentication Weak auth mechanisms specific to API contexts
API3 Broken Object Property Level Authorization Exposing or allowing modification of properties users should not access
API4 Unrestricted Resource Consumption No rate limiting, enabling denial of service
API5 Broken Function Level Authorization Accessing admin-level API functions without proper checks
API6 Unrestricted Access to Sensitive Business Flows Automated abuse of business features like bot scalping
API7 Server Side Request Forgery APIs that fetch external resources without validation
API8 Security Misconfiguration Missing headers, verbose errors, default configurations
API9 Improper Inventory Management Exposed old API versions, undocumented endpoints
API10 Unsafe Consumption of APIs Trusting third-party API responses without validation

Three categories were new in 2023: Unrestricted Access to Sensitive Business Flows (API6), SSRF (API7), and Unsafe Consumption of APIs (API10). Injection and Insufficient Logging dropped off the API list, though they remain significant concerns. BOLA (API1) remains the number one API vulnerability, reflecting how common it is for APIs to expose internal object references without proper authorization checks.

If you are building an API-first product, map your endpoints against this list. Every endpoint that accepts an object ID in its path or query string is a potential BOLA vector. Every endpoint that calls a third-party API is a potential unsafe consumption risk.

The OWASP LLM Top 10 (2025)

AI applications introduce entirely new vulnerability categories. The OWASP Top 10 for Large Language Model Applications was updated for 2025 and reflects the rapid evolution of real-world LLM deployments.

Rank Category Description
LLM01 Prompt Injection Manipulating inputs to override instructions or extract data
LLM02 Sensitive Information Disclosure LLMs leaking training data or PII
LLM03 Supply Chain Compromised models, datasets, or plugins
LLM04 Data and Model Poisoning Corrupting training data to influence outputs
LLM05 Improper Output Handling Unsanitized LLM outputs leading to XSS or injection
LLM06 Excessive Agency LLM agents granted too many permissions
LLM07 System Prompt Leakage Extracting system prompts that reveal business logic
LLM08 Vector and Embedding Weaknesses Vulnerabilities in RAG systems and vector databases
LLM09 Misinformation / Overreliance Trusting LLM outputs without verification
LLM10 Unbounded Consumption Resource exhaustion and runaway costs

Two categories are new for 2025: System Prompt Leakage (LLM07) and Vector and Embedding Weaknesses (LLM08). The latter is particularly relevant given that 53% of companies use RAG pipelines rather than fine-tuning, making vector databases a growing attack surface.

If you are building AI products, Prompt Injection (LLM01) is the equivalent of SQL injection for LLMs: the number one risk, and the hardest to fully eliminate. Treat all user inputs to LLMs as untrusted, just like you would with database queries. For a deeper dive into building AI systems that pass compliance audits, see the audit-ready LLM architecture guide.

Mapping It All Together: 2021 to 2025 Comparison

2021 Rank 2021 Category 2025 Rank 2025 Category Change
A01 Broken Access Control A01 Broken Access Control Stayed (absorbed SSRF)
A02 Cryptographic Failures A04 Cryptographic Failures Dropped 2 spots
A03 Injection A05 Injection Dropped 2 spots
A04 Insecure Design A06 Insecure Design Dropped 2 spots
A05 Security Misconfiguration A02 Security Misconfiguration Rose 3 spots
A06 Vulnerable and Outdated Components A03 Software Supply Chain Failures Expanded and rose 3 spots
A07 Identification and Authentication Failures A07 Authentication Failures Stayed (renamed)
A08 Software and Data Integrity Failures A08 Software or Data Integrity Failures Stayed
A09 Security Logging and Monitoring Failures A09 Security Logging and Alerting Failures Stayed (renamed)
A10 Server-Side Request Forgery -- Merged into A01 Consolidated
-- -- A10 Mishandling of Exceptional Conditions NEW

The biggest signal in this evolution: supply chain and misconfiguration have overtaken traditional injection as the primary concerns. This does not mean injection is less dangerous. It means that frameworks and ORMs have made injection harder to introduce accidentally, while cloud infrastructure and dependency ecosystems have made misconfigurations and supply chain attacks easier to exploit at scale.

What to Do Next

Knowing the OWASP Top 10 is step one. Acting on it is what matters. Here is a practical sequence.

This week: Run npm audit or pip-audit on your current projects. Fix critical and high severity findings. Set up Dependabot or Renovate for automated dependency updates.

This month: Add Semgrep to your CI/CD pipeline. Start with the default security rulesets. Every pull request gets scanned before merge.

This quarter: Run OWASP ZAP against your staging environment. Review the findings, prioritize anything in the A01 through A05 range. Set up structured logging if you do not already have it.

Ongoing: Conduct threat modeling for new features. Review the OWASP Top 10 during architecture decisions. If you are preparing for SOC 2, documenting how you address each OWASP category is strong evidence for the Security and Availability trust service criteria.

The OWASP Top 10 is not a compliance checkbox. It is a map of where real attackers are actually succeeding. Use it as one.

Frequently Asked Questions

What changed in the OWASP Top 10 2025 compared to 2021? The 2025 edition added two new categories: Software Supply Chain Failures (A03) and Mishandling of Exceptional Conditions (A10). SSRF was merged into Broken Access Control (A01). Security Misconfiguration rose from #5 to #2, while Injection dropped from #3 to #5. The analysis covered 175,000+ CVE records mapped to 643 CWEs, up from 241 in 2021.

What is the most common OWASP Top 10 vulnerability? Broken Access Control (A01) remains the most common and most serious application security risk, affecting 3.73% of tested applications. It has held the number one position for two consecutive OWASP editions and covers 40 mapped CWEs including IDOR, privilege escalation, and SSRF.

What free tools can I use to test for OWASP Top 10 vulnerabilities? The core free toolstack includes Semgrep for static analysis (covers injection, access control, crypto, and deserialization), OWASP ZAP for dynamic application security testing, and Trivy for dependency and infrastructure scanning. Additional free tools include Bandit for Python, Gitleaks for secrets detection, and SonarQube Community Edition for multi-language coverage.

Is the OWASP API Security Top 10 different from the main Top 10? Yes. The OWASP API Security Top 10 (2023 edition, still current) focuses on API-specific attack patterns. Its number one risk is Broken Object Level Authorization (BOLA), which targets how APIs expose internal object references. It also includes API-specific categories like Unrestricted Resource Consumption, Improper Inventory Management, and Unsafe Consumption of APIs that the main Top 10 does not cover directly.

Does the OWASP Top 10 cover AI and LLM vulnerabilities? OWASP maintains a separate Top 10 for Large Language Model Applications, updated for 2025. It covers AI-specific risks including prompt injection (the number one LLM risk), sensitive information disclosure, supply chain attacks on models and datasets, excessive agency in autonomous agents, and new categories for system prompt leakage and vector/embedding weaknesses in RAG systems.

How often is the OWASP Top 10 updated? The main OWASP Top 10 is updated roughly every three to four years. The 2025 edition is the eighth version, following the 2021, 2017, 2013, and 2010 editions. The API Security Top 10 follows a similar cycle and was last updated in 2023. The LLM Top 10 updates more frequently given the rapid evolution of AI technology, with its 2025 edition being the second version.


Keep reading:

Building a web application and want to make sure it's secure against the OWASP Top 10? Let's talk.