A10:2025 — NEW

Mishandling of Exceptional Conditions

A brand-new category in OWASP Top 10:2025 that formalizes what security engineers have known for years: the way your application handles errors is itself a security decision — and most applications get it wrong.

Why this category is new in 2025

Error handling has always appeared as a sub-issue inside other OWASP categories. A stack trace leaking database credentials was filed under Security Misconfiguration. An uncaught exception that bypassed an auth check was logged as Broken Access Control. An inconsistent error response that fingerprinted the server version was another misconfiguration finding. The 2025 OWASP Top 10 working group pulled all of these threads together and recognized that they share a root cause: applications are built for the happy path, and exceptional conditions are an afterthought.

The data that pushed this into a dedicated category was striking. In OWASP's 2024 community survey, error handling flaws appeared in roughly 40% of assessed applications — but they rarely appeared in isolation. They amplified other vulnerabilities. A SQL injection was more exploitable because verbose errors confirmed the injection point. An IDOR was harder to detect because inconsistent error codes masked unauthorized access. Exceptional condition mishandling isn't just a vulnerability class on its own; it makes everything else worse.

By giving it a named category, OWASP is signaling that developers and security teams should test for this explicitly, not wait for it to surface as a side-effect of other findings.

Real-world impact

Stack trace exposure — Revealed internal file paths, framework versions, database connection strings, and class names. Common in early-stage startups that shipped with debug mode enabled.

Fail-open on exception — Payment and access control logic that crashed silently and defaulted to allowing the operation. Multiple fintech incidents where error paths skipped authorization checks entirely.

Error-based information disclosure — Timing differences and error message variations used to enumerate usernames, valid API keys, and internal object IDs at scale.

Race conditions in error cleanup — Partially completed operations during exceptions left application state inconsistent, allowing double-spend and privilege escalation in several high-profile bug bounty reports.

Stack trace exposure

This is the most common manifestation. A request triggers an unhandled exception — a null pointer, a missing configuration value, a database timeout — and instead of returning a generic 500 error, the application dumps a full stack trace to the HTTP response.

What's in a typical stack trace? Quite a bit, from an attacker's perspective:

In 2022, a major e-commerce platform's staging endpoint was accessible from the internet with debug mode enabled. A researcher discovered that malformed JSON in the shipping address field triggered an unhandled Jackson deserialization exception containing the full Spring Boot stack trace — including the database hostname, the internal microservice mesh topology, and the fact that they were running a version of Log4j vulnerable to Log4Shell. The entire attack chain started with a bad error message.

The fix sounds trivial: catch exceptions, return generic error messages, log details server-side. But at scale, this is genuinely hard. Developer tooling that makes debugging easy — verbose errors, detailed exceptions, helpful stack traces — has to be systematically stripped out before production. And it keeps getting re-introduced. A new developer enables debug mode to troubleshoot something. A library update changes how exceptions propagate. A single uncaught code path slips through code review.

Inconsistent error handling as an oracle

Even when you don't expose stack traces, inconsistent error behavior leaks information. This is subtler and more dangerous because it's easy to miss in testing.

Consider a login endpoint. If it returns "Invalid password" for a valid username with wrong password, and "User not found" for an invalid username, you've just given attackers a free username enumeration oracle. They can pre-validate a list of millions of email addresses without ever succeeding at a login.

This pattern appears everywhere:

Timing attacks are a particularly nasty subset of this. If your password hashing short-circuits early for usernames that don't exist (skipping bcrypt entirely), a timing measurement reveals whether the user is real. Libraries like Python's hmac.compare_digest() or Go's subtle.ConstantTimeCompare() exist specifically to prevent this, but they only work if you use them — and they have to be used correctly throughout the entire code path, not just at the final comparison.

Fail-open: when exceptions become security bypasses

The most dangerous category. When exception handling defaults to permitting an operation rather than denying it, errors in the security logic itself create vulnerabilities.

Here's a representative example from a bug bounty report (sanitized):

def check_subscription(user_id, feature):
    try:
        plan = db.get_user_plan(user_id)
        return plan.has_feature(feature)
    except DatabaseException:
        # DB is down, don't block the user
        return True

The developer's reasoning was operationally sensible: if the database is temporarily unavailable, we shouldn't lock legitimate users out. But the consequence is that during any database disruption — even an attacker-induced one — all subscription checks return True, granting access to premium features. One researcher found they could trigger this by sending a malformed user_id that caused a different database exception, effectively granting themselves premium access by exploiting error handling.

Similar patterns appear in authentication middleware:

