647928a0a1
Fundamentals: secure-defaults, input-validation, credential-handling, audit-logging Identity: authentication, authorization Attack Prevention: injection-prevention, dos-prevention, prompt-injection
181 lines
4.1 KiB
Markdown
181 lines
4.1 KiB
Markdown
# 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)
|