The OWASP Top 10 is a ranked list of the most important classes of web application security risk, published by the Open Worldwide Application Security Project. The 2025 edition, the eighth and the first update since 2021, was first published in November 2025. It is an awareness document, not a standard: it tells you which failure classes cause the most damage so you can spend effort where it counts, and it points to the OWASP Application Security Verification Standard (ASVS) when you need testable requirements.

This article explains each of the ten 2025 categories from first principles: what the failure is, how it is exploited, what vulnerable and fixed code look like, and how to test for it. It then walks one endpoint through the whole list and ends with a plan you can start this week. APIs have their own list, covered in the OWASP API Security Top 10.

What changed from 2021 to 2025

The 2025 list was built from data on 589 CWEs, the Common Weakness Enumeration entries that classify bugs, of which 248 are mapped into the ten categories, using roughly 175,000 CVE records mapped to CWEs. Eight categories come from that data and two from a community survey, because incident data lags real-world trends.

2025CategoryChange from 2021
A01Broken Access ControlStill first; Server-Side Request Forgery (A10:2021) merged in
A02Security MisconfigurationUp from fifth
A03Software Supply Chain FailuresNew; expands Vulnerable and Outdated Components
A04Cryptographic FailuresDown from second
A05InjectionDown from third
A06Insecure DesignDown from fourth
A07Authentication FailuresRenamed from Identification and Authentication Failures
A08Software or Data Integrity FailuresSame position; the name now says or instead of and
A09Security Logging and Alerting FailuresRenamed from Security Logging and Monitoring Failures
A10Mishandling of Exceptional ConditionsNew
Where the 2025 categories sit on a request pathBrowserA07 sessionsEdge and configA02 misconfigurationAuthenticationA07 failuresAuthorizationA01 incl. SSRFBusiness logicA06 insecure designError pathsA10 exceptionalQueries and outputA05 injectionData at restA04 cryptoDependencies, CIA03 supply chainArtifacts, updatesA08 integrityLogs and alertsA09 every layerRuntime risks sit on the request path; A03 and A08 enter through how the code is built and shipped; A09 watches all of it.
Most categories sit at a point on the request path; supply chain and integrity enter through the build; logging spans everything.

A01 Broken Access Control, now including SSRF

Access control decides whether this user may perform this action on this object. It breaks when the check is missing, done only in the UI, or checks the wrong thing. The classic form is an insecure direct object reference: GET /invoices/1043 returns invoice 1043 to anyone logged in, so changing the number reveals other customers' invoices.

# Vulnerable: authenticated, but never asks whose invoice this is.
@app.get("/invoices/<int:invoice_id>")
@login_required
def get_invoice(invoice_id):
    return jsonify(db.invoices.get(invoice_id))

# Fixed: scope the lookup to the caller, deny by default, same response for "missing" and "not yours".
@app.get("/invoices/<int:invoice_id>")
@login_required
def get_invoice(invoice_id):
    inv = db.invoices.find_one(id=invoice_id, account_id=current_user.account_id)
    if inv is None:
        abort(404)
    return jsonify(inv)

SSRF now lives here because it is the same mistake aimed at the network: the server fetches a URL the user supplied, and the attacker points it at http://169.254.169.254/ cloud metadata or an internal admin port. The server's network position is a privilege the user should not borrow. Fix it by allowlisting destinations, resolving the hostname and rejecting private, loopback and link-local addresses, connecting to the resolved IP so a second DNS answer cannot swap it, and disabling redirects.

Access control also breaks at the function level, when an admin action is reachable by any user who guesses its URL, and at the property level, when an update endpoint accepts a role or price field the caller should never set. The durable fix is architectural: put the decision in one policy layer that every handler calls, deny by default, derive ownership from the server-side session rather than from request parameters, and allowlist the fields each role may write. Then write tests that try each endpoint as the wrong user, because no scanner knows who should own what.

A02 to A04: misconfiguration, supply chain and cryptography

A02 Security Misconfiguration covers insecure defaults and drift: debug mode in production, default credentials, verbose stack traces, permissive CORS, public storage buckets, missing security headers and unneeded services. Its rise to second reflects how much of an application is now configuration. Treat configuration as code: keep it in version control, review it, and check it in CI with policy tools, with a hardened baseline per environment. Headers such as Content-Security-Policy are covered in CSP and security headers.