// Dangerous: exception in JWT validation falls through
app.use((req, res, next) => {
    try {
        req.user = jwt.verify(req.headers.authorization, secret);
    } catch (e) {
        // token might be expired, try to continue anyway
        console.log('JWT error:', e.message);
    }
    next(); // always continue
});

This middleware catches the JWT verification exception and continues the request anyway. Any downstream handler that doesn't double-check req.user is now accessible without authentication. This exact pattern appeared in a CVE disclosed in 2023 affecting a popular Node.js SaaS template that thousands of developers had forked.

The principle here is called fail-closed: when something goes wrong in your security logic, the safe default is to deny access and throw an error upward, not to grant access and hope for the best. Exception handling in security-critical code paths should be explicit about what it's denying, not optimistic about what it allows.

Uncaught exceptions and application state corruption

Exceptions that propagate to the top of the call stack — or to a global handler — can leave application state partially modified. This is especially dangerous in multi-step operations: financial transactions, user registration flows, role assignment, and anywhere atomicity matters.

Imagine an account upgrade flow:

  1. Charge the payment method
  2. Update the user's plan in the database
  3. Send a confirmation email
  4. Invalidate cached user object

If an exception occurs at step 4 — say, a Redis connection error — and the exception handler doesn't properly roll back steps 1-3, you have a user who was charged and upgraded but whose cached session still shows the old plan. If the exception is at step 2, you have a charged user who never got their upgrade. In a business logic sense, these are failures. But from an adversarial perspective, an attacker who can deliberately trigger exceptions at specific steps in multi-step flows may be able to achieve state that would normally be impossible: double-spend a coupon, get an upgrade without being charged, trigger a refund without losing access.

In 2021, a cryptocurrency exchange had a race condition in their withdrawal flow where a specific network error during transaction broadcast would trigger an exception. The exception handler returned an error to the user but didn't mark the withdrawal as failed in the database — it remained in "pending" state. An attacker who triggered withdrawals and then caused the network exception at exactly the right moment could initiate multiple withdrawals from the same pending transaction.

Race conditions in error paths

Error handling code is almost never tested for concurrent access. The main path gets load tested and reviewed; the catch blocks don't. This creates a class of race conditions that exist specifically in exception paths.

Time-of-check to time-of-use (TOCTOU) races often happen precisely because exception handling adds retry logic. A lock is released when an exception occurs, then re-acquired on retry — but between those two moments, another request can observe inconsistent state. Resource cleanup in finally blocks can introduce similar windows: the resource is being cleaned up, another thread tries to use it, gets an error, triggers its own exception handler, which also tries to clean up the same resource.

These bugs are notoriously hard to find with static analysis or normal testing. They require either fuzzing with deliberate fault injection or careful code review of every exception path in the context of concurrent access. Most teams do neither.

Error-based information disclosure in APIs

REST and GraphQL APIs are particularly prone to this. When a query or request fails, the error response often contains more information than intended:

// GraphQL error leaking schema internals
{
  "errors": [{
    "message": "Cannot query field \"adminNotes\" on type \"User\".
                Did you mean \"notes\"?",
    "locations": [{"line": 3, "column": 5}]
  }]
}

This error confirms that an adminNotes field exists on the User type — it's just not accessible from this context. Combined with GraphQL introspection (if enabled) or systematic field enumeration, error messages like this allow an attacker to discover the full schema including private fields. In several disclosed vulnerabilities, this technique revealed internal fields containing PII, admin flags, and security-relevant metadata that were never intended to be visible to end users.

Database errors surfaced through ORM exceptions are another common vector. If your API directly maps database exceptions to HTTP responses, an attacker can infer table structure from unique constraint violations, determine which fields are indexed from error timing, and identify foreign key relationships from referential integrity errors.

How to find these vulnerabilities

Testing for exceptional condition mishandling requires actively exercising the error paths your normal tests skip:

Look for stack traces, debug output, internal object IDs, file system paths, SQL fragments, server version strings, or any internal implementation detail in error responses. Compare error messages across similar operations — differences are often intentional information disclosure that the developers didn't recognize as a security issue.

Mitigation and secure coding practices

Most of these issues have well-understood fixes. The challenge is consistency:

The underlying culture shift OWASP is trying to drive with this category is to treat error handling as a first-class security concern. Happy-path thinking — where security is validated on the successful execution path and error paths are assumed to be unreachable in adversarial conditions — is exactly the kind of insecure design that attackers have exploited for decades. Exceptional conditions are not exceptional from an attacker's perspective. They're a systematic probe surface.

Test your error handling

Our AI-powered scanner probes your application's error paths — malformed inputs, boundary conditions, timing oracles, and verbose error responses — across all OWASP A10:2025 attack vectors.

Test your error handling