CVE-2021-43836: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Sulu is an open-source PHP content management system based on the Symfony framework. In affected versions an attacker can read arbitrary local files via a PHP file include. In a default configuration this also leads to remote code execution. The problem is patched with the Versions 1.6.44, 2.2.18, 2.3.8, 2.4.0. For users unable to upgrade overwrite the service `sulu_route.generator.expression_token_provider` and wrap the translator before passing it to the expression language.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 8.8
Published: 2021-12-15T20:15Z
Repository: https://github.com/sulu/sulu
Language: PHP
Files changed: src/Sulu/Bundle/RouteBundle/Generator/SymfonyExpressionTokenProvider.php, src/Sulu/Bundle/RouteBundle/Generator/TranslatorWrapper.php, src/Sulu/Bundle/RouteBundle/Tests/Unit/Generator/SymfonyExpressionTokenProviderTest.php
Commit date: 2021-12-07 13:16:26 +0100
Commit message: Merge pull request from GHSA-vx6j-pjrh-vgjh


Diff: diff --git a/src/Sulu/Bundle/RouteBundle/Generator/SymfonyExpressionTokenProvider.php b/src/Sulu/Bundle/RouteBundle/Generator/SymfonyExpressionTokenProvider.php
index d44f6506a7..359e3e83e9 100644
--- a/src/Sulu/Bundle/RouteBundle/Generator/SymfonyExpressionTokenProvider.php
+++ b/src/Sulu/Bundle/RouteBundle/Generator/SymfonyExpressionTokenProvider.php
@@ -42,11 +42,14 @@ class SymfonyExpressionTokenProvider implements TokenProviderInterface
         try {
             if (\method_exists($entity, 'getLocale')) {
                 $this->translator->setLocale($entity->getLocale());
             }
 
-            $result = $this->expressionLanguage->evaluate($name, ['object' => $entity, 'translator' => $this->translator]);
+            $result = $this->expressionLanguage->evaluate($name, [
+                'object' => $entity,
+                'translator' => new TranslatorWrapper($this->translator),
+            ]);
             $this->translator->setLocale($locale);
 
             return $result;
         } catch (\Exception $e) {
             throw new CannotEvaluateTokenException($name, $entity, $e);
diff --git a/src/Sulu/Bundle/RouteBundle/Generator/TranslatorWrapper.php b/src/Sulu/Bundle/RouteBundle/Generator/TranslatorWrapper.php
new file mode 100644
index 0000000000..d29dce0654
--- /dev/null
+++ b/src/Sulu/Bundle/RouteBundle/Generator/TranslatorWrapper.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Sulu\Bundle\RouteBundle\Generator;
+
+use Symfony\Component\Translation\TranslatorInterface;
+
+/**
+ * @internal
+ */
+class TranslatorWrapper implements TranslatorInterface
+{
+    /**
+     * @var TranslatorInterface
+     */
+    private $translator;
+
+    public function __construct(TranslatorInterface $translator)
+    {
+        $this->translator = $translator;
+    }
+
+    public function trans($id, array $parameters = [], $domain = null, $locale = null)
+    {
+        return $this->translator->trans($id, $parameters, $domain, $locale);
+    }
+
+    public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null)
+    {
+        return $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
+    }
+
+    public function setLocale($locale)
+    {
+        throw new \Exception('Not supported.');
+    }
+
+    public function getLocale()
+    {
+        return $this->translator->getLocale();
+    }
+}
diff --git a/src/Sulu/Bundle/RouteBundle/Tests/Unit/Generator/SymfonyExpressionTokenProviderTest.php b/src/Sulu/Bundle/RouteBundle/Tests/Unit/Generator/SymfonyExpressionTokenProviderTest.php
index c9f9f6a34c..8f1a3072e4 100644
--- a/src/Sulu/Bundle/RouteBundle/Tests/Unit/Generator/SymfonyExpressionTokenProviderTest.php
+++ b/src/Sulu/Bundle/RouteBundle/Tests/Unit/Generator/SymfonyExpressionTokenProviderTest.php
@@ -9,13 +9,15 @@
  * with this source code in the file LICENSE.
  */
 
 namespace Sulu\Bundle\RouteBundle\Tests\Unit\Generator;
 