A03 Software Supply Chain Failures widens the old vulnerable-components category to everything between source and production: third-party packages, transitive dependencies, build tools, CI runners and registries. Attacks include typosquatted packages, dependency confusion between internal and public names, compromised maintainer accounts and poisoned build steps. Defences: lock files with hashes, a private registry or proxy with scoped names, an SBOM per release, automated vulnerability alerts with an owner, pinned CI actions and least-privilege CI tokens. See SBOMs and supply-chain security.

A04 Cryptographic Failures means data exposed because cryptography was missing, weak or misused: plaintext HTTP, passwords stored with a fast hash, home-made encryption, ECB mode, reused nonces, keys in source code. Rules: TLS everywhere; passwords with a slow, salted function such as Argon2id, scrypt or bcrypt; authenticated encryption such as AES-GCM through a vetted library; keys in a KMS, never in the repository.

# Passwords: a slow, salted, memory-hard hash; never sha256(password).
from argon2 import PasswordHasher
ph = PasswordHasher()                       # Argon2id with library defaults
stored = ph.hash(password)                  # salt and parameters encoded in the string
ph.verify(stored, attempt)                  # raises on mismatch
if ph.check_needs_rehash(stored):           # upgrade parameters on next login
    stored = ph.hash(attempt)

A05 Injection and A06 Insecure Design

A05 Injection happens when untrusted data is interpreted as code by an interpreter: SQL, a shell, LDAP, a template engine or the browser in the case of cross-site scripting. The cure is the same everywhere: keep data and code in separate channels. Use parameterized queries, pass argument lists instead of shell strings, and encode output for its context. Its fall to fifth reflects frameworks that make the safe path the default. Details in SQL injection.

# Vulnerable: the input becomes part of the SQL text.
cur.execute(f"SELECT * FROM users WHERE email = '{email}'")
# Fixed: the driver sends the value separately; it can never change the query's structure.
cur.execute("SELECT * FROM users WHERE email = %s", (email,))

# Vulnerable: shell parses the string.   Fixed: no shell, argument list.
subprocess.run(f"convert {name} out.png", shell=True)
subprocess.run(["convert", safe_path(name), "out.png"], check=True)   # safe_path: validated, ./-prefixed

A06 Insecure Design is a flaw in the design itself, which perfect code cannot fix: a password reset that relies on guessable security questions, a discount endpoint without a per-user limit, a checkout that trusts a client-side price. The fix happens before code, through threat modelling, abuse cases next to use cases, rate and quantity limits, and secure design patterns reused across teams. Start with threat modelling.

A07 to A10: authentication, integrity, logging and exceptional conditions

A07 Authentication Failures: credential stuffing with breached passwords, no rate limiting, weak recovery flows, session IDs that survive logout or are not rotated after login, and missing multi-factor authentication. Defences: MFA, preferably phishing-resistant passkeys; checking new passwords against breached lists; throttling and lockout by account and IP; regenerating the session on login and invalidating it on logout.

A08 Software or Data Integrity Failures: trusting code or data whose integrity was never verified, such as auto-updates without signatures, CI artifacts deployed without provenance, or deserializing untrusted input into objects. Sign and verify releases and container images, deploy only artifacts built by your pipeline, and never deserialize untrusted data with native object formats such as Python pickle or Java serialization; use JSON with a schema.

A09 Security Logging and Alerting Failures: attacks that succeed slowly because nobody is told. Logging alone is not enough; the rename stresses alerting. Log authentication events, access-control denials, input-validation failures and admin actions with user, source and request ID; protect logs from tampering and from containing secrets; and route a few high-signal patterns to a person. Good examples: a burst of access-denied responses from one account, a new administrator grant, a login from a new country followed by a password change. Test the path end to end by running the attack in staging and checking that a human is actually paged, not just that a line was written.

A10 Mishandling of Exceptional Conditions is new. It covers what happens off the happy path: errors, timeouts, retries, partial failures and resource exhaustion. Security checks that fail open, error messages that leak internals, and transactions left half-applied all belong here.

