A10 Mishandling of Exceptional Conditions
Security often depends on what happens when something goes wrong. If an error means "allow", a stack trace means "here is my code", or a half-finished operation means "corrupted state", the failure path is the vulnerability.
What it is
Mishandling of exceptional conditions is improper preparation for, detection of, and response to unusual situations: failed dependencies, timeouts, malformed input, resource exhaustion, partial writes, and unexpected states. The typical mistakes are failing open, leaking details in error messages, swallowing exceptions, and leaving transactions half done.
It is a new category in :2025 (A10), and it ties together several older weaknesses around error handling.
How the attack works
- Fail open: an authorization service times out and the code treats the exception as "no restriction".
- Information leak: a database error is returned to the client with the table name, query and driver version.
- Swallowed errors: an empty catch hides a failed signature check and processing continues.
- Partial state: a payment is charged but the order write fails; retrying duplicates the charge.
- Resource exhaustion: an unbounded input or an unhandled exception in a loop lets one request take the service down.
Vulnerable code
The check fails open, the error leaks internals, and the multi-step update is not atomic.
def can_access(user, doc):
try:
return policy_service.allowed(user.id, doc.id)
except Exception:
return True # service is down, so let them in
@app.errorhandler(Exception)
def on_error(e):
return str(e), 500 # leaks internals to the clientawait accounts.debit(from, amount);
await accounts.credit(to, amount); // if this throws, the money has vanishedThe fix
: on any doubt about a security decision, deny. Return a generic message with a to the client and keep the details in the logs. Make multi-step changes atomic with a transaction, and bound everything: time, size, retries, memory.
def can_access(user, doc):
try:
return policy_service.allowed(user.id, doc.id, timeout=0.5)
except Exception:
log.exception("policy check failed", extra={"user": user.id, "doc": doc.id})
return False # fail closed
@app.errorhandler(Exception)
def on_error(e):
rid = uuid4().hex
log.exception("unhandled error", extra={"request_id": rid})
return {"error": "internal error", "request_id": rid}, 500await db.transaction(async (trx) => {
await accounts.debit(from, amount, trx);
await accounts.credit(to, amount, trx); // both happen or neither does
});ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
defer cancel()
resp, err := policy.Check(ctx, user, doc)
if err != nil {
log.Error("policy check failed", "err", err)
http.Error(w, "forbidden", http.StatusForbidden) // fail closed
return
}Prove the fix
Inject the failure on purpose. Make the dependency time out and assert that access is denied; make the second step throw and assert that nothing changed; trigger an error and assert that the response carries no internals. Fuzzing adds odd inputs you did not think of.
def test_denies_when_policy_service_is_down(monkeypatch, user, doc):
monkeypatch.setattr(policy_service, "allowed", lambda *a, **k: (_ for _ in ()).throw(TimeoutError()))
assert can_access(user, doc) is False
def test_error_response_has_no_internals(client, monkeypatch):
monkeypatch.setattr(db, "query", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("relation users_secret does not exist")))
body = client.get("/api/things").get_json()
assert "users_secret" not in str(body) and "request_id" in body
@given(st.text())
def test_parser_never_crashes_on_any_text(s): # Hypothesis
parse_input(s) # may reject (ValueError) but must not raise anything elsetest("a failed credit rolls back the debit", async () => {
jest.spyOn(accounts, "credit").mockRejectedValue(new Error("boom"));
const before = await accounts.balance(from);
await expect(transfer(from, to, 50)).rejects.toThrow();
expect(await accounts.balance(from)).toBe(before);
});Checklist
- Decide the failure behavior of every security check up front: it should deny.
- Catch narrowly; never leave an empty catch; log with context and re-raise or return a safe error.
- Return generic errors with a correlation id; keep stack traces and queries in logs only.
- Use transactions and idempotency keys for multi-step operations, so retries are safe.
- Set timeouts, size limits, rate limits and retry budgets on every external call and input.
- Practice failure: fault injection, fuzzing and game days reveal fail-open paths before attackers do.
connections on the map
- A09 Security Logging and Alerting FailuresrecordsA10 Mishandling of Exceptional Conditions
updated on 14/10/2026 · sources to go deeper · report an error
in other domains
- Reliability at scale: idempotency, retries and backpressuresecengfail closed and idempotently
Study before
test what you learned
Loading questions…