Initial commit: 9 security patterns for code review
Fundamentals: secure-defaults, input-validation, credential-handling, audit-logging Identity: authentication, authorization Attack Prevention: injection-prevention, dos-prevention, prompt-injection
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
# Denial of Service Prevention
|
||||
|
||||
## Rule
|
||||
|
||||
Bound all resource consumption. Assume attackers will send worst-case input.
|
||||
|
||||
**Source:** [CWE-400: Uncontrolled Resource Consumption](https://cwe.mitre.org/data/definitions/400.html)
|
||||
|
||||
## Request Limits
|
||||
|
||||
### Correct Pattern
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
import time
|
||||
|
||||
# Rate limiting
|
||||
class RateLimiter:
|
||||
def __init__(self, max_requests: int, window_seconds: int):
|
||||
self.max_requests = max_requests
|
||||
self.window = window_seconds
|
||||
self.requests = {} # ip -> [timestamps]
|
||||
|
||||
def is_allowed(self, ip: str) -> bool:
|
||||
now = time.time()
|
||||
cutoff = now - self.window
|
||||
|
||||
# Clean old entries
|
||||
self.requests[ip] = [
|
||||
t for t in self.requests.get(ip, [])
|
||||
if t > cutoff
|
||||
]
|
||||
|
||||
if len(self.requests[ip]) >= self.max_requests:
|
||||
return False
|
||||
|
||||
self.requests[ip].append(now)
|
||||
return True
|
||||
|
||||
# Request size limits
|
||||
MAX_BODY_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
|
||||
@app.before_request
|
||||
def limit_request_size():
|
||||
if request.content_length and request.content_length > MAX_BODY_SIZE:
|
||||
abort(413) # Payload too large
|
||||
```
|
||||
|
||||
### Incorrect Pattern
|
||||
|
||||
```python
|
||||
# Wrong: no size limit
|
||||
data = request.get_data() # Could be gigabytes
|
||||
|
||||
# Wrong: unbounded loop based on user input
|
||||
for i in range(int(request.args["count"])):
|
||||
process_item(i)
|
||||
|
||||
# Wrong: no timeout
|
||||
response = requests.get(user_url) # Hangs forever
|
||||
```
|
||||
|
||||
## Algorithmic Complexity
|
||||
|
||||
### Correct Pattern
|
||||
|
||||
```python
|
||||
# Limit input size before expensive operations
|
||||
MAX_ITEMS = 10000
|
||||
|
||||
def process_list(items: list) -> list:
|
||||
if len(items) > MAX_ITEMS:
|
||||
raise ValueError(f"Too many items: {len(items)} > {MAX_ITEMS}")
|
||||
return sorted(items) # O(n log n) but bounded
|
||||
|
||||
# Use timeouts for expensive operations
|
||||
import signal
|
||||
|
||||
def timeout_handler(signum, frame):
|
||||
raise TimeoutError("Operation timed out")
|
||||
|
||||
def with_timeout(seconds: int):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
signal.signal(signal.SIGALRM, timeout_handler)
|
||||
signal.alarm(seconds)
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
@with_timeout(5)
|
||||
def expensive_operation(data):
|
||||
...
|
||||
```
|
||||
|
||||
### Incorrect Pattern
|
||||
|
||||
```python
|
||||
# Wrong: O(n²) or worse on unbounded input
|
||||
def find_duplicates(items):
|
||||
for i in items:
|
||||
for j in items: # O(n²)
|
||||
if i == j:
|
||||
yield i
|
||||
|
||||
# Wrong: regex with catastrophic backtracking
|
||||
import re
|
||||
pattern = re.compile(r'(a+)+$') # ReDoS vulnerable
|
||||
pattern.match('a' * 30 + 'b') # Hangs
|
||||
```
|
||||
|
||||
## Memory Limits
|
||||
|
||||
### Correct Pattern
|
||||
|
||||
```python
|
||||
# Stream large files instead of loading into memory
|
||||
def process_large_file(path: str):
|
||||
with open(path, 'r') as f:
|
||||
for line in f: # Streaming, constant memory
|
||||
process_line(line)
|
||||
|
||||
# Limit collection sizes
|
||||
class BoundedCache:
|
||||
def __init__(self, max_size: int = 1000):
|
||||
self.max_size = max_size
|
||||
self.cache = {}
|
||||
|
||||
def set(self, key, value):
|
||||
if len(self.cache) >= self.max_size:
|
||||
# Evict oldest
|
||||
oldest = next(iter(self.cache))
|
||||
del self.cache[oldest]
|
||||
self.cache[key] = value
|
||||
```
|
||||
|
||||
### Incorrect Pattern
|
||||
|
||||
```python
|
||||
# Wrong: loading entire file into memory
|
||||
data = open(path).read() # Could be huge
|
||||
|
||||
# Wrong: unbounded cache
|
||||
cache = {}
|
||||
def get_or_compute(key):
|
||||
if key not in cache:
|
||||
cache[key] = expensive_compute(key) # Grows forever
|
||||
return cache[key]
|
||||
```
|
||||
|
||||
## Connection Limits
|
||||
|
||||
```python
|
||||
# Limit concurrent connections per IP
|
||||
MAX_CONNECTIONS_PER_IP = 10
|
||||
|
||||
# Timeouts on all network operations
|
||||
import socket
|
||||
socket.setdefaulttimeout(30)
|
||||
|
||||
# Connection pooling with limits
|
||||
from urllib3 import PoolManager
|
||||
http = PoolManager(
|
||||
maxsize=100,
|
||||
block=True,
|
||||
timeout=30
|
||||
)
|
||||
```
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- Zip bombs (small file, huge uncompressed)
|
||||
- XML entity expansion (billion laughs attack)
|
||||
- Hash collision attacks (hash flooding)
|
||||
- Slowloris (slow, incomplete requests)
|
||||
- Amplification attacks (small request, large response)
|
||||
Reference in New Issue
Block a user