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)
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.
abc.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.