Brand new category for 2025, replacing SSRF (absorbed into A01). Contains 24 CWEs with 769,581 recorded occurrences. Addresses programs that fail to prevent, detect, and respond to unusual situations — leading to crashes, unexpected behaviour, information disclosure, and exploitable states.
Core Concepts
What It Covers
| Weakness | Risk |
|---|---|
| Improper error handling | Unhandled exceptions crash the app or expose internals |
| Failing open (CWE-636) | Auth/authz checks that pass when an exception is thrown |
| Sensitive data in error messages (CWE-209) | Stack traces, DB details, file paths leaked to clients |
| Uncaught exceptions (CWE-248) | No global safety net → 500 with debug output |
| Resource exhaustion in exception paths | DB connections, file handles never closed on error |
| State corruption | Partial transactions committed on exception |
| NULL pointer / None dereference (CWE-476) | Missing guard for None before attribute access |
| Missing default cases (CWE-754) | if/elif chains with no else; match with no wildcard |
| Race conditions in error recovery | Cleanup logic itself can be interrupted |
Prevention Principles
- Catch errors where they occur — at the function that can handle them, not three layers up.
- Fail closed — when auth/authz logic throws, deny access; never pass through.
- Roll back completely — partial DB transactions must be rolled back, never committed.
- Separate user messages from technical details — log the stack trace server-side; return a safe, generic message to the client.
- Global exception handler as safety net — every app needs one, but it is not a substitute for local handling.
- Rate-limit and quota — prevent exceptional conditions (e.g., resource exhaustion) from being triggered intentionally.
FastAPI Testing Checklist
1. Global Exception Handler
# REQUIRED — catches anything not handled locally
@app.exception_handler(Exception)
async def global_handler(request: Request, exc: Exception):
logger.error("Unhandled exception", exc_info=exc)
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
Flag if absent. The default FastAPI 500 response may include traceback in development mode.
2. Fail-Closed Auth Middleware (CWE-636)
# BAD — exception causes pass-through (fail open)
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
try:
verify_token(request)
except Exception:
pass # ← VULNERABILITY
return await call_next(request)
# GOOD — exception causes denial (fail closed)
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
try:
verify_token(request)
except Exception as e:
logger.warning("Auth check failed", exc_info=e)
return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
return await call_next(request)
3. Custom HTTP Error Responses (CWE-756 / CWE-209)
# Register handlers for all common error codes
@app.exception_handler(404)
async def not_found(request, exc):
return JSONResponse(status_code=404, content={"detail": "Not found"})
@app.exception_handler(422)
async def validation_error(request, exc):
return JSONResponse(status_code=422, content={"detail": "Invalid input"})
Flag any handler that returns str(exc), repr(exc), or traceback.format_exc() in the response body.
4. Resource Cleanup in Exception Paths
# BAD — connection leaked if query raises
conn = db.connect()
result = conn.execute(query) # raises → conn never closed
conn.close()
# GOOD — context manager guarantees cleanup
with db.connect() as conn:
result = conn.execute(query)
Scan for: db.connect(), open(file), socket.socket() not inside a with block or try/finally.
5. SQLAlchemy Transaction Rollback
# BAD — partial write committed on error
session.add(obj)
risky_operation() # raises → session still commits elsewhere
session.commit()
# GOOD — rollback on exception
try:
session.add(obj)
risky_operation()
session.commit()
except Exception:
session.rollback()
raise
Flag: any session.commit() not paired with session.rollback() in an except branch.
Flask Testing Checklist
1. Global Exception Handler
@app.errorhandler(Exception)
def global_error(e):
app.logger.error("Unhandled exception", exc_info=e)
return {"error": "Internal server error"}, 500
Also register specific handlers:
@app.errorhandler(404)
def not_found(e):
return {"error": "Not found"}, 404
@app.errorhandler(500)
def server_error(e):
return {"error": "Server error"}, 500
2. Flask Debug Mode — Doubles as A02 + A10 Issue
Debug mode (debug=True or FLASK_DEBUG=1) enables the Werkzeug interactive debugger, which:
- Exposes full tracebacks to the browser.
- Allows arbitrary code execution via the debugger console.
Flag immediately if found in production config or environment variables.
3. request.args.get() Silent None Returns
# BAD — returns None silently; downstream code may crash
user_id = request.args.get("user_id")
user = db.query(User).filter_by(id=user_id).first() # id=None → wrong query
# GOOD — explicit validation
user_id = request.args.get("user_id")
if user_id is None:
abort(400, description="user_id is required")
FastAPI's Pydantic models prevent this automatically; Flask requires manual guards.
4. Fail-Closed before_request Hooks
# BAD — exception in auth check bypasses protection
@app.before_request
def check_auth():
try:
verify_session()
except:
pass # ← VULNERABILITY
# GOOD
@app.before_request
def check_auth():
try:
verify_session()
except Exception as e:
app.logger.warning("Session verify failed", exc_info=e)
abort(401)
Automated Scanning Approach
Static Analysis (Bandit)
bandit -r . -t B110,B112 # try-except-pass and try-except-continue
bandit -r . --severity-level medium
Key Bandit test IDs for this category:
B110—try: ... except: pass(swallowed exception)B112—try: ... except: continue(swallowed in loop)
Pattern Search (grep / ripgrep)
# Fail-open patterns
rg "except.*:\s*(pass|continue|return True)" --type py
# Stack traces leaking to response
rg "traceback\.format_exc\(\)|str\(exc\)|repr\(exc\)" --type py
# Missing context managers
rg "open\(|\.connect\(\)" --type py # review each hit for 'with'
# Missing rollback
rg "session\.commit\(\)" --type py # check each for paired rollback
Runtime / DAST
- Trigger deliberate 404, 500, and validation errors (422 on FastAPI).
- Inspect HTTP responses for presence of: file paths, stack frames, SQL query strings, exception class names.
- Probe middleware by sending a malformed Authorization header — verify the response is
401, not200.
Key CWEs (A10:2025)
| CWE | Name | Python Manifestation |
|---|---|---|
| CWE-209 | Sensitive Info in Error Messages | str(exc) in JSON response, debug traceback |
| CWE-248 | Uncaught Exception | No global handler; unhandled raise crashes app |
| CWE-636 | Failing Open | except: pass in auth check |
| CWE-703 | Improper Handling of Exceptional Conditions | Broad except that hides errors |
| CWE-754 | Improper Check for Unusual Conditions | Missing None/empty checks on external input |
| CWE-476 | NULL Pointer Dereference | Attribute access on potentially-None object |
| CWE-756 | Missing Custom Error Page | Default framework error pages with debug info |
Remediation Priority
- Critical: Fail-open auth middleware /
before_requesthooks (CWE-636) — direct authentication bypass. - Critical: Debug mode enabled in production (Werkzeug debugger = RCE).
- High: Stack traces / DB details in HTTP responses (CWE-209) — aids attacker reconnaissance.
- High: Missing global exception handler — unhandled paths crash app and may reveal details.
- Medium: Missing transaction rollback — data integrity corruption.
- Medium: Resource leaks in exception paths — connection pool exhaustion under attack.
- Low: Silent
Nonereturns fromrequest.args.get()— logic bugs, not directly exploitable.