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)
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# 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](https://owasp.org/Top10/2025/A10_2025-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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
# 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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
# 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
|
||||
Reference in New Issue
Block a user