1
0

TrustedProxiesTest.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Http;
  4. use App\Http\TrustedProxies;
  5. use App\Tests\TestCase;
  6. /**
  7. * Pins the contract for R01-N05 + R01-N07: the proxy-aware client-IP and
  8. * HTTPS detector. The class is pure (no DB/session) so we exercise it with
  9. * plain `$_SERVER`-shaped arrays.
  10. */
  11. final class TrustedProxiesTest extends TestCase
  12. {
  13. public function testEmptyConfigReturnsRemoteAddrVerbatim(): void
  14. {
  15. $tp = new TrustedProxies([]);
  16. self::assertSame('203.0.113.5', $tp->clientIp([
  17. 'REMOTE_ADDR' => '203.0.113.5',
  18. 'HTTP_X_FORWARDED_FOR' => '198.51.100.7, 192.0.2.1',
  19. ]));
  20. }
  21. public function testTrustsXffWhenRemoteIsTrusted(): void
  22. {
  23. $tp = new TrustedProxies(['10.0.0.0/8']);
  24. self::assertSame('203.0.113.5', $tp->clientIp([
  25. 'REMOTE_ADDR' => '10.5.5.5',
  26. 'HTTP_X_FORWARDED_FOR' => '203.0.113.5',
  27. ]));
  28. }
  29. public function testWalksRightmostUntrustedHop(): void
  30. {
  31. // Two trusted internal hops in front of the real client.
  32. $tp = new TrustedProxies(['10.0.0.0/8', '192.168.0.0/16']);
  33. self::assertSame('203.0.113.5', $tp->clientIp([
  34. 'REMOTE_ADDR' => '10.0.0.1',
  35. 'HTTP_X_FORWARDED_FOR' => '203.0.113.5, 192.168.1.1, 10.0.0.2',
  36. ]));
  37. }
  38. public function testFallsBackToLeftmostWhenAllHopsTrusted(): void
  39. {
  40. $tp = new TrustedProxies(['10.0.0.0/8']);
  41. self::assertSame('10.0.0.7', $tp->clientIp([
  42. 'REMOTE_ADDR' => '10.0.0.1',
  43. 'HTTP_X_FORWARDED_FOR' => '10.0.0.7, 10.0.0.2',
  44. ]));
  45. }
  46. public function testIgnoresXffWhenRemoteUntrusted(): void
  47. {
  48. // Attacker hits the app directly and forges X-Forwarded-For. We
  49. // must NOT believe the header.
  50. $tp = new TrustedProxies(['10.0.0.0/8']);
  51. self::assertSame('203.0.113.42', $tp->clientIp([
  52. 'REMOTE_ADDR' => '203.0.113.42',
  53. 'HTTP_X_FORWARDED_FOR' => '127.0.0.1, 10.0.0.1',
  54. ]));
  55. }
  56. public function testStripsPortFromXffHops(): void
  57. {
  58. $tp = new TrustedProxies(['10.0.0.0/8']);
  59. self::assertSame('203.0.113.5', $tp->clientIp([
  60. 'REMOTE_ADDR' => '10.0.0.1',
  61. 'HTTP_X_FORWARDED_FOR' => '203.0.113.5:51234',
  62. ]));
  63. }
  64. public function testIpv6Cidr(): void
  65. {
  66. $tp = new TrustedProxies(['2001:db8::/32']);
  67. self::assertSame('2001:db8::1', $tp->clientIp([
  68. 'REMOTE_ADDR' => '2001:db8::a',
  69. 'HTTP_X_FORWARDED_FOR' => '2001:db8::1',
  70. ]));
  71. }
  72. public function testBareIpTreatedAsHostMask(): void
  73. {
  74. $tp = new TrustedProxies(['127.0.0.1']);
  75. self::assertTrue($tp->isTrusted('127.0.0.1'));
  76. self::assertFalse($tp->isTrusted('127.0.0.2'));
  77. }
  78. public function testInvalidEntriesAreSkippedNotTrusted(): void
  79. {
  80. // Typos must not silently widen trust.
  81. $tp = new TrustedProxies(['nonsense', '10.0.0.0/40', '10.0.0.0/8']);
  82. self::assertTrue($tp->isTrusted('10.1.2.3'));
  83. self::assertFalse($tp->isTrusted('203.0.113.5'));
  84. }
  85. public function testFromEnvParsesCommaList(): void
  86. {
  87. $prev = getenv('TRUSTED_PROXIES');
  88. try {
  89. putenv('TRUSTED_PROXIES=10.0.0.0/8 , 192.168.0.0/16');
  90. $tp = TrustedProxies::fromEnv();
  91. self::assertTrue($tp->isTrusted('10.5.5.5'));
  92. self::assertTrue($tp->isTrusted('192.168.0.1'));
  93. self::assertFalse($tp->isTrusted('172.16.0.1'));
  94. } finally {
  95. $prev === false ? putenv('TRUSTED_PROXIES') : putenv('TRUSTED_PROXIES=' . $prev);
  96. }
  97. }
  98. public function testFromEnvWithBlankReturnsNoTrust(): void
  99. {
  100. $prev = getenv('TRUSTED_PROXIES');
  101. try {
  102. putenv('TRUSTED_PROXIES= ');
  103. $tp = TrustedProxies::fromEnv();
  104. self::assertFalse($tp->isTrusted('10.5.5.5'));
  105. self::assertFalse($tp->isTrusted('127.0.0.1'));
  106. } finally {
  107. $prev === false ? putenv('TRUSTED_PROXIES') : putenv('TRUSTED_PROXIES=' . $prev);
  108. }
  109. }
  110. public function testIsHttpsDirectTls(): void
  111. {
  112. $tp = new TrustedProxies([]);
  113. self::assertTrue($tp->isHttps(['HTTPS' => 'on']));
  114. self::assertTrue($tp->isHttps(['REQUEST_SCHEME' => 'https']));
  115. self::assertTrue($tp->isHttps(['SERVER_PORT' => '443']));
  116. self::assertFalse($tp->isHttps(['HTTPS' => 'off']));
  117. self::assertFalse($tp->isHttps([]));
  118. }
  119. public function testIsHttpsTrustsXfpFromTrustedProxyOnly(): void
  120. {
  121. $tp = new TrustedProxies(['10.0.0.0/8']);
  122. self::assertTrue($tp->isHttps([
  123. 'REMOTE_ADDR' => '10.0.0.1',
  124. 'HTTP_X_FORWARDED_PROTO' => 'https',
  125. ]));
  126. // Same header but from an untrusted peer — must NOT be honoured.
  127. self::assertFalse($tp->isHttps([
  128. 'REMOTE_ADDR' => '203.0.113.5',
  129. 'HTTP_X_FORWARDED_PROTO' => 'https',
  130. ]));
  131. }
  132. public function testIsHttpsHandlesMultiHopXfpList(): void
  133. {
  134. // RFC-style "https, http" → leftmost is what the user actually saw.
  135. $tp = new TrustedProxies(['10.0.0.0/8']);
  136. self::assertTrue($tp->isHttps([
  137. 'REMOTE_ADDR' => '10.0.0.1',
  138. 'HTTP_X_FORWARDED_PROTO' => 'https, http',
  139. ]));
  140. self::assertFalse($tp->isHttps([
  141. 'REMOTE_ADDR' => '10.0.0.1',
  142. 'HTTP_X_FORWARDED_PROTO' => 'http, https',
  143. ]));
  144. }
  145. }