diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 47be887f..ce464c80 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -243,9 +243,8 @@ - + $headers['cookie'] - $host $headers diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php index 30eb71cb..506970d9 100644 --- a/src/ServerRequestFactory.php +++ b/src/ServerRequestFactory.php @@ -168,7 +168,15 @@ private static function marshalHostAndPort(array $server, array $headers) : arra $host = self::getHeaderFromArray('host', $headers, false); if ($host !== false) { - return self::marshalHostAndPortFromHeader($host); + // Ignore obviously malformed host headers: + // - Whitespace is invalid within a hostname and break the URI representation within HTTP. + // non-printable characters other than SPACE and TAB are already rejected by HeaderSecurity. + // - A comma indicates that multiple host headers have been sent which is not legal + // and might be used in an attack where a load balancer sees a different host header + // than Diactoros. + if (! \preg_match('/[\\t ,]/', $host)) { + return self::marshalHostAndPortFromHeader($host); + } } if (! isset($server['SERVER_NAME'])) { @@ -289,9 +297,10 @@ private static function marshalHostAndPortFromHeader($host): array /** * Retrieve a header value from an array of headers using a case-insensitive lookup. * + * @template T * @param array> $headers Key/value header pairs - * @param mixed $default Default value to return if header not found - * @return mixed + * @param T $default Default value to return if header not found + * @return string|T */ private static function getHeaderFromArray(string $name, array $headers, $default = null) { diff --git a/test/ServerRequestFactoryTest.php b/test/ServerRequestFactoryTest.php index eeda1682..e52c0aad 100644 --- a/test/ServerRequestFactoryTest.php +++ b/test/ServerRequestFactoryTest.php @@ -786,4 +786,40 @@ public function testHonorsHostHeaderOverServerNameWhenMarshalingUrl(): void $uri = $request->getUri(); $this->assertSame('example.com', $uri->getHost()); } + + /** + * @psalm-return iterable + */ + public function invalidHostHeaders(): iterable + { + return [ + 'comma' => ['example.com,example.net'], + 'space' => ['example com'], + 'tab' => ["example\tcom"], + ]; + } + + /** + * @dataProvider invalidHostHeaders + */ + public function testRejectsDuplicatedHostHeader(string $host): void + { + $server = [ + 'HTTP_HOST' => $host, + ]; + + $request = ServerRequestFactory::fromGlobals( + $server, + null, + null, + null, + null, + new DoNotFilter() + ); + + $uri = $request->getUri(); + $this->assertSame('', $uri->getHost()); + } }