diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index ab8f5b081f61b..5710507e628ee 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -351,10 +351,21 @@ public static function create(string $uri, string $method = 'GET', array $parame $server['PATH_INFO'] = ''; $server['REQUEST_METHOD'] = strtoupper($method); + if (($i = strcspn($uri, ':/?#')) && ':' === ($uri[$i] ?? null) && (strspn($uri, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-.') !== $i || strcspn($uri, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'))) { + throw new BadRequestException('Invalid URI: Scheme is malformed.'); + } if (false === $components = parse_url(\strlen($uri) !== strcspn($uri, '?#') ? $uri : $uri.'#')) { throw new BadRequestException('Invalid URI.'); } + $part = ($components['user'] ?? '').':'.($components['pass'] ?? ''); + + if (':' !== $part && \strlen($part) !== strcspn($part, '[]')) { + throw new BadRequestException('Invalid URI: Userinfo is malformed.'); + } + if (($part = $components['host'] ?? '') && !self::isHostValid($part)) { + throw new BadRequestException('Invalid URI: Host is malformed.'); + } if (false !== ($i = strpos($uri, '\\')) && $i < strcspn($uri, '?#')) { throw new BadRequestException('Invalid URI: A URI cannot contain a backslash.'); } @@ -1151,10 +1162,8 @@ public function getHost(): string // host is lowercase as per RFC 952/2181 $host = strtolower(preg_replace('/:\d+$/', '', trim($host))); - // as the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user) - // check that it does not contain forbidden characters (see RFC 952 and RFC 2181) - // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names - if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) { + // the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user) + if ($host && !self::isHostValid($host)) { if (!$this->isHostValid) { return ''; } @@ -2135,4 +2144,21 @@ private function isIisRewrite(): bool return $this->isIisRewrite; } + + /** + * See https://url.spec.whatwg.org/. + */ + private static function isHostValid(string $host): bool + { + if ('[' === $host[0]) { + return ']' === $host[-1] && filter_var(substr($host, 1, -1), \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6); + } + + if (preg_match('/\.[0-9]++\.?$/D', $host)) { + return null !== filter_var($host, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4 | \FILTER_NULL_ON_FAILURE); + } + + // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names + return '' === preg_replace('/[-a-zA-Z0-9_]++\.?/', '', $host); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index 3ab13d1f22c4f..429a2862d9faa 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -2237,11 +2237,9 @@ public function testFactory() Request::setFactory(null); } - /** - * @dataProvider getLongHostNames - */ - public function testVeryLongHosts($host) + public function testVeryLongHosts() { + $host = 'a'.str_repeat('.a', 40000); $start = microtime(true); $request = Request::create('/'); @@ -2284,14 +2282,6 @@ public static function getHostValidities() ]; } - public static function getLongHostNames() - { - return [ - ['a'.str_repeat('.a', 40000)], - [str_repeat(':', 101)], - ]; - } - /** * @dataProvider methodIdempotentProvider */ @@ -2667,6 +2657,71 @@ public function testReservedFlags() $this->assertNotSame(0b10000000, $value, \sprintf('The constant "%s" should not use the reserved value "0b10000000".', $constant)); } } + + /** + * @dataProvider provideMalformedUrls + */ + public function testMalformedUrls(string $url, string $expectedException) + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage($expectedException); + + Request::create($url); + } + + public static function provideMalformedUrls(): array + { + return [ + ['http://normal.com[@vulndetector.com/', 'Invalid URI: Userinfo is malformed.'], + ['http://[normal.com@vulndetector.com/', 'Invalid URI: Userinfo is malformed.'], + ['http://normal.com@[vulndetector.com/', 'Invalid URI: Host is malformed.'], + ['http://[[normal.com@][vulndetector.com/', 'Invalid URI: Userinfo is malformed.'], + ['http://[vulndetector.com]', 'Invalid URI: Host is malformed.'], + ['http://[0:0::vulndetector.com]:80', 'Invalid URI: Host is malformed.'], + ['http://[2001:db8::vulndetector.com]', 'Invalid URI: Host is malformed.'], + ['http://[malicious.com]', 'Invalid URI: Host is malformed.'], + ['http://[evil.org]', 'Invalid URI: Host is malformed.'], + ['http://[internal.server]', 'Invalid URI: Host is malformed.'], + ['http://[192.168.1.1]', 'Invalid URI: Host is malformed.'], + ['http://192.abc.1.1', 'Invalid URI: Host is malformed.'], + ['http://[localhost]', 'Invalid URI: Host is malformed.'], + ["\x80https://example.com", 'Invalid URI: Scheme is malformed.'], + ['>https://example.com', 'Invalid URI: Scheme is malformed.'], + ["http\x0b://example.com", 'Invalid URI: Scheme is malformed.'], + ["https\x80://example.com", 'Invalid URI: Scheme is malformed.'], + ['http>://example.com', 'Invalid URI: Scheme is malformed.'], + ['0http://example.com', 'Invalid URI: Scheme is malformed.'], + ]; + } + + /** + * @dataProvider provideLegitimateUrls + */ + public function testLegitimateUrls(string $url) + { + $request = Request::create($url); + + $this->assertInstanceOf(Request::class, $request); + } + + public static function provideLegitimateUrls(): array + { + return [ + ['http://example.com'], + ['https://example.com'], + ['http://example.com:8080'], + ['https://example.com:8443'], + ['http://user:pass@example.com'], + ['http://user:pass@example.com:8080'], + ['http://user:pass@example.com/path'], + ['http://[2001:db8::1]'], + ['http://[2001:db8::1]:8080'], + ['http://[2001:db8::1]/path'], + ['http://[::1]'], + ['http://example.com/path'], + [':path'], + ]; + } } class RequestContentProxy extends Request diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php index 8c270a8e6e13e..ffceed9876af8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php @@ -205,7 +205,8 @@ public function testRequestWithBadHost() { $this->expectException(BadRequestHttpException::class); $kernel = $this->createMock(HttpKernelInterface::class); - $request = Request::create('http://bad host %22/'); + $request = Request::create('/'); + $request->headers->set('host', 'bad host %22'); $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestMatcher = $this->createMock(RequestMatcherInterface::class);