LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

The OWASP Top 10 (2025 Edition): What Changed and How to Fix Each Risk

owaspvulnerabilitiesweb-securityapplication-securitysupply-chaininjectionaccess-controlsecure-coding

The OWASP Top 10 is the closest thing web security has to a shared vocabulary, and in 2025 it changed in ways worth paying attention to: two entirely new categories appeared, Security Misconfiguration jumped from #5 to #2, and Server-Side Request Forgery — its own entry in 2021 — got folded back into Broken Access Control. If your mental model or your scanner config still maps to the 2021 list, you are now describing risks with the wrong names and missing two categories outright. The list is not gospel; it is a data-driven snapshot of what is actually getting exploited in real applications. Here is the 2025 list, what moved and why, and the concrete fix for each item.


What Changed From 2021

The reshuffle is driven by OWASP’s analysis of real vulnerability data plus a survey of practitioners. Three structural changes matter most: Software Supply Chain Failures (A03) is a new, broader successor to the old “Vulnerable and Outdated Components,” reflecting that attacks now target build systems and distribution channels, not just outdated libraries; Mishandling of Exceptional Conditions (A10) is brand new, covering error-handling bugs and fail-open logic; and SSRF lost its standalone spot, absorbed into Broken Access Control.

2025 Category 2021 origin
A01 Broken Access Control A01 (now also absorbs SSRF)
A02 Security Misconfiguration A05 (up from #5)
A03 Software Supply Chain Failures New (expands A06 Vulnerable/Outdated Components)
A04 Cryptographic Failures A02
A05 Injection A03
A06 Insecure Design A04
A07 Authentication Failures A07 (renamed)
A08 Software or Data Integrity Failures A08
A09 Security Logging & Alerting Failures A09 (renamed; “Alerting”)
A10 Mishandling of Exceptional Conditions New
  2021                          2025
  --------------------          --------------------
  A01 Broken Access Ctrl  ───▶  A01 Broken Access Ctrl  ◀── A10 SSRF merges in
  A02 Cryptographic        ╲    A02 Security Misconfig  ◀── (was A05, promoted)
  A03 Injection             ╲   A03 Supply Chain        ◀── NEW (grows from A06)
  A04 Insecure Design        ╲  A04 Cryptographic
  A05 Security Misconfig   ───╳  A05 Injection
  A06 Vulnerable Comps    ───╯   A06 Insecure Design
  A07 Auth Failures        ───▶  A07 Authentication
  A08 Data Integrity       ───▶  A08 Software/Data Integrity
  A09 Logging Failures     ───▶  A09 Logging & Alerting
  A10 SSRF  ──(merged into A01)  A10 Mishandling Exceptions  ◀── NEW

The takeaway from the movement: the industry’s pain has shifted from “we wrote an injectable query” toward “we trusted something we shouldn’t have” — a misconfigured default, a compromised dependency, an error path that failed open. The new entries reward defense at the boundaries of your system, not just inside your request handlers.


A01: Broken Access Control

Still #1, still the most exploited class of bug: users acting outside their intended permissions. The classic case is an endpoint that trusts an ID from the URL without checking who is asking.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Vulnerable: any authenticated user can read any user's data (IDOR)
@app.get("/api/users/{user_id}/data")
def get_user_data(user_id: int):
    return db.get_user_data(user_id)

# Fixed: authorize the action against the caller, server-side
@app.get("/api/users/{user_id}/data")
def get_user_data(user_id: int, current_user: User = Depends(auth)):
    if current_user.id != user_id and not current_user.is_admin:
        raise HTTPException(403)
    return db.get_user_data(user_id)

Because SSRF now lives here, the same “don’t trust attacker-supplied targets” principle extends to outbound requests: if a user controls a URL your server fetches, they can reach your cloud metadata endpoint or internal services.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# SSRF defense: allowlist destinations; block internal/link-local ranges
import ipaddress, socket
from urllib.parse import urlparse

ALLOWED_HOSTS = {"api.trusted.com"}

def safe_fetch(url: str):
    host = urlparse(url).hostname
    if host not in ALLOWED_HOSTS:
        raise HTTPException(400, "destination not allowed")
    ip = ipaddress.ip_address(socket.gethostbyname(host))
    if ip.is_private or ip.is_loopback or ip.is_link_local:
        raise HTTPException(400, "internal address blocked")
    return requests.get(url, timeout=5).text

Deny by default, enforce authorization on the server (never in the client), and prefer access checks at a central chokepoint over scattering them across handlers.


A02: Security Misconfiguration

The biggest mover, promoted to #2 — because the average system now has far more knobs (cloud IAM, container runtimes, framework defaults, CI/CD), and the defaults are frequently insecure. Default credentials, an exposed admin panel, an S3 bucket set to public, a verbose stack trace handed to an attacker, CORS set to *.

  • Change every default credential; disable features and ports you don’t use.
  • Return generic error pages in production — never raw stack traces (which ties directly into A10).
  • Treat configuration as reviewed, version-controlled artifacts, and scan it: trivy config, tfsec, or kube-score catch the obvious holes before they ship. This is the same discipline covered in container security.

A03: Software Supply Chain Failures (New)

The successor to “Vulnerable and Outdated Components,” widened to cover the whole chain: the dependencies you pull, the build system that assembles them, and the registry that distributes the result. The 2025 framing acknowledges that attackers increasingly compromise upstream — a poisoned npm package, a typosquatted PyPI name, a backdoored build step — rather than waiting for you to run an old version.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Audit declared dependencies
npm audit --audit-level=high
pip-audit
osv-scanner -r .

# Generate a bill of materials so you know what you actually ship
syft dir:. -o spdx-json > sbom.json

# Verify provenance/signatures of what you pull and publish
cosign verify-attestation --type slsaprovenance myregistry.io/app@sha256:...

Defenses: pin dependencies with a committed lockfile, pull from a vetted internal registry rather than the public one directly, verify signatures and SLSA provenance, and gate builds in CI/CD on an SBOM diff so a new transitive dependency can’t sneak in unreviewed.


A04: Cryptographic Failures

Was #2 in 2021; the risk is unchanged even if the rank slipped — sensitive data exposed through weak or absent cryptography.

  • Use TLS 1.3 in transit (see HTTPS and TLS explained); encrypt sensitive data at rest.
  • Hash passwords with Argon2id or bcrypt — never a bare SHA-256, never MD5.
  • Don’t roll your own crypto; use vetted libraries and authenticated encryption (AES-GCM, not raw AES).
  • Don’t log secrets, and keep keys out of source — manage them with a real secrets manager.

A05: Injection

SQL, NoSQL, OS command, LDAP — untrusted input interpreted as code. The fix is the same as it has been for two decades, and it works: never concatenate input into a command; let the interpreter separate code from data.

1
2
3
4
5
# Vulnerable: string interpolation into SQL
query = f"SELECT * FROM users WHERE id = {user_input}"

# Fixed: parameterized query, the driver handles escaping
cursor.execute("SELECT * FROM users WHERE id = ?", (user_input,))

Use parameterized queries / prepared statements everywhere, prefer an ORM’s safe builders, and validate input against an allowlist. For OS commands, pass argument arrays rather than a shell string, and avoid spawning a shell at all where you can.


A06: Insecure Design

Flaws baked into the architecture, where no amount of clean implementation saves you — a password-reset flow with no rate limit, a checkout that trusts a client-side price, tenants that share a trust boundary they shouldn’t.

  • Threat model before you build; ask “how would an attacker abuse this flow?” at design time.
  • Use secure design patterns: enforce business limits server-side, separate tenants, fail closed.
  • Add abuse-case tests alongside functional tests so the design intent is verified, not assumed.

A07: Authentication Failures

Renamed from “Identification and Authentication Failures.” Weak or broken proof of identity: credential stuffing, brittle password rules, guessable session tokens, no MFA.

  • Offer MFA and prefer phishing-resistant factors (passkeys/WebAuthn) over SMS.
  • Rate-limit and add backoff on login; check passwords against known-breached lists instead of forcing arcane composition rules.
  • Generate long, random session IDs; rotate on privilege change; set Secure, HttpOnly, SameSite cookies and a sane idle timeout.

A08: Software or Data Integrity Failures

Trusting code or data whose integrity you never verified — an auto-update that doesn’t check signatures, deserialization of untrusted objects, a CI pipeline that anyone can inject steps into. This sits one level below A03: where supply chain is about the whole ecosystem, this is about your trust boundaries and verification.

  • Verify signatures on updates and artifacts before executing them.
  • Never deserialize untrusted data into live objects (Java/Python pickle/PHP are classic footguns); use a data-only format and validate it.
  • Lock down who can modify pipeline definitions; treat the pipeline as production infrastructure.

A09: Security Logging & Alerting Failures

Renamed to add “Alerting” — because a log nobody acts on is theater. The 2025 emphasis: capturing security events is necessary but not sufficient; you must alert on them fast enough to respond.

Log (and alert on): authentication successes and failures, access-control denials, input-validation failures, and high-value actions. Then make the logs useful — centralized, tamper-resistant, with enough context to investigate, and wired to alerts that page a human on the patterns that matter. Capturing the event and discovering it three months later in a breach report is the failure this category names.


A10: Mishandling of Exceptional Conditions (New)

The other new entry, and an easy one to overlook: bugs that emerge when something goes wrong — improper error handling, logic that fails open, swallowed exceptions, and abnormal states the code never anticipated. The canonical disaster is an auth check that grants access when it errors instead of denying it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Vulnerable: on any error, the function "fails open" and allows access
def is_authorized(user, resource):
    try:
        return policy.check(user, resource)
    except Exception:
        return True   # error path silently grants access

# Fixed: fail closed, log the anomaly, surface a generic error
def is_authorized(user, resource):
    try:
        return policy.check(user, resource)
    except Exception:
        log.error("authz check failed", exc_info=True)
        return False  # deny on uncertainty

Fail closed by default, never leak internal detail in error responses (that overlaps with A02), handle the unexpected explicitly instead of catching-and-ignoring, and make sure a partial failure can’t leave the system in a permissive state.


Testing for All Ten

No single tool covers the list; layer them:

Technique Catches Tools
SAST (static analysis) Injection, crypto misuse, insecure patterns Semgrep, CodeQL
DAST (dynamic testing) Access control, misconfiguration, runtime flaws OWASP ZAP, Burp
SCA (dependency scanning) A03 supply chain, vulnerable components osv-scanner, Trivy, Snyk
Config / IaC scanning A02 misconfiguration tfsec, trivy config, kube-score
Penetration testing Design flaws, chained exploits Human testers

Wire SAST and SCA into the pipeline so they gate merges, run DAST against staging, and schedule periodic manual pentests for the logic flaws automation can’t see.


Verdict

The 2025 refresh tells a clear story: the soft spots have moved outward. A decade ago the list was dominated by what you typed into a request handler; today three of the highest-impact categories — Security Misconfiguration at #2, the new Software Supply Chain Failures at #3, and Mishandling of Exceptional Conditions at #10 — are about what your system trusts and how it behaves when something goes wrong. Injection is still here and parameterized queries still fix it, but the marginal risk now lives in your defaults, your dependencies, and your error paths.

Use the list the way it’s meant to be used: as a prioritized baseline, not a compliance checklist. Get the durable habits right — deny by default and authorize server-side, treat configuration and dependencies as reviewed artifacts, fail closed and alert on what matters — and you cover most of all ten at once. Then layer SAST, SCA, DAST, and the occasional human pentest so the categories you can’t reason about in your head get caught by something that can. The names changed in 2025; the discipline didn’t.


Sources

Comments