+use Prophecy\Argument;
 use Sulu\Bundle\RouteBundle\Generator\CannotEvaluateTokenException;
 use Sulu\Bundle\RouteBundle\Generator\SymfonyExpressionTokenProvider;
 use Sulu\Bundle\RouteBundle\Model\RoutableInterface;
+use Symfony\Component\Translation\Translator;
 use Symfony\Component\Translation\TranslatorInterface;
 
 class SymfonyExpressionTokenProviderTest extends \PHPUnit_Framework_TestCase
 {
     public function testResolve()
@@ -35,18 +37,35 @@ class SymfonyExpressionTokenProviderTest extends \PHPUnit_Framework_TestCase
     public function testResolveTranslation()
     {
         $translator = $this->prophesize(TranslatorInterface::class);
         $translator->getLocale()->willReturn('de');
         $translator->setLocale('de')->shouldBeCalled();
-        $translator->trans('test-key')->willReturn('TEST');
+        $translator->trans('test-key', Argument::cetera())->willReturn('TEST');
         $entity = $this->prophesize(RoutableInterface::class);
         $entity->getLocale()->willReturn('en');
 
         $provider = new SymfonyExpressionTokenProvider($translator->reveal());
         $this->assertEquals('TEST', $provider->provide($entity, 'translator.trans("test-key")'));
     }
 
+    public function testResolveTranslationAddResource()
+    {
+        $this->setExpectedException(CannotEvaluateTokenException::class);
+
+        $translator = $this->prophesize(Translator::class);
+        $translator->getLocale()->willReturn('de');
+        $translator->setLocale('de')->shouldBeCalled();
+        $entity = $this->prophesize(RoutableInterface::class);
+        $entity->getLocale()->willReturn('en');
+
+        $entity->getLocale = function() {
+            return 'en';
+        };
+        $provider = new SymfonyExpressionTokenProvider($translator->reveal());
+        $provider->provide($entity, 'translator.addResource("php", "/test.php")');
+    }
+
     public function testResolveNotExists()
     {
         $this->setExpectedException(CannotEvaluateTokenException::class);
 
         $translator = $this->prophesize(TranslatorInterface::class);

Vulnerable code:             $result = $this->expressionLanguage->evaluate($name, ['object' => $entity, 'translator' => $this->translator]);
        $translator->trans('test-key')->willReturn('TEST');
Fixed code:             $result = $this->expressionLanguage->evaluate($name, [
                'object' => $entity,
                'translator' => new TranslatorWrapper($this->translator),
            ]);
<?php

namespace Sulu\Bundle\RouteBundle\Generator;

use Symfony\Component\Translation\TranslatorInterface;

/**
 * @internal
 */
class TranslatorWrapper implements TranslatorInterface
{
    /**
     * @var TranslatorInterface
     */
    private $translator;

    public function __construct(TranslatorInterface $translator)
    {
        $this->translator = $translator;
    }

    public function trans($id, array $parameters = [], $domain = null, $locale = null)
    {
        return $this->translator->trans($id, $parameters, $domain, $locale);
    }

    public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null)
    {
        return $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
    }

    public function setLocale($locale)
    {
        throw new \Exception('Not supported.');
    }

    public function getLocale()
    {
        return $this->translator->getLocale();
    }
}
use Prophecy\Argument;
use Symfony\Component\Translation\Translator;
        $translator->trans('test-key', Argument::cetera())->willReturn('TEST');
    public function testResolveTranslationAddResource()
    {
        $this->setExpectedException(CannotEvaluateTokenException::class);

        $translator = $this->prophesize(Translator::class);
        $translator->getLocale()->willReturn('de');
        $translator->setLocale('de')->shouldBeCalled();
        $entity = $this->prophesize(RoutableInterface::class);
        $entity->getLocale()->willReturn('en');

        $entity->getLocale = function() {
            return 'en';
        };
        $provider = new SymfonyExpressionTokenProvider($translator->reveal());
        $provider->provide($entity, 'translator.addResource("php", "/test.php")');
    }