Skip to content

Harden client IP resolution in RateLimitMiddleware - #19567

Merged
markstory merged 1 commit into
fix-ratelimit-headerfrom
ratelimit-client-ip-hardening
Jul 27, 2026
Merged

markstory merged 1 commit into
fix-ratelimit-headerfrom
ratelimit-client-ip-hardening

Conversation

@dereuromark

Copy link
Copy Markdown
Member

Follow-up to #19565, targeting that branch.

The default change from x-forwarded-for to remote_addr is the right fix. This adds three hardening/correctness follow-ups on top of it.

1. Restore left-most IP extraction (behavior regression)

#19565 replaced the explode(',', ...) + trim($ips[0]) extraction with the raw getHeaderLine() value. For anyone opting back into ipHeader => 'x-forwarded-for', the rate-limit identifier became the entire forwarded chain (192.168.1.101, 10.0.0.1) instead of the client IP.

That is a bypass: an attacker who controls XFF can vary the downstream hops (client, junk1, client, junk2, ...) and get a fresh key each request. This restores taking the left-most (originating client) entry.

2. Explicit remote_addr handling

The new default only worked because getHeaderLine('remote_addr') returns '' and falls through to $params['REMOTE_ADDR']. That is implicit - any nonexistent header name behaves the same. Handle the remote_addr sentinel explicitly so intent is clear.

3. Docs + tests

  • Docblock now warns that forwarded headers are client-supplied and spoofable unless a trusted proxy overwrites them.
  • testProxyHeadersWithTrustProxy now asserts the resolved identifier: it primes on client 192.168.1.101 via the chain, then re-requests with the same client but a different proxy hop and asserts the limit is hit. The previous version only asserted 200 twice and would have passed even with the chain-keying bug.
  • Status assertions switched to assertSame(200, ...) (expected-first, type-strict).

Gates: RateLimitMiddlewareTest 10/10 green, phpstan clean, phpcs clean.

Follow-up to the ipHeader default change:

- Extract the left-most entry from a forwarded chain again. Using the
  raw header line keyed the limit on the whole "client, proxy1, ..."
  string, so an attacker could vary downstream hops to bypass the limit.
- Handle the 'remote_addr' default explicitly instead of relying on it
  not matching any header name and falling through.
- Document that forwarded headers are spoofable and only safe when a
  trusted proxy overwrites them.
- Assert the resolved identifier in the trust-proxy test and switch the
  status assertions to assertSame(200, ...).
Comment thread src/Http/Middleware/RateLimitMiddleware.php
@markstory
markstory merged commit 3f9162d into fix-ratelimit-header Jul 27, 2026
14 of 15 checks passed
@markstory
markstory deleted the ratelimit-client-ip-hardening branch July 27, 2026 17:27
@markstory

Copy link
Copy Markdown
Member

Thanks for catching the mistake I had with remote_addr ❤️

markstory added a commit that referenced this pull request Jul 28, 2026
The previous defaults were not safe, and I don't think we should have
released this like this. I've also cleaned up some of the implementation
as ServerRequestInterface has methods for this.

Thank you to `@yousukezan` for reporting this issue through our security
mailing list.


* Harden client IP resolution in RateLimitMiddleware (#19567)

- Extract the left-most entry from a forwarded chain again. Using the
  raw header line keyed the limit on the whole "client, proxy1, ..."
  string, so an attacker could vary downstream hops to bypass the limit.
- Handle the 'remote_addr' default explicitly instead of relying on it
  not matching any header name and falling through.
- Document that forwarded headers are spoofable and only safe when a
  trusted proxy overwrites them.
- Assert the resolved identifier in the trust-proxy test and switch the
  status assertions to assertSame(200, ...).

---------

Co-authored-by: Kevin Pfeifer <[email protected]>
Co-authored-by: othercorey <[email protected]>
Co-authored-by: Mark Scherer <[email protected]>
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.

2 participants