Skip to content
This repository was archived by the owner on Nov 13, 2025. It is now read-only.
/ symfony-index0 Public archive

Proof of concept for two Symfony bugs leading to bypass of authorization

Notifications You must be signed in to change notification settings

cs278/symfony-index0

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Symfony /index0.php problem

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:

  1. UrlMatcher normalizes falsy strings to / rather than just the empty string – I’m pretty sure this is a bug.
  2. 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.

Issue 1

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();

Issue 2

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.

Demo

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'
200

The last request should result in a 404.

About

Proof of concept for two Symfony bugs leading to bypass of authorization

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages