fix(rate-limit): non fidarsi del primo hop di X-Forwarded-For (#7)
_client_ip usava il primo elemento di XFF, impostabile dal client. Ora usa l'elemento a -trusted_proxy_count (nuova impostazione, default 1, env TRUSTED_PROXY_COUNT), quello aggiunto dal proxy fidato più esterno. Con trusted_proxy_count=0 XFF e X-Real-IP sono ignorati. Aggiunta anche web_concurrency (env WEB_CONCURRENCY, default 1): i bucket del rate limit sono per processo, con --workers N il limite configurato viene diviso per N (arrotondato per eccesso, minimo 1) in _effective_limit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,29 @@ class TestRateLimiting:
|
||||
retry_val = int(resp.headers["retry-after"])
|
||||
assert retry_val >= 1
|
||||
|
||||
async def test_login_rate_limit_survives_rotated_x_forwarded_for(
|
||||
self, client: AsyncClient
|
||||
):
|
||||
"""A client rotating the spoofable (first) hop of XFF cannot dodge the
|
||||
login rate limit: with one trusted proxy (the default), the real
|
||||
client IP is the last hop, which stays constant."""
|
||||
for i in range(5):
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
json={"username": f"rotated{i}", "password": "pass"},
|
||||
headers={
|
||||
"X-Forwarded-For": f"{i}.{i}.{i}.{i}, 198.51.100.42",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
resp = await client.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "rotated-overflow", "password": "pass"},
|
||||
headers={"X-Forwarded-For": "9.9.9.9, 198.51.100.42"},
|
||||
)
|
||||
assert resp.status_code == 429
|
||||
|
||||
|
||||
class TestPathTraversal:
|
||||
"""Path traversal protection in file endpoints."""
|
||||
|
||||
Reference in New Issue
Block a user