clientIp([ 'REMOTE_ADDR' => '203.0.113.5', 'HTTP_X_FORWARDED_FOR' => '198.51.100.7, 192.0.2.1', ])); } public function testTrustsXffWhenRemoteIsTrusted(): void { $tp = new TrustedProxies(['10.0.0.0/8']); self::assertSame('203.0.113.5', $tp->clientIp([ 'REMOTE_ADDR' => '10.5.5.5', 'HTTP_X_FORWARDED_FOR' => '203.0.113.5', ])); } public function testWalksRightmostUntrustedHop(): void { // Two trusted internal hops in front of the real client. $tp = new TrustedProxies(['10.0.0.0/8', '192.168.0.0/16']); self::assertSame('203.0.113.5', $tp->clientIp([ 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => '203.0.113.5, 192.168.1.1, 10.0.0.2', ])); } public function testFallsBackToLeftmostWhenAllHopsTrusted(): void { $tp = new TrustedProxies(['10.0.0.0/8']); self::assertSame('10.0.0.7', $tp->clientIp([ 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => '10.0.0.7, 10.0.0.2', ])); } public function testIgnoresXffWhenRemoteUntrusted(): void { // Attacker hits the app directly and forges X-Forwarded-For. We // must NOT believe the header. $tp = new TrustedProxies(['10.0.0.0/8']); self::assertSame('203.0.113.42', $tp->clientIp([ 'REMOTE_ADDR' => '203.0.113.42', 'HTTP_X_FORWARDED_FOR' => '127.0.0.1, 10.0.0.1', ])); } public function testStripsPortFromXffHops(): void { $tp = new TrustedProxies(['10.0.0.0/8']); self::assertSame('203.0.113.5', $tp->clientIp([ 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => '203.0.113.5:51234', ])); } public function testIpv6Cidr(): void { $tp = new TrustedProxies(['2001:db8::/32']); self::assertSame('2001:db8::1', $tp->clientIp([ 'REMOTE_ADDR' => '2001:db8::a', 'HTTP_X_FORWARDED_FOR' => '2001:db8::1', ])); } public function testBareIpTreatedAsHostMask(): void { $tp = new TrustedProxies(['127.0.0.1']); self::assertTrue($tp->isTrusted('127.0.0.1')); self::assertFalse($tp->isTrusted('127.0.0.2')); } public function testInvalidEntriesAreSkippedNotTrusted(): void { // Typos must not silently widen trust. $tp = new TrustedProxies(['nonsense', '10.0.0.0/40', '10.0.0.0/8']); self::assertTrue($tp->isTrusted('10.1.2.3')); self::assertFalse($tp->isTrusted('203.0.113.5')); } public function testFromEnvParsesCommaList(): void { $prev = getenv('TRUSTED_PROXIES'); try { putenv('TRUSTED_PROXIES=10.0.0.0/8 , 192.168.0.0/16'); $tp = TrustedProxies::fromEnv(); self::assertTrue($tp->isTrusted('10.5.5.5')); self::assertTrue($tp->isTrusted('192.168.0.1')); self::assertFalse($tp->isTrusted('172.16.0.1')); } finally { $prev === false ? putenv('TRUSTED_PROXIES') : putenv('TRUSTED_PROXIES=' . $prev); } } public function testFromEnvWithBlankReturnsNoTrust(): void { $prev = getenv('TRUSTED_PROXIES'); try { putenv('TRUSTED_PROXIES= '); $tp = TrustedProxies::fromEnv(); self::assertFalse($tp->isTrusted('10.5.5.5')); self::assertFalse($tp->isTrusted('127.0.0.1')); } finally { $prev === false ? putenv('TRUSTED_PROXIES') : putenv('TRUSTED_PROXIES=' . $prev); } } public function testIsHttpsDirectTls(): void { $tp = new TrustedProxies([]); self::assertTrue($tp->isHttps(['HTTPS' => 'on'])); self::assertTrue($tp->isHttps(['REQUEST_SCHEME' => 'https'])); self::assertTrue($tp->isHttps(['SERVER_PORT' => '443'])); self::assertFalse($tp->isHttps(['HTTPS' => 'off'])); self::assertFalse($tp->isHttps([])); } public function testIsHttpsTrustsXfpFromTrustedProxyOnly(): void { $tp = new TrustedProxies(['10.0.0.0/8']); self::assertTrue($tp->isHttps([ 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_PROTO' => 'https', ])); // Same header but from an untrusted peer — must NOT be honoured. self::assertFalse($tp->isHttps([ 'REMOTE_ADDR' => '203.0.113.5', 'HTTP_X_FORWARDED_PROTO' => 'https', ])); } public function testIsHttpsHandlesMultiHopXfpList(): void { // RFC-style "https, http" → leftmost is what the user actually saw. $tp = new TrustedProxies(['10.0.0.0/8']); self::assertTrue($tp->isHttps([ 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_PROTO' => 'https, http', ])); self::assertFalse($tp->isHttps([ 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_PROTO' => 'http, https', ])); } }