diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index c08c01530b79..a825c961e65a 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -283,7 +283,21 @@ public function __construct(FileLocatorInterface $locator, Modules $moduleConfig $this->fileLocator = $locator; $this->moduleConfig = $moduleConfig; - $this->httpHost = service('request')->getServer('HTTP_HOST'); + // Remove port from HTTP_HOST (supports domains, IPv4, and IPv6, e.g., [::1]:8080) + $httpHost = service('request')->getServer('HTTP_HOST'); + + if ($httpHost !== null) { + $host = parse_url('http://' . $httpHost, PHP_URL_HOST); + + $isValid = $host && ( + filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) + || filter_var(trim($host, '[]'), FILTER_VALIDATE_IP) + ); + + $this->httpHost = $isValid ? strtolower($host) : null; + } else { + $this->httpHost = null; + } // Setup based on config file. Let routes file override. $this->defaultNamespace = rtrim($routing->defaultNamespace, '\\') . '\\'; diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 5e3e7bd8015f..a785c6cf640d 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -579,6 +579,84 @@ public function testHostnameOption(): void $this->assertSame($expected, $routes->getRoutes()); } + #[DataProvider('provideHostnameOptionWithPortMatchesCorrectly')] + public function testHostnameOptionWithPortMatchesCorrectly( + string $incomingHost, + string $routeHostname, + bool $shouldMatch, + ): void { + $superglobals = service('superglobals'); + $originalHost = $superglobals->server('HTTP_HOST'); + + try { + $superglobals->setServer('HTTP_HOST', $incomingHost); + + $routes = $this->getCollector(); + $routes->add('test-route', 'Controller::method', ['hostname' => $routeHostname]); + + $expected = $shouldMatch ? ['test-route' => '\Controller::method'] : []; + + $this->assertSame($expected, $routes->getRoutes()); + } finally { + // Restore original HTTP_HOST or clear it if it wasn't set originally + if ($originalHost !== null) { + $superglobals->setServer('HTTP_HOST', $originalHost); + } else { + $superglobals->setServer('HTTP_HOST', []); + } + } + } + + /** + * @return iterable + */ + public static function provideHostnameOptionWithPortMatchesCorrectly(): iterable + { + yield from self::provideHostnameWithPortCases(); + } + + /** + * @return iterable + */ + public static function provideHostnameWithPortCases(): iterable + { + yield 'domain with dev port' => [ + 'example.com:8080', + 'example.com', + true, + ]; + + yield 'domain with custom port' => [ + 'example.com:3000', + 'example.com', + true, + ]; + + yield 'case-insensitive domain with port' => [ + 'EXAMPLE.COM:8080', + 'example.com', + true, + ]; + + yield 'IPv6 address with port' => [ + '[::1]:8080', + '[::1]', + true, + ]; + + yield 'IPv6 address without port' => [ + '[::1]', + '[::1]', + true, + ]; + + yield 'mismatched domain with port should not match' => [ + 'attacker.com:8080', + 'example.com', + false, + ]; + } + public function testResourceScaffoldsCorrectly(): void { $routes = $this->getCollector();