ASVS 5.0 V16.5.3 requires that the application fails gracefully and securely when an exception occurs. The example in the standard is a transaction that is processed despite an error in validation logic. That is a fail open condition. This is a Level 2 requirement.
The rule of thumb: the default result of any check must be denial. Only an explicit, successful check produces approval.
Why it matters
Code that sets approved = True first and flips it to false when a check fails will approve anything that throws before the flip. Attackers send inputs designed to cause exceptions: nulls, huge numbers, odd encodings. If the exception skips the check, they win.
How to implement it
Structure checks so approval happens at the end, and only on the happy path.
Risky:
def transfer(req):
ok = True
try:
validate(req) # raises on bad data
except ValueError:
ok = False
# a TypeError from validate() is not caught, but a bare except below hides it
...
Safer:
def transfer(req):
try:
validate(req)
check_limits(req.account, req.amount)
except Exception:
log.exception("transfer_rejected")
raise TransferRejected() # caller returns a generic 4xx or 5xx
return execute(req) # reached only if nothing raised
Other habits that help:
- Start permission variables as denied, and grant by assignment after a positive result.
- Use
finallyonly to release resources, never to complete a business action. - Make transactions atomic so a mid way error rolls back instead of leaving half a change.
- Treat unknown enum values, missing fields and parse errors as invalid input.
Put a test beside every guard that throws on purpose, and add a pull request gate so changes to guards must pass those tests.
Click through it at your own pace, or open it full screen.
How to test it
For each security check (validation, authorization, signature, limit), write tests that force failure in unusual ways: the dependency throws, the input is null, the number overflows, the JSON is truncated. Assert the outcome is rejection and that no side effect occurred, such as a row written or a message sent.
Fuzz the validator with random bytes and confirm the process never reports success for garbage.
Reading code for fail open
When reviewing, look for these shapes: a boolean that starts true, a catch block that returns or continues without rethrowing, a default branch that grants access, a missing else after a check, and checks placed after the side effect they should guard. Authorization middleware deserves special attention, because a thrown exception inside it should end the request, not pass it along.
Transactions and partial failure
Failing securely includes leaving data consistent. If validation passes but the ledger write fails, the system must not report success, and it must not leave a debit without a credit. Use database transactions or a saga with compensating steps, and make operations idempotent so a retry cannot double apply. Return a clear failure to the caller and log enough to reconcile.
Graceful for the user, strict for the system
Graceful means the user sees a calm, generic message and can retry; it does not mean the system proceeds. Keep these separate in the design. The user interface can be forgiving while the backend remains strict.
Common mistakes
except Exception: passaround a guard.- Returning true from a helper in the error branch "to keep things moving".
- Validation that runs after the action instead of before.
- Authorization middleware that calls
next()inside a catch block.
Quick check
What should the default value of an authorization flag be?
Denied. Set it to allowed only after a check completes successfully.