Skip to main content

Trusted Proxy IP Resolution

TrustedProxyClientIpResolver (v0.6.0) resolves the real client IP behind Cloudflare, an AWS load balancer, or any other proxy chain. It only honours forwarded headers when the immediate connection comes from a trusted CIDR, so unauthenticated callers cannot spoof their IP by setting X-Forwarded-For.

Http\ClientIpResolver (since 0.3.0) keeps working — its constructor and resolve(?array $server = null) API are unchanged. Internally it now delegates to TrustedProxyClientIpResolver, which means the same hardening rules apply to existing 0.3.x callers.

For new code, prefer TrustedProxyClientIpResolver directly — it implements ClientIpResolverInterface and slots into RateLimitMiddleware and IpAllowlistMiddleware cleanly.

Minimal example

use BetterRoute\Middleware\Network\TrustedProxyClientIpResolver;

$resolver = new TrustedProxyClientIpResolver(
trustedProxyCidrs: [
'173.245.48.0/20', // Cloudflare
'103.21.244.0/22',
'2400:cb00::/32',
],
forwardedHeaders: ['CF-Connecting-IP', 'X-Forwarded-For']
);

$clientIp = $resolver->resolve();

Pass it to RateLimitMiddleware for proxy-aware rate-limit keys:

use BetterRoute\Middleware\RateLimit\RateLimitMiddleware;
use BetterRoute\Middleware\RateLimit\TransientRateLimiter;

$rateLimit = new RateLimitMiddleware(
limiter: new TransientRateLimiter(),
limit: 60,
windowSeconds: 60,
clientIpResolver: $resolver
);

RateLimitMiddleware's $clientIpResolver argument now accepts either the legacy Http\ClientIpResolver or any Middleware\Network\ClientIpResolverInterface.

Constructor

new TrustedProxyClientIpResolver(
array $trustedProxyCidrs = [],
array $forwardedHeaders = ['CF-Connecting-IP', 'X-Forwarded-For'],
?callable $serverResolver = null
);
  • trustedProxyCidrs — list of IPv4/IPv6 CIDRs or single IPs. Each entry is validated at construction.
  • forwardedHeaders — header order the resolver consults when REMOTE_ADDR is trusted. The first header that yields a valid IP wins.
  • serverResolver — defaults to a callable returning $_SERVER. Inject in tests.

Behavior

REMOTE_ADDRTrusted?Forwarded headers present?Result
missing/invalidnull
outside trustedProxyCidrsnoREMOTE_ADDR returned (forwarded headers ignored)
inside trustedProxyCidrsyesfirst valid IP foundfirst valid IP from the first matching header
inside trustedProxyCidrsyesnone parseableREMOTE_ADDR returned

X-Forwarded-For is comma-delimited; the first valid IP wins (left-most, which is the original client per spec).

The resolver checks the request object first ($request->get_header($header)) when one is passed in, then falls back to $_SERVER. Both Header-Name and HTTP_HEADER_NAME shapes are accepted.

CIDR matching

BetterRoute\Middleware\Network\CidrMatcher handles the IPv4/IPv6 math. CIDRs without a /prefix are treated as single hosts (/32 for IPv4, /128 for IPv6). The matcher rejects malformed input at construction:

  • 192.0.2.0/24
  • 2400:cb00::/32
  • 203.0.113.5 (single host) ✓
  • 300.0.0.0/24 ✗ — not a valid IP
  • 192.0.2.0/40 ✗ — prefix out of range

Use CidrMatcher::matches($ip, $cidr) and CidrMatcher::assertValid($cidr) if you need the same logic outside the resolver.

Validation checklist

  • a request with REMOTE_ADDR from outside the trusted CIDR returns REMOTE_ADDR (proxy headers ignored);
  • a request from a trusted proxy with CF-Connecting-IP: 203.0.113.5 returns 203.0.113.5;
  • a request from a trusted proxy with X-Forwarded-For: 203.0.113.5, 10.0.0.1 returns 203.0.113.5;
  • a malformed CIDR in the constructor throws InvalidArgumentException.

Common mistakes

  • Passing the proxy's actual IP into forwardedHeaders thinking it is a CIDR — it must be a header name (e.g. CF-Connecting-IP).
  • Trusting every CIDR ("just to be safe"). Each entry is a delegation of trust — only list the proxy ranges you control or rely on.
  • Letting Http\ClientIpResolver be constructed without trustedProxies. With an empty list, every header is ignored and only REMOTE_ADDR is returned. That is safer than the default WP behavior, but you still need to populate the list to actually use forwarded IPs.