forked from claw/flaskpaste
security: implement pentest remediation (PROXY-001, BURN-001, RATE-001)
PROXY-001: Add startup warning when TRUSTED_PROXY_SECRET empty in production - validate_security_config() checks for missing proxy secret - Additional warning when PKI enabled without proxy secret - Tests for security configuration validation BURN-001: HEAD requests now trigger burn-after-read deletion - Prevents attacker from probing paste existence before retrieval - Updated test to verify new behavior RATE-001: Add RATE_LIMIT_MAX_ENTRIES to cap memory usage - Default 10000 unique IPs tracked - Prunes oldest entries when limit exceeded - Protects against memory exhaustion DoS Test count: 284 -> 291 (7 new security tests)
This commit is contained in:
@@ -193,3 +193,77 @@ class TestRateLimitCleanup:
|
||||
cleaned = cleanup_rate_limits(window=60)
|
||||
assert isinstance(cleaned, int)
|
||||
assert cleaned >= 0
|
||||
|
||||
|
||||
class TestRateLimitMaxEntries:
|
||||
"""Tests for RATE-001: rate limit storage cap to prevent memory DoS."""
|
||||
|
||||
def test_max_entries_config_exists(self, app):
|
||||
"""RATE_LIMIT_MAX_ENTRIES config should exist."""
|
||||
assert "RATE_LIMIT_MAX_ENTRIES" in app.config
|
||||
assert isinstance(app.config["RATE_LIMIT_MAX_ENTRIES"], int)
|
||||
assert app.config["RATE_LIMIT_MAX_ENTRIES"] > 0
|
||||
|
||||
def test_rate_limit_storage_bounded(self, app):
|
||||
"""Rate limit storage should not exceed max entries."""
|
||||
from app.api.routes import (
|
||||
_rate_limit_requests,
|
||||
check_rate_limit,
|
||||
reset_rate_limits,
|
||||
)
|
||||
|
||||
# Reset state
|
||||
reset_rate_limits()
|
||||
|
||||
# Set very low max entries for testing
|
||||
original_max = app.config.get("RATE_LIMIT_MAX_ENTRIES", 10000)
|
||||
app.config["RATE_LIMIT_MAX_ENTRIES"] = 10
|
||||
|
||||
try:
|
||||
with app.app_context():
|
||||
# Add more entries than max
|
||||
for i in range(20):
|
||||
check_rate_limit(f"192.168.1.{i}", authenticated=False)
|
||||
|
||||
# Storage should be bounded
|
||||
assert len(_rate_limit_requests) <= 10
|
||||
finally:
|
||||
app.config["RATE_LIMIT_MAX_ENTRIES"] = original_max
|
||||
reset_rate_limits()
|
||||
|
||||
def test_prune_keeps_recent_entries(self, app):
|
||||
"""Pruning should keep the most recent entries."""
|
||||
import time
|
||||
|
||||
from app.api.routes import (
|
||||
_rate_limit_requests,
|
||||
check_rate_limit,
|
||||
reset_rate_limits,
|
||||
)
|
||||
|
||||
reset_rate_limits()
|
||||
|
||||
original_max = app.config.get("RATE_LIMIT_MAX_ENTRIES", 10000)
|
||||
app.config["RATE_LIMIT_MAX_ENTRIES"] = 5
|
||||
|
||||
try:
|
||||
with app.app_context():
|
||||
# Add entries with known order
|
||||
for i in range(3):
|
||||
check_rate_limit(f"old-{i}.0.0.1", authenticated=False)
|
||||
time.sleep(0.01) # Ensure distinct timestamps
|
||||
|
||||
# Add more recent entries
|
||||
for i in range(3):
|
||||
check_rate_limit(f"new-{i}.0.0.1", authenticated=False)
|
||||
time.sleep(0.01)
|
||||
|
||||
# When we exceed max, old entries should be pruned
|
||||
# Storage should be bounded
|
||||
assert len(_rate_limit_requests) <= 5
|
||||
|
||||
# Most recent should still be present
|
||||
assert any("new" in ip for ip in _rate_limit_requests)
|
||||
finally:
|
||||
app.config["RATE_LIMIT_MAX_ENTRIES"] = original_max
|
||||
reset_rate_limits()
|
||||
|
||||
Reference in New Issue
Block a user