Files
Rodin 8a94a08511 Add supply-chain, deserialization, cryptography, error-handling patterns
Now covers all OWASP Top 10:2025 categories:
- A03: supply-chain.md (SolarWinds, Bybit, npm worm examples)
- A04: cryptography.md (algorithm recommendations, key management)
- A08: deserialization.md (pickle, yaml, language-specific risks)
- A10: error-handling.md (fail closed, error messages)
2026-05-10 22:48:39 -07:00

5.1 KiB

Error Handling

Rule

Handle all errors explicitly. Fail closed. Never leak sensitive information in error messages.

Source: OWASP Top 10 2025 - A10 Mishandling of Exceptional Conditions

Fail Closed vs Fail Open

Scenario Fail Closed (Correct) Fail Open (Wrong)
Auth check errors Deny access Allow access
Input validation errors Reject request Process anyway
Transaction errors Roll back Partial commit
Permission check timeout Deny Allow

Correct Pattern

import logging
from contextlib import contextmanager

# Explicit error handling with fail-closed
def check_permission(user_id: str, resource_id: str) -> bool:
    """Return False on any error — fail closed."""
    try:
        permissions = fetch_permissions(user_id)
        return resource_id in permissions.allowed_resources
    except Exception as e:
        logging.exception("Permission check failed", extra={
            "user_id": user_id,
            "resource_id": resource_id
        })
        return False  # Deny on error

# Transaction rollback on failure
@contextmanager
def transaction():
    """Ensure complete rollback on any failure."""
    tx = begin_transaction()
    try:
        yield tx
        tx.commit()
    except Exception:
        tx.rollback()
        raise

def transfer_funds(from_acct: str, to_acct: str, amount: Decimal):
    with transaction() as tx:
        debit(tx, from_acct, amount)
        credit(tx, to_acct, amount)
        # If credit fails, debit is rolled back

# Generic error messages to users
def handle_request(request):
    try:
        return process(request)
    except ValidationError as e:
        # Specific, safe error for user
        return {"error": str(e)}, 400
    except Exception as e:
        # Log details internally
        logging.exception("Unexpected error", extra={
            "request_id": request.id
        })
        # Generic message to user
        return {"error": "An unexpected error occurred"}, 500

Incorrect Pattern

# Wrong: fail open
def check_access(user_id, resource):
    try:
        return has_permission(user_id, resource)
    except:
        return True  # "If in doubt, let them in"

# Wrong: swallowing exceptions
try:
    process_payment()
except:
    pass  # Silently fails, state unknown

# Wrong: leaking sensitive info
except DatabaseError as e:
    return {"error": f"Database error: {e}"}  # Exposes internals

# Wrong: stack trace to user
except Exception as e:
    import traceback
    return {"error": traceback.format_exc()}

# Wrong: partial transaction
def transfer(from_acct, to_acct, amount):
    debit(from_acct, amount)
    try:
        credit(to_acct, amount)
    except:
        pass  # Debit happened but credit didn't!

Error Message Guidelines

Internal Log User-Facing Message
SQLException: column 'password' at line 5 An error occurred. Please try again.
FileNotFoundError: /etc/shadow Resource not found.
ConnectionError: redis://prod-cache:6379 Service temporarily unavailable.
KeyError: user['admin_token'] Invalid request.

Global Exception Handler

from flask import Flask, jsonify
import logging

app = Flask(__name__)

@app.errorhandler(Exception)
def handle_exception(e):
    """Global handler — catch anything we missed."""
    # Log full details
    logging.exception("Unhandled exception")
    
    # Return generic error to user
    if app.debug:
        # Only in dev — never in prod
        return {"error": str(e)}, 500
    else:
        return {"error": "Internal server error"}, 500

# Rate limit repeated errors (DOS prevention)
class ErrorRateLimiter:
    def __init__(self, max_errors: int = 100, window: int = 60):
        self.max_errors = max_errors
        self.window = window
        self.errors = []
    
    def record_error(self, error_type: str):
        now = time.time()
        self.errors = [t for t in self.errors if now - t < self.window]
        self.errors.append(now)
        
        if len(self.errors) > self.max_errors:
            logging.warning(f"Error rate limit exceeded: {error_type}")
            # Could trigger alerting or blocking

Unchecked Return Values

# Wrong: ignoring return values
def process_file(path):
    f = open(path)  # Could fail
    data = f.read()
    f.close()
    return data

# Correct: handle all failure modes
def process_file(path: str) -> str:
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        raise ValueError(f"File not found: {path}")
    except PermissionError:
        raise ValueError(f"Permission denied: {path}")
    except IOError as e:
        raise ValueError(f"IO error reading file: {e}")

Edge Cases

  • Errors during error handling (recursive failure)
  • Resource leaks when exceptions occur
  • Timeout handling (treat as failure)
  • Async error handling (unhandled promise rejections)
  • Background job failures (need monitoring)
  • Partial failures in distributed systems