[security] fix(dashboard-auth): ignore spoofed XFF for login limits - #40285
[security] fix(dashboard-auth): ignore spoofed XFF for login limits#40285Hinotoi-agent wants to merge 2 commits into
Conversation
|
Verified — this is a real rate-limit bypass and the fix is correct. Vulnerability: Fix correctness: Removing Edge cases verified:
No issues found. Clean security fix with proper regression coverage. |
alpindiay
left a comment
There was a problem hiding this comment.
LGTM. Important security fix — _client_ip() in the dashboard auth routes now uses request.client.host (the socket peer) instead of the X-Forwarded-For header for rate-limiting IP identification.
Why this matters: X-Forwarded-For is trivially spoofable when clients connect directly to the dashboard. An attacker could rotate the header value to get fresh rate-limit buckets per request, completely bypassing the password-login brute-force protection. The fix correctly notes that uvicorn's trusted-proxy handling can rewrite request.client for reverse-proxy deployments — that's the proper way to handle forwarded IPs.
The new test test_spoofed_x_forwarded_for_does_not_reset_rate_limit confirms that rotating X-Forwarded-For across 11 attempts does not reset the rate limit counter (all attempts count toward the same bucket, 11th returns 429). Well done.
|
Thanks for the focused hardening patch. The current-main path is vulnerable: Problems
Suggested changes
Automated hermes-sweeper review. |
|
Updated the stale limiter comment per the latest review. It now explains that trusted server/proxy handling must normalize the address exposed through Validation:
Commit: |
|
looks mergeable Security evidence:
Not checked:
Signed: GPT-5.6-sol-xhigh in Codex |
Summary
This PR hardens the dashboard password-login rate-limit boundary so direct clients cannot choose their own rate-limit bucket with a spoofed
X-Forwarded-Forheader.X-Forwarded-Forvalues do not reset the password-login limiter.Security issues covered
X-Forwarded-Forbypass for dashboard password-login rate limitingPOST /auth/password-logincould rotateX-Forwarded-Forvalues to avoid the per-client online-guessing throttle.Before this PR
_client_ip(request)preferred the first value in the client-suppliedX-Forwarded-Forheader./auth/password-loginused that helper as the rate-limit key for failed password-login attempts.X-Forwarded-Forvalue after exhausting one bucket and continue attempts in a fresh bucket.After this PR
_client_ip(request)returns only the direct peer address fromrequest.client.host.X-Forwarded-For.X-Forwarded-Forstill returns429once the direct client is limited.Why this matters
Password-login throttling is an online guessing control. If the limiter key is derived from an attacker-controlled header, the control can be bypassed without changing the real network peer. That is especially risky for dashboard deployments where password authentication is used to protect a non-local bind or a reverse-proxy-exposed dashboard.
The patch keeps the trust boundary simple: untrusted HTTP headers do not decide the application's login-throttle identity.
How this differs from related issue/PR
dashboard_auth/routes.pyimplementation and adds focused regression coverage for the spoofed-forwarded-header limiter bypass.Attack flow
Affected code
hermes_cli/dashboard_auth/routes.py,tests/hermes_cli/test_dashboard_auth_password_login.pyRoot cause
Issue: spoofable
X-Forwarded-Forbypass for password-login rate limiting_client_ip(request)treatedX-Forwarded-Foras trustworthy for all callers./auth/password-loginused_client_ip(request)for rate-limit and audit decisions.X-Forwarded-Foris only trustworthy when it has been normalized by a trusted proxy/server boundary; direct dashboard clients can set it arbitrarily.CVSS assessment
X-Forwarded-Forbypass for dashboard password-login rate limitingCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:NRationale:
Safe reproduction steps
_client_ip(request)trustsX-Forwarded-For.POST /auth/password-loginlocally with a fake provider and fake password so no real credentials or external providers are touched.X-Forwarded-Forand observe the 11th response is429.X-Forwarded-For, and observe the 11th response is429.X-Forwarded-Forand observe vulnerable code admits it instead of returning429.X-Forwarded-Forvalues and observe vulnerable code keeps admitting attempts in new buckets.X-Forwarded-Forvalues remain blocked with429after the direct client is limited.Expected vulnerable behavior
Local vulnerable-code proof output showed:
In this local harness,
404means the fake provider lookup was reached and the request was not rate-limited;429means the login limiter blocked before provider lookup.After this PR, the same proof showed:
Changes in this PR
X-Forwarded-Forparsing from_client_ip(request).X-Forwarded-Forvalues and verifies the rate limit is not reset.Files changed
hermes_cli/dashboard_auth/routes.py_client_ip()now ignores untrustedX-Forwarded-Forand uses the direct peer address.tests/hermes_cli/test_dashboard_auth_password_login.pyX-Forwarded-Forrotation cannot bypass the password-login limiter.Maintainer impact
request.client.Fix rationale
The durable boundary is to avoid trusting forwarded headers in application code for security decisions unless they have already been validated by a trusted proxy boundary. Keying the login limiter from
request.client.hostpreserves a safe default for direct dashboard exposure and avoids duplicating proxy-trust policy inside the route handler.The regression test is focused on the exact bypass: one direct client rotates
X-Forwarded-Forvalues after exhausting the limiter and must still receive429.Type of change
Test plan
uv run --extra dev python -m pytest tests/hermes_cli/test_dashboard_auth_password_login.py -q20 passed, 1 warninguv run --extra dev ruff check hermes_cli/dashboard_auth/routes.py tests/hermes_cli/test_dashboard_auth_password_login.pyAll checks passed!python3 -m py_compile hermes_cli/dashboard_auth/routes.pygit diff --checkorigin/mainand patched code, using only fake provider/password values.Disclosure notes