The XSS Rat OWASP Lab by The XSS Rat
A10:2025 Medium

Mishandling of Exceptional Conditions

Theory

New in the 2025 edition. These weaknesses used to be scattered under the vague heading of "poor code quality". The 2025 list gives them their own category covering 24 CWEs: improper error handling, logical errors, failing open, and other fallout from abnormal conditions.

An application mishandles an exceptional condition when it does one of three things: it fails to prevent the unusual situation, fails to detect it while it is happening, or responds to it inadequately afterwards. The bug is rarely the error itself — it is what the code does next.

Notable CWEs

  • CWE-209 — Generation of an error message containing sensitive information
  • CWE-636 — Not failing securely ("failing open"): the check errors out, and the caller is let through
  • CWE-476 — NULL pointer dereference
  • CWE-234 — Failure to handle a missing parameter
  • CWE-274 — Improper handling of insufficient privileges

Attack Scenarios

  • Denial of service — an upload handler catches the exception but never releases the file handle or temp file. Every error leaks a resource; enough errors take the service down.
  • Information disclosure — a raw database error is rendered to the user, handing the attacker table names, driver versions and query structure, which sharpens the next injection attempt.
  • Financial fraud — an attacker interrupts a multi-step transfer mid-flight. With no rollback, one account is debited and the other is never credited.

Vulnerable Code Pattern

# VULNERABLE 1 — the error message is the vulnerability (CWE-209)
@app.get("/convert")
def convert(amount: str):
    try:
        return {"usd": float(amount) * RATE}
    except Exception as exc:
        # hands the caller the traceback, the config and the stack frame
        return {"error": repr(exc), "config": settings.dict(), "trace": traceback.format_exc()}

# VULNERABLE 2 — fails OPEN (CWE-636): the exception grants the thing it should deny
def has_valid_licence(token: str) -> bool:
    try:
        return licence_service.verify(token)       # network call, can raise
    except Exception:
        return True        # "don't punish the user for our outage" — now nobody needs a licence

Fixed Code Pattern

# FIXED 1 — log the detail, return a correlation id, never the internals
@app.get("/convert")
def convert(amount: str):
    try:
        return {"usd": float(amount) * RATE}
    except ValueError:
        raise HTTPException(400, "amount must be a number")      # expected, specific
    except Exception:
        ref = uuid4().hex
        log.exception("convert failed ref=%s", ref)              # detail goes to the log
        raise HTTPException(500, f"internal error (ref {ref})")  # caller gets an id

# FIXED 2 — fail CLOSED, and alert on the condition
def has_valid_licence(token: str) -> bool:
    try:
        return licence_service.verify(token)
    except Exception:
        log.error("licence service unreachable — denying by default")
        alert("licence-service-down")       # the outage is an incident, not a free pass
        return False

How to Fix — Checklist

  • Catch narrowly, where it happens — handle the specific exception at the call site rather than wrapping half the app in except Exception
  • Fail closed — when a security decision cannot be made, the answer is "no". Deny, log, alert.
  • Roll the whole transaction back — partial recovery is how money goes missing; restart the operation instead of patching it mid-flight
  • Return a reference, not a reason — the user gets an opaque id, the log gets the stack trace
  • Cap the blast radius — rate limits, quotas and throttling keep an exceptional condition from becoming an outage
  • Centralise the handler — one error path for the whole application, so behaviour cannot drift per endpoint
  • Alert on error patterns — a spike of the same handled exception is an attack signal, not noise (see A09:2025)
Challenge 1

Verbose Error — Leak internals through an unhandled type (CWE-209)

The currency converter expects a number. Give it something that is not a number and the handler dumps the exception, the stack frame and the service configuration straight into the response.

Steps
Submit a non-numeric amount, e.g. abc.
Challenge 2

Fail-Open Licence Check — Break the validator to get in (CWE-636)

Premium export is gated behind a licence check that calls an external validator. The validator rejects malformed input by raising — and the except branch was written to let the user through rather than block them. Send a token the validator cannot parse.

Steps
The validator raises on a token longer than 64 characters. Paste a long string of junk.