# Vulnerable: if the policy service times out, everyone is allowed in.
def can_access(user, resource):
    try:
        return policy.check(user, resource)
    except Exception:
        log.warning("policy check failed")
        return True                     # fails open

# Fixed: fail closed, keep the error generic for the client and detailed in the log.
def can_access(user, resource):
    try:
        return policy.check(user, resource, timeout=0.2)
    except Exception:
        log.exception("policy check failed", extra={"user": user.id, "resource": resource})
        return False

Worked example: one endpoint against the whole list

Worked example. Review one new endpoint, POST /api/exports, which takes a report ID and a webhook URL, generates a CSV, and posts it to the webhook. Walking the list turns a vague review into specific questions.

CategoryQuestion for this endpointFinding and fix
A01Can user A export user B's report? Can the webhook target internal hosts?Scope report lookup to the tenant; webhook allowlist and private-IP rejection
A02Does the export bucket allow public reads?Private bucket, short-lived signed URLs
A03Is the CSV library pinned and monitored?Lock file with hashes, SBOM entry
A04Is the CSV encrypted at rest and in transit?Bucket encryption; HTTPS-only webhooks
A05Can a cell start with =, +, - or @?Escape formula-leading cells to stop spreadsheet injection
A06Can one user trigger thousands of exports?Per-tenant quota and queue
A07Is the endpoint behind the same session and MFA rules?Yes; add re-authentication for bulk exports
A08Is the webhook payload signed so receivers can verify it?HMAC signature header
A09Will a burst of exports to new domains alert anyone?Log export plus destination; alert on new-domain spikes
A10What if the webhook times out mid-upload?Bounded retries, idempotency key, no partial file left public

Testing each category

No single tool covers the list. Map each category to a control and a test:

  • Static analysis (SAST) finds injection sinks, weak crypto calls and some error-handling patterns in code. See SAST and DAST.
  • Software composition analysis and SBOM diffing cover A03.
  • Dynamic scanning (DAST) finds misconfiguration, missing headers and reflected injection on a running app.
  • Authorization tests for A01 must be written by you: for every endpoint, call it as an anonymous user, as another tenant and as a low-privilege role, and assert denial. Scanners cannot know your ownership rules.
  • Fault injection for A10: make dependencies time out and error in tests, and assert that checks fail closed.
  • Design review and threat modelling for A06, and alert drills for A09: run a known attack in staging and confirm someone is paged.

Failure modes and trade-offs

The list is a prioritisation aid, so its main failure mode is being used as a checklist: a team marks ten boxes, passes an audit and still ships an IDOR because nobody tested ownership rules. Ranking is also based on incidence across many applications, not on your risk; a single-tenant internal tool and a multi-tenant SaaS have different top threats. Use the list for training and coverage, ASVS for verifiable requirements, and your own threat model for priorities. Effort trades off too: authorization tests and threat modelling are the expensive controls and also cover the highest-ranked categories, while scanners are cheap but find mostly the lower ones.

What to do next

  1. Write cross-tenant and cross-role authorization tests for your ten most sensitive endpoints this week.
  2. Find every place your servers fetch user-supplied URLs and add an allowlist with private-address rejection.
  3. Diff production configuration against a hardened baseline and add a CI policy check for it.
  4. Turn on dependency alerts with a named owner, enforce lock files with hashes, and produce an SBOM per release.
  5. Grep for password hashing, ECB, hard-coded keys and string-built SQL or shell commands, and fix each hit.
  6. Search for broad exception handlers around security checks and make each fail closed.
  7. Pick three attack signals, such as access-denied bursts, new admin grants and login failure spikes, and confirm each pages a person.
  8. Threat-model your next feature using the worked-example table above as the agenda.
Key takeaway: The OWASP Top 10:2025 keeps Broken Access Control first and folds SSRF into it, lifts Security Misconfiguration to second, adds Software Supply Chain Failures and Mishandling of Exceptional Conditions, and renames the authentication and logging categories. Treat it as a map of where damage comes from, not a compliance checklist: write your own authorization tests, keep data and code separate against injection, fail closed on errors, verify what you build and deploy, and make sure attacks page a person.