diff --git a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php index 2d978a367e17b..c0e4e03ae3934 100644 --- a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php +++ b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php @@ -310,6 +310,12 @@ public function getEnv(string $prefix, string $name, \Closure $getEnv): mixed if (!isset($params['scheme'], $params['host'])) { throw new RuntimeException(\sprintf('Invalid URL in env var "%s": scheme and host expected.', $name)); } + if (('\\' !== \DIRECTORY_SEPARATOR || 'file' !== $params['scheme']) && false !== ($i = strpos($env, '\\')) && $i < strcspn($env, '?#')) { + throw new RuntimeException(\sprintf('Invalid URL in env var "%s": backslashes are not allowed.', $name)); + } + if (\ord($env[0]) <= 32 || \ord($env[-1]) <= 32 || \strlen($env) !== strcspn($env, "\r\n\t")) { + throw new RuntimeException(\sprintf('Invalid URL in env var "%s": leading/trailing ASCII control characters or whitespaces are not allowed.', $name)); + } $params += [ 'port' => null, 'user' => null, diff --git a/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php b/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php index afe15f3ebca4a..e5875c6282739 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php @@ -996,6 +996,27 @@ public static function provideGetEnvUrlPath() ]; } + /** + * @testWith ["http://foo.com\\bar"] + * ["\\\\foo.com/bar"] + * ["a\rb"] + * ["a\nb"] + * ["a\tb"] + * ["\u0000foo"] + * ["foo\u0000"] + * [" foo"] + * ["foo "] + * [":"] + */ + public function testGetEnvBadUrl(string $url) + { + $this->expectException(RuntimeException::class); + + (new EnvVarProcessor(new Container()))->getEnv('url', 'foo', static function () use ($url): string { + return $url; + }); + } + /** * @testWith ["", "string"] * [null, ""] diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index 41d12230ed032..d6fe59d9aa634 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -625,6 +625,16 @@ 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 !== ($i = strpos($url, '\\')) && $i < strcspn($url, '?#')) { + throw new InvalidArgumentException(\sprintf('Malformed URL "%s": backslashes are not allowed.', $url)); + } + if (\strlen($url) !== strcspn($url, "\r\n\t")) { + throw new InvalidArgumentException(\sprintf('Malformed URL "%s": CR/LF/TAB characters are not allowed.', $url)); + } + if ('' !== $url && (\ord($url[0]) <= 32 || \ord($url[-1]) <= 32)) { + throw new InvalidArgumentException(\sprintf('Malformed URL "%s": leading/trailing ASCII control characters or spaces are not allowed.', $url)); + } + if (false === $parts = parse_url($url)) { if ('/' !== ($url[0] ?? '') || false === $parts = parse_url($url.'#')) { throw new InvalidArgumentException(\sprintf('Malformed URL "%s".', $url)); diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php index 2e7a166a45600..9176e349b18aa 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTraitTest.php @@ -239,13 +239,32 @@ public function testResolveUrlWithoutScheme() self::resolveUrl(self::parseUrl('localhost:8080'), null); } - public function testResolveBaseUrlWitoutScheme() + public function testResolveBaseUrlWithoutScheme() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid URL: scheme is missing in "//localhost:8081". Did you forget to add "http(s)://"?'); self::resolveUrl(self::parseUrl('/foo'), self::parseUrl('localhost:8081')); } + /** + * @testWith ["http://foo.com\\bar"] + * ["\\\\foo.com/bar"] + * ["a\rb"] + * ["a\nb"] + * ["a\tb"] + * ["\u0000foo"] + * ["foo\u0000"] + * [" foo"] + * ["foo "] + * [":"] + */ + public function testParseMalformedUrl(string $url) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Malformed URL'); + self::parseUrl($url); + } + /** * @dataProvider provideParseUrl */ diff --git a/src/Symfony/Component/Routing/RequestContext.php b/src/Symfony/Component/Routing/RequestContext.php index e3f4831b3cc7b..5e9e79d916802 100644 --- a/src/Symfony/Component/Routing/RequestContext.php +++ b/src/Symfony/Component/Routing/RequestContext.php @@ -47,6 +47,13 @@ public function __construct(string $baseUrl = '', string $method = 'GET', string public static function fromUri(string $uri, string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443): self { + if (false !== ($i = strpos($uri, '\\')) && $i < strcspn($uri, '?#')) { + $uri = ''; + } + if ('' !== $uri && (\ord($uri[0]) <= 32 || \ord($uri[-1]) <= 32 || \strlen($uri) !== strcspn($uri, "\r\n\t"))) { + $uri = ''; + } + $uri = parse_url($uri); $scheme = $uri['scheme'] ?? $scheme; $host = $uri['host'] ?? $host; diff --git a/src/Symfony/Component/Routing/Tests/RequestContextTest.php b/src/Symfony/Component/Routing/Tests/RequestContextTest.php index 179ef33d2aae0..fcc42ff593a96 100644 --- a/src/Symfony/Component/Routing/Tests/RequestContextTest.php +++ b/src/Symfony/Component/Routing/Tests/RequestContextTest.php @@ -85,6 +85,28 @@ public function testFromUriBeingEmpty() $this->assertSame('/', $requestContext->getPathInfo()); } + /** + * @testWith ["http://foo.com\\bar"] + * ["\\\\foo.com/bar"] + * ["a\rb"] + * ["a\nb"] + * ["a\tb"] + * ["\u0000foo"] + * ["foo\u0000"] + * [" foo"] + * ["foo "] + * [":"] + */ + public function testFromBadUri(string $uri) + { + $context = RequestContext::fromUri($uri); + + $this->assertSame('http', $context->getScheme()); + $this->assertSame('localhost', $context->getHost()); + $this->assertSame('', $context->getBaseUrl()); + $this->assertSame('/', $context->getPathInfo()); + } + public function testFromRequest() { $request = Request::create('https://test.com:444/foo?bar=baz');