Back to Cheat Sheets

🧯 Improper Exception Handling & Verbose Error Messages

OWASP Serverless Top 10 - SAS-10

MEDIUM RISK

📋 What Is It?

Improper Exception Handling & Verbose Error Messages occurs when a function lets an exception decide its own fate: the error bubbles up unhandled, the platform serialises whatever it can, and the caller receives a response full of internal detail-or the function fails in a way that quietly skips a security check, half-completes a write, or vanishes without an audit record. Serverless magnifies this: the platform returns unhandled errors to the caller by default (API Gateway, Function URL, invoke response) and retries failures automatically (at-least-once). Both defaults are hostile in production. The fix: a generic message to the caller, rich structured detail to the logs, fail-closed security paths, and idempotent, safe-to-retry side effects.

SAS-10 OWASP Rank
Default Is Hostile
2 Audiences Caller vs. Logs

⚠️ Common Exploits

  • Raw Stack Trace to Caller: File paths, line numbers, runtime internals disclosed on a bad request
  • Secrets in the Error Dump: Handler echoes env/config-DSNs, ARNs, account IDs, passwords
  • Fail-Open Security Check: A caught auth error yields empty claims and continues
  • Duplicate Side Effect: Error after a charge, before ack → retry charges again
  • Error-Type Oracle: 500 vs 404 (or timing) reveals which records exist
  • Driver Error Leak: Query text exposes table & column names
  • Destroyed Audit Trail: Crash before the log line leaves no record

🔴 Attack Flow

1. Trigger the error
(malformed / oversized / unexpected input)
↓
2. Harvest internals from the response
(stack trace, secrets, ARNs, table names)
↓
3. Build oracles from response differences
(status/message/timing → enumerate records)
↓
4. Exploit fail-open & force duplicates
(make a check throw; fail the tail to retry)
↓
5. IMPACT: Recon, secret leak, authz bypass, double-charge!

❌ Vulnerable Code

// No try/catch. Any throw is serialised straight back to the caller with the // full stack trace through the API Gateway proxy integration. exports.handler = async (event) => { const body = JSON.parse(event.body); // throws on bad JSON const order = await db.getOrder(body.orderId); // throws on DB error return { statusCode: 200, body: JSON.stringify(order) }; }; // Client receives: errorType, errorMessage, and "trace" with /var/task/... paths # Python - dumps the whole event AND environment "to help debugging" except Exception as e: return { "statusCode": 500, "body": json.dumps({ "error": str(e), "event": event, ← tokens, PII in the body "env": dict(os.environ) }) } ← DB_PASSWORD, ARNs, account id # Fail-OPEN: a verification error yields empty claims, then falls through try: claims = verify_token(hdr) except Exception: claims = {} ← error -> empty claims, keep going if claims.get("role") == "admin" or not claims: ← BUG: `not claims` grants access return do_admin_action(event)

✅ Secure Code

// Reusable boundary: caller gets a generic message + correlation id; // full detail goes only to CloudWatch. Apply to every handler. const withErrorBoundary = (fn) => async (event, context) => { const correlationId = context.awsRequestId; try { return await fn(event, context); } catch (err) { console.error(JSON.stringify({ // server-side only level: 'ERROR', correlationId, name: err.name, message: err.message, stack: err.stack })); return { statusCode: 500, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'internal_error', message: 'The request could not be processed.', correlationId }) }; // safe to show, leaks nothing } }; # Fail-CLOSED: any exception in verification is a hard 401 deny (Python) try: claims = verify_token(hdr) except Exception as e: log.warning({"event": "auth_error", "reason": type(e).__name__}) return client_error(401, "unauthorized", "Authentication required.", ctx) if claims.get("role") != "admin": # explicit allow only on success return client_error(403, "forbidden", "Access denied.", ctx) # Idempotency key + compensation make at-least-once retries safe if (await alreadyProcessed(record.messageId)) continue; // dedupe on retry await chargeCard(id, amt, { idempotencyKey: record.messageId }); // no-op if repeated # API Gateway maps 4xx/5xx to generic bodies so crashes never leak verbatim x-amazon-apigateway-gateway-responses: DEFAULT_5XX: { responseTemplates: { application/json: '{"error":"internal_error"}' } }

✓ Prevention Checklist

  • Wrap every handler in an outermost error boundary
  • Generic client message + correlation id - nothing internal
  • Full structured detail to server-side logs only
  • Map errors at API Gateway & Function URL layers
  • Never serialise env/config into an error
  • Redact sensitive fields before logging
  • Fail closed on auth/authz/validation/signature paths
  • Every failure branch ends in a return/deny
  • Idempotency keys + compensation on side effects
  • Uniform responses & constant-time checks (no oracles)
  • Log-before-return so the audit trail survives a crash
  • Disable verbose/debug output in production

🔍 Detection & Tools

Controls & Services:

API Gateway Gateway Responses Lambda Powertools CloudWatch Logs Structured Logging AWS Secrets Manager

Testing & Scanning:

Burp Suite OWASP ZAP Fuzzing (malformed input) SAST / code review

How to Test:

  • Send malformed JSON / wrong-type fields - read the response
  • Check responses for stack traces, ARNs, query text, account IDs
  • Force a DB/timeout error and inspect what leaks
  • Send a malformed token - does the check fail open?
  • Compare 500 vs 404 and response timing for enumeration
  • Verify audit logs survive a crash mid-request

🌍 Real-World

Described as classes of failure repeatedly documented across the industry, mapping to long-standing CWE entries (improper error handling, sensitive info in an error message):

  • Stack Traces as Free Reconnaissance: Framework debug behaviour left on ships full traces-file paths, library versions, config fragments-to the client; scanners trigger errors specifically to read them and match disclosed versions to known CVEs.
  • Secrets Surfacing Through Error/Debug Output: Handlers log or return the full event or environment "to help debugging"; credentials and connection strings land in responses or over-readable log stores, then get reused directly (overlaps SAS-7).
  • Retries Turning One Action Into Many: A non-idempotent side effect hits an unhandled error before ack on an at-least-once source; the platform re-runs it-duplicate charges, notifications, or records (overlaps SAS-9).

📌 Quick Tips

  • DO NOT return raw exceptions or stack traces to callers
  • DO NOT serialise the event or environment on error
  • DO NOT let a caught security error fall through to "allow"
  • DO return a generic message + correlation id
  • DO fail closed and log before you return
  • DO make retried side effects idempotent
  • DO keep responses uniform to avoid oracles

📜 Compliance

Related Standards:

  • CWE-209 / CWE-388 Info in Error Msg / Error Handling
  • OWASP Top 10 - Security Misconfiguration
  • NIST 800-53 SI-11 - Error Handling
  • PCI-DSS Requirement 6.5 - Secure Coding
  • SOC 2 CC7.2 - Monitoring & Response