1
0

InternalNetworkMiddlewareTest.php 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Unit\Http;
  4. use App\Infrastructure\Http\Middleware\InternalNetworkMiddleware;
  5. use PHPUnit\Framework\Attributes\DataProvider;
  6. use PHPUnit\Framework\TestCase;
  7. use Psr\Http\Message\ResponseInterface;
  8. use Psr\Http\Message\ServerRequestInterface;
  9. use Psr\Http\Server\RequestHandlerInterface;
  10. use Slim\Psr7\Factory\ResponseFactory;
  11. use Slim\Psr7\Factory\ServerRequestFactory;
  12. /**
  13. * Network gate must let RFC1918 + loopback through, 404 everything else,
  14. * and never leak via 403 (which would tell attackers the endpoint exists).
  15. * The handler is stubbed to a marker response so we can confirm whether
  16. * the middleware short-circuited or passed through.
  17. */
  18. final class InternalNetworkMiddlewareTest extends TestCase
  19. {
  20. /**
  21. * @return iterable<string, array{string, bool}>
  22. */
  23. public static function addressProvider(): iterable
  24. {
  25. yield 'loopback v4' => ['127.0.0.1', true];
  26. yield 'loopback v6' => ['::1', true];
  27. yield 'rfc1918 10/8' => ['10.5.6.7', true];
  28. yield 'rfc1918 172.16/12' => ['172.16.42.1', true];
  29. yield 'rfc1918 172.31/12 (boundary)' => ['172.31.255.255', true];
  30. yield 'just outside 172.16/12' => ['172.32.0.1', false];
  31. yield 'rfc1918 192.168/16' => ['192.168.1.1', true];
  32. yield 'public 1.1.1.1' => ['1.1.1.1', false];
  33. yield 'public v4' => ['203.0.113.4', false];
  34. yield 'public v6' => ['2001:db8::1', false];
  35. yield 'malformed' => ['not-an-ip', false];
  36. yield 'empty' => ['', false];
  37. }
  38. #[DataProvider('addressProvider')]
  39. public function testNetworkGate(string $remoteAddr, bool $shouldPass): void
  40. {
  41. $middleware = new InternalNetworkMiddleware(new ResponseFactory());
  42. $req = (new ServerRequestFactory())->createServerRequest(
  43. 'POST',
  44. '/internal/jobs/tick',
  45. ['REMOTE_ADDR' => $remoteAddr],
  46. );
  47. $passthrough = new class () implements RequestHandlerInterface {
  48. public bool $reached = false;
  49. public function handle(ServerRequestInterface $request): ResponseInterface
  50. {
  51. $this->reached = true;
  52. $factory = new ResponseFactory();
  53. return $factory->createResponse(204);
  54. }
  55. };
  56. $response = $middleware->process($req, $passthrough);
  57. if ($shouldPass) {
  58. self::assertSame(204, $response->getStatusCode());
  59. self::assertTrue($passthrough->reached);
  60. } else {
  61. self::assertSame(404, $response->getStatusCode());
  62. self::assertFalse($passthrough->reached, 'handler must not see disallowed sources');
  63. }
  64. }
  65. }