From fb76e537641e80584cbc0891ca3da1607741b6b4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 10 Sep 2024 10:17:27 +0200 Subject: [PATCH] Work around parse_url() bug --- .../Controller/RedirectController.php | 11 ++++++++--- .../Tests/Controller/RedirectControllerTest.php | 14 ++++++++++++++ .../Component/DomCrawler/Tests/UriResolverTest.php | 1 + src/Symfony/Component/DomCrawler/UriResolver.php | 6 +++++- .../Component/HttpClient/HttpClientTrait.php | 5 ++++- .../Component/HttpClient/NativeHttpClient.php | 2 +- src/Symfony/Component/HttpFoundation/Request.php | 6 +++++- .../Component/HttpFoundation/Tests/RequestTest.php | 3 +++ .../Http/Impersonate/ImpersonateUrlGenerator.php | 2 +- 9 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php index 702d69748062b..3d8efe0deab1b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php @@ -119,14 +119,19 @@ public function urlRedirectAction(Request $request, string $path, bool $permanen $statusCode = $permanent ? 301 : 302; } + if (null === $scheme) { + $scheme = $request->getScheme(); + } + + if (str_starts_with($path, '//')) { + $path = $scheme.':'.$path; + } + // redirect if the path is a full URL if (parse_url($path, \PHP_URL_SCHEME)) { return new RedirectResponse($path, $statusCode); } - if (null === $scheme) { - $scheme = $request->getScheme(); - } if ($qs = $request->server->get('QUERY_STRING') ?: $request->getQueryString()) { if (!str_contains($path, '?')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php index b2da9ef58c5c1..161424e0e43ee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php @@ -183,6 +183,20 @@ public function testFullURLWithMethodKeep() $this->assertEquals(307, $returnResponse->getStatusCode()); } + public function testProtocolRelative() + { + $request = new Request(); + $controller = new RedirectController(); + + $returnResponse = $controller->urlRedirectAction($request, '//foo.bar/'); + $this->assertRedirectUrl($returnResponse, 'http://foo.bar/'); + $this->assertSame(302, $returnResponse->getStatusCode()); + + $returnResponse = $controller->urlRedirectAction($request, '//foo.bar/', false, 'https'); + $this->assertRedirectUrl($returnResponse, 'https://foo.bar/'); + $this->assertSame(302, $returnResponse->getStatusCode()); + } + public function testUrlRedirectDefaultPorts() { $host = 'www.example.com'; diff --git a/src/Symfony/Component/DomCrawler/Tests/UriResolverTest.php b/src/Symfony/Component/DomCrawler/Tests/UriResolverTest.php index ab98cb52cbeeb..e0a2a990802b4 100644 --- a/src/Symfony/Component/DomCrawler/Tests/UriResolverTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/UriResolverTest.php @@ -86,6 +86,7 @@ public static function provideResolverTests() ['foo', 'http://localhost#bar', 'http://localhost/foo'], ['http://', 'http://localhost', 'http://'], + ['/foo:123', 'http://localhost', 'http://localhost/foo:123'], ]; } } diff --git a/src/Symfony/Component/DomCrawler/UriResolver.php b/src/Symfony/Component/DomCrawler/UriResolver.php index 5ff2245284c67..4140dc05d0be7 100644 --- a/src/Symfony/Component/DomCrawler/UriResolver.php +++ b/src/Symfony/Component/DomCrawler/UriResolver.php @@ -32,8 +32,12 @@ public static function resolve(string $uri, ?string $baseUri): string { $uri = trim($uri); + if (false === ($scheme = parse_url($uri, \PHP_URL_SCHEME)) && '/' === ($uri[0] ?? '')) { + $scheme = parse_url($uri.'#', \PHP_URL_SCHEME); + } + // absolute URL? - if (null !== parse_url($uri, \PHP_URL_SCHEME)) { + if (null !== $scheme) { return $uri; } diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index d436a4c04cda4..3da4b2942efb1 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -515,7 +515,10 @@ private static function resolveUrl(array $url, ?array $base, array $queryDefault private static function parseUrl(string $url, array $query = [], array $allowedSchemes = ['http' => 80, 'https' => 443]): array { if (false === $parts = parse_url($url)) { - throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url)); + if ('/' !== ($url[0] ?? '') || false === $parts = parse_url($url.'#')) { + throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url)); + } + unset($parts['fragment']); } if ($query) { diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php index 0880513d6aeb9..e5bc61ce70cd2 100644 --- a/src/Symfony/Component/HttpClient/NativeHttpClient.php +++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php @@ -416,7 +416,7 @@ private static function createRedirectResolver(array $options, string $host, ?ar [$host, $port] = self::parseHostPort($url, $info); - if (false !== (parse_url($location, \PHP_URL_HOST) ?? false)) { + if (false !== (parse_url($location.'#', \PHP_URL_HOST) ?? false)) { // Authorization and Cookie headers MUST NOT follow except for the initial host name $requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth']; $requestHeaders[] = 'Host: '.$host.$port; diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 75db0300b8a57..561cb887fc453 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -355,7 +355,11 @@ public static function create(string $uri, string $method = 'GET', array $parame $server['PATH_INFO'] = ''; $server['REQUEST_METHOD'] = strtoupper($method); - $components = parse_url($uri); + if (false === ($components = parse_url($uri)) && '/' === ($uri[0] ?? '')) { + $components = parse_url($uri.'#'); + unset($components['fragment']); + } + if (isset($components['host'])) { $server['SERVER_NAME'] = $components['host']; $server['HTTP_HOST'] = $components['host']; diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php index 395df09c525cd..082e8695c3a7f 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php @@ -244,6 +244,9 @@ public function testCreate() // Fragment should not be included in the URI $request = Request::create('http://test.com/foo#bar'); $this->assertEquals('http://test.com/foo', $request->getUri()); + + $request = Request::create('/foo:123'); + $this->assertEquals('http://localhost/foo:123', $request->getUri()); } public function testCreateWithRequestUri() diff --git a/src/Symfony/Component/Security/Http/Impersonate/ImpersonateUrlGenerator.php b/src/Symfony/Component/Security/Http/Impersonate/ImpersonateUrlGenerator.php index 512b6efc7294a..cccc3784cf65a 100644 --- a/src/Symfony/Component/Security/Http/Impersonate/ImpersonateUrlGenerator.php +++ b/src/Symfony/Component/Security/Http/Impersonate/ImpersonateUrlGenerator.php @@ -69,7 +69,7 @@ private function buildExitPath(?string $targetUri = null): string $targetUri = $request->getRequestUri(); } - $targetUri .= (parse_url($targetUri, \PHP_URL_QUERY) ? '&' : '?').http_build_query([$switchUserConfig['parameter'] => SwitchUserListener::EXIT_VALUE], '', '&'); + $targetUri .= (str_contains($targetUri, '?') ? '&' : '?').http_build_query([$switchUserConfig['parameter'] => SwitchUserListener::EXIT_VALUE], '', '&'); return $targetUri; }