| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- <?php
- /*
- * Copyright 2026 Alessandro Chiapparini <sprint_planer_web@chiapparini.org>
- * SPDX-License-Identifier: Apache-2.0
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * See the LICENSE file in the project root for the full license text.
- */
- declare(strict_types=1);
- namespace App\Tests\Http;
- use App\Http\TrustedProxies;
- use App\Tests\TestCase;
- /**
- * Pins the contract for R01-N05 + R01-N07: the proxy-aware client-IP and
- * HTTPS detector. The class is pure (no DB/session) so we exercise it with
- * plain `$_SERVER`-shaped arrays.
- */
- final class TrustedProxiesTest extends TestCase
- {
- public function testEmptyConfigReturnsRemoteAddrVerbatim(): void
- {
- $tp = new TrustedProxies([]);
- self::assertSame('203.0.113.5', $tp->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',
- ]));
- }
- }
|