Skip to content

fix(rate-limit): stop trusting X-Forwarded-For / X-Real-IP by default - #229

Merged
ZhuchkaTriplesix merged 2 commits into
devfrom
issue-207-rate-limit-trust-proxy
Sep 29, 2026
Merged

ZhuchkaTriplesix merged 2 commits into
devfrom
issue-207-rate-limit-trust-proxy

Conversation

@ZhuchkaTriplesix

Copy link
Copy Markdown
Member

Problem

The default rate_limit_key="ip" strategy honored X-Forwarded-For / X-Real-IP unconditionally, with no trusted-proxy check. Any client could:

  • bypass its own rate limit by sending a fresh value on every request, or
  • frame another client by spoofing that client's IP in the header, getting it rate-limited.

Fix

  • Default "ip" now keys on the actual TCP peer address (scope.client) only — the client cannot control this.
  • New opt-in strategy "trusted-forwarded-ip" (alias "forwarded-ip") restores the X-Forwarded-For/X-Real-IP behavior, documented as safe only behind a reverse proxy that actually overwrites those headers.
  • Bonus fix found while touching this: scope.client parsing assumed an ASGI-style (host, port) tuple via get_item(0), but Granian's real RSGI scope exposes it as a "host:port" string — so the old "safe" fallback was silently returning just the first character of that string. Added scope_client_host() to handle both forms correctly.

Testing

  • Updated tests/test_rate_limit.py: the existing test asserted the vulnerable behavior (spoofed X-Forwarded-For not blocked) — flipped to assert it's now blocked, since it's the same underlying peer.
  • Added test_trusted_forwarded_ip_opt_in covering the new explicit opt-in strategy.
  • cargo test --lib rate_limit: 2 passed.
  • Full suite (pytest tests/): 161 passed, 1 skipped.
  • Updated docs/rate-limiting.md.

Closes #207

The default "ip" rate-limit key strategy honored X-Forwarded-For and
X-Real-IP unconditionally, with no trusted-proxy check. Any client
could bypass its rate limit by sending a fresh value on each request,
or frame another client by spoofing its IP in the header.

The default "ip" strategy now keys on the actual TCP peer address
(scope.client) only, which the client cannot control. A new opt-in
strategy, "trusted-forwarded-ip" (aliased "forwarded-ip"), restores
the X-Forwarded-For / X-Real-IP behavior for deployments that are
actually behind a reverse proxy overwriting those headers.

Also fixes scope.client parsing: Granian's RSGI scope exposes it as a
"host:port" string, but the old code indexed it with get_item(0) as
if it were an ASGI-style (host, port) tuple, silently returning only
the first character of the string. scope_client_host() now handles
both the RSGI string form and the ASGI/test-scope tuple form.

Closes #207
@ZhuchkaTriplesix
ZhuchkaTriplesix merged commit a2dd88a into dev Sep 29, 2026
17 checks passed

@ZhuchkaTriplesix ZhuchkaTriplesix left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

123

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant