Important
The router was fixed in Symfony 6.4 in: symfony/symfony#62290
The authorization bypass was issued CVE-2025-64500 / GHSA-3rg7-wf37-54rm and fixed in Symfony 5.4.
If a client makes a request to /index.php0 it can bypass the authorization
layer and allow users access to resources they should not be able to access,
depending on the Symfony configuration.
I expect this also depends on the web server configuration, but the issue is reproducible on Platform.sh and Symfony CLI.
This appears to be possible due to 2 separate issues:
UrlMatchernormalizes falsy strings to/rather than just the empty string – I’m pretty sure this is a bug.- Security bundle does not enforce that paths begin with
/when applying access control rules. This does not appear to be mentioned in the documentation anywhere either, certainly all the examples use a leading forward slash.
Combined these two issues make it possible for authorization bypass.
If you make a request to /index.php0 this results in PATH_INFO of '0' (string zero)
which is falsy. Inside UrlMatcher (and friends) use of the shorthand ternary
operator results in '0' being normalised to /, this causes routes to match
which should not. issue1.php is a minimal reproducer for this problem.
Patch with test for this issue
diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/CompiledUrlMatcherTrait.php b/src/Symfony/Component/Routing/Matcher/Dumper/CompiledUrlMatcherTrait.php
index db754e6de0..9b03102b71 100644
--- a/src/Symfony/Component/Routing/Matcher/Dumper/CompiledUrlMatcherTrait.php
+++ b/src/Symfony/Component/Routing/Matcher/Dumper/CompiledUrlMatcherTrait.php
@@ -73,8 +73,8 @@ public function match(string $pathinfo): array
private function doMatch(string $pathinfo, array &$allow = [], array &$allowSchemes = []): array
{
$allow = $allowSchemes = [];
- $pathinfo = rawurldecode($pathinfo) ?: '/';
- $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/';
+ $pathinfo = self::nonEmptyString(rawurldecode($pathinfo), '/');
+ $trimmedPathinfo = self::nonEmptyString(rtrim($pathinfo, '/'), '/');
$context = $this->context;
$requestMethod = $canonicalMethod = $context->getMethod();
diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php
index ec281fde73..73150cfb5e 100644
--- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php
+++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php
@@ -79,7 +79,7 @@ public function match(string $pathinfo): array
{
$this->allow = $this->allowSchemes = [];
- if ($ret = $this->matchCollection(rawurldecode($pathinfo) ?: '/', $this->routes)) {
+ if ($ret = $this->matchCollection(self::nonEmptyString(rawurldecode($pathinfo), '/'), $this->routes)) {
return $ret;
}
@@ -109,6 +109,19 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac
$this->expressionLanguageProviders[] = $provider;
}
+ /**
+ * Ensure string is not of zero length.
+ *
+ * @param string $input
+ * @param non-empty-string $onEmpty
+ *
+ * @return non-empty-string
+ */
+ protected static function nonEmptyString(string $input, string $onEmpty): string
+ {
+ return $input !== '' ? $input : $onEmpty;
+ }
+
/**
* Tries to match a URL with a set of routes.
*
diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php
index fcc2eda113..ee50618c4f 100644
--- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php
+++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php
@@ -22,6 +22,22 @@
class UrlMatcherTest extends TestCase
{
+ public function testZero(): void
+ {
+ $this->expectNotToPerformAssertions();
+
+ $coll = new RouteCollection();
+ $coll->add('index', new Route('/'));
+
+ $matcher = $this->getUrlMatcher($coll);
+
+ try {
+ $matcher->match('0');
+ $this->fail();
+ } catch (ResourceNotFoundException $e) {
+ }
+ }
+
public function testNoMethodSoAllowed()
{
$coll = new RouteCollection();I’m not so sure if this is actually a bug, but it seems like the documentation could do with an example showing how to correctly add a rule that applies to any request.
With this repository running symfony serve, you can see the problem:
$ curl -s -o /dev/null -w "%{http_code}\n" 'http://127.0.0.1:8003/'
401
$ curl -s -o /dev/null -w "%{http_code}\n" 'http://127.0.0.1:8003/index.php1'
404
$ curl -s -o /dev/null -w "%{http_code}\n" 'http://127.0.0.1:8003/index.php'
401
# This is the problematic request
$ curl -s -o /dev/null -w "%{http_code}\n" 'http://127.0.0.1:8003/index.php0'
200The last request should result in a 404.