1
0

TrustedProxiesTest.php 5.9 KB

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