VYPR
Moderate severityNVD Advisory· Published Nov 10, 2023· Updated Feb 13, 2025

Symfony potential Cross-site Scripting vulnerabilities in CodeExtension filters

CVE-2023-46734

Description

Symfony is a PHP framework for web and console applications and a set of reusable PHP components. Starting in versions 2.0.0, 5.0.0, and 6.0.0 and prior to versions 4.4.51, 5.4.31, and 6.3.8, some Twig filters in CodeExtension use is_safe=html but don't actually ensure their input is safe. As of versions 4.4.51, 5.4.31, and 6.3.8, Symfony now escapes the output of the affected filters.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
symfony/twig-bridgePackagist
>= 2.0.0, < 4.4.514.4.51
symfony/twig-bridgePackagist
>= 5.0.0, < 5.4.315.4.31
symfony/twig-bridgePackagist
>= 6.0.0, < 6.3.86.3.8
symfony/symfonyPackagist
>= 2.0.0, < 4.4.514.4.51
symfony/symfonyPackagist
>= 5.0.0, < 5.4.315.4.31
symfony/symfonyPackagist
>= 6.0.0, < 6.3.86.3.8

Affected products

1

Patches

2
5d095d5feb13

security #cve-2023-46734 [TwigBridge] Ensure CodeExtension's filters properly escape their input (nicolas-grekas, GromNaN)

https://github.com/symfony/symfonyNicolas GrekasNov 9, 2023via ghsa
2 files changed · +133 30
  • src/Symfony/Bridge/Twig/Extension/CodeExtension.php+14 9 modified
    @@ -48,8 +48,8 @@ public function __construct($fileLinkFormat, string $projectDir, string $charset
         public function getFilters()
         {
             return [
    -            new TwigFilter('abbr_class', [$this, 'abbrClass'], ['is_safe' => ['html']]),
    -            new TwigFilter('abbr_method', [$this, 'abbrMethod'], ['is_safe' => ['html']]),
    +            new TwigFilter('abbr_class', [$this, 'abbrClass'], ['is_safe' => ['html'], 'pre_escape' => 'html']),
    +            new TwigFilter('abbr_method', [$this, 'abbrMethod'], ['is_safe' => ['html'], 'pre_escape' => 'html']),
                 new TwigFilter('format_args', [$this, 'formatArgs'], ['is_safe' => ['html']]),
                 new TwigFilter('format_args_as_text', [$this, 'formatArgsAsText']),
                 new TwigFilter('file_excerpt', [$this, 'fileExcerpt'], ['is_safe' => ['html']]),
    @@ -95,22 +95,23 @@ public function formatArgs($args)
             $result = [];
             foreach ($args as $key => $item) {
                 if ('object' === $item[0]) {
    +                $item[1] = htmlspecialchars($item[1], \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
                     $parts = explode('\\', $item[1]);
                     $short = array_pop($parts);
                     $formattedValue = sprintf('<em>object</em>(<abbr title="%s">%s</abbr>)', $item[1], $short);
                 } elseif ('array' === $item[0]) {
    -                $formattedValue = sprintf('<em>array</em>(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]);
    +                $formattedValue = sprintf('<em>array</em>(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset));
                 } elseif ('null' === $item[0]) {
                     $formattedValue = '<em>null</em>';
                 } elseif ('boolean' === $item[0]) {
    -                $formattedValue = '<em>'.strtolower(var_export($item[1], true)).'</em>';
    +                $formattedValue = '<em>'.strtolower(htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset)).'</em>';
                 } elseif ('resource' === $item[0]) {
                     $formattedValue = '<em>resource</em>';
                 } else {
                     $formattedValue = str_replace("\n", '', htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset));
                 }
     
    -            $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue);
    +            $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", htmlspecialchars($key, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $formattedValue);
             }
     
             return implode(', ', $result);
    @@ -178,13 +179,17 @@ public function fileExcerpt($file, $line, $srcContext = 3)
         public function formatFile($file, $line, $text = null)
         {
             $file = trim($file);
    +        $line = (int) $line;
     
             if (null === $text) {
    -            $text = $file;
    -            if (null !== $rel = $this->getFileRelative($text)) {
    -                $rel = explode('/', $rel, 2);
    -                $text = sprintf('<abbr title="%s%2$s">%s</abbr>%s', $this->projectDir, $rel[0], '/'.($rel[1] ?? ''));
    +            if (null !== $rel = $this->getFileRelative($file)) {
    +                $rel = explode('/', htmlspecialchars($rel, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), 2);
    +                $text = sprintf('<abbr title="%s%2$s">%s</abbr>%s', htmlspecialchars($this->projectDir, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $rel[0], '/'.($rel[1] ?? ''));
    +            } else {
    +                $text = htmlspecialchars($file, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
                 }
    +        } else {
    +            $text = htmlspecialchars($text, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
             }
     
             if (0 < $line) {
    
  • src/Symfony/Bridge/Twig/Tests/Extension/CodeExtensionTest.php+119 21 modified
    @@ -14,6 +14,8 @@
     use PHPUnit\Framework\TestCase;
     use Symfony\Bridge\Twig\Extension\CodeExtension;
     use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
    +use Twig\Environment;
    +use Twig\Loader\ArrayLoader;
     
     class CodeExtensionTest extends TestCase
     {
    @@ -28,38 +30,123 @@ public function testFileRelative()
             $this->assertEquals('file.txt', $this->getExtension()->getFileRelative(\DIRECTORY_SEPARATOR.'project'.\DIRECTORY_SEPARATOR.'file.txt'));
         }
     
    -    /**
    -     * @dataProvider getClassNameProvider
    -     */
    -    public function testGettingClassAbbreviation($class, $abbr)
    +    public function testClassAbbreviationIntegration()
         {
    -        $this->assertEquals($this->getExtension()->abbrClass($class), $abbr);
    +        $data = [
    +            'fqcn' => 'F\Q\N\Foo',
    +            'xss' => '<script>',
    +        ];
    +
    +        $template = <<<'TWIG'
    +{{ 'Bare'|abbr_class }}
    +{{ fqcn|abbr_class }}
    +{{ xss|abbr_class }}
    +TWIG;
    +
    +        $expected = <<<'HTML'
    +<abbr title="Bare">Bare</abbr>
    +<abbr title="F\Q\N\Foo">Foo</abbr>
    +<abbr title="&lt;script&gt;">&lt;script&gt;</abbr>
    +HTML;
    +
    +        $this->assertEquals($expected, $this->render($template, $data));
         }
     
    -    /**
    -     * @dataProvider getMethodNameProvider
    -     */
    -    public function testGettingMethodAbbreviation($method, $abbr)
    +    public function testMethodAbbreviationIntegration()
         {
    -        $this->assertEquals($this->getExtension()->abbrMethod($method), $abbr);
    +        $data = [
    +            'fqcn' => 'F\Q\N\Foo::Method',
    +            'xss' => '<script>',
    +        ];
    +
    +        $template = <<<'TWIG'
    +{{ 'Bare::Method'|abbr_method }}
    +{{ fqcn|abbr_method }}
    +{{ 'Closure'|abbr_method }}
    +{{ 'Method'|abbr_method }}
    +{{ xss|abbr_method }}
    +TWIG;
    +
    +        $expected = <<<'HTML'
    +<abbr title="Bare">Bare</abbr>::Method()
    +<abbr title="F\Q\N\Foo">Foo</abbr>::Method()
    +<abbr title="Closure">Closure</abbr>
    +<abbr title="Method">Method</abbr>()
    +<abbr title="&lt;script&gt;">&lt;script&gt;</abbr>()
    +HTML;
    +
    +        $this->assertEquals($expected, $this->render($template, $data));
         }
     
    -    public function getClassNameProvider()
    +    public function testFormatArgsIntegration()
         {
    -        return [
    -            ['F\Q\N\Foo', '<abbr title="F\Q\N\Foo">Foo</abbr>'],
    -            ['Bare', '<abbr title="Bare">Bare</abbr>'],
    +        $data = [
    +            'args' => [
    +                ['object', 'Foo'],
    +                ['array', [['string', 'foo'], ['null']]],
    +                ['resource'],
    +                ['string', 'bar'],
    +                ['int', 123],
    +                ['bool', true],
    +            ],
    +            'xss' => [
    +                ['object', '<Foo>'],
    +                ['array', [['string', '<foo>']]],
    +                ['string', '<bar>'],
    +                ['int', 123],
    +                ['bool', true],
    +                ['<xss>', '<script>'],
    +            ],
             ];
    +
    +        $template = <<<'TWIG'
    +{{ args|format_args }}
    +{{ xss|format_args }}
    +{{ args|format_args_as_text }}
    +{{ xss|format_args_as_text }}
    +TWIG;
    +
    +        $expected = <<<'HTML'
    +<em>object</em>(<abbr title="Foo">Foo</abbr>), <em>array</em>('foo', <em>null</em>), <em>resource</em>, 'bar', 123, true
    +<em>object</em>(<abbr title="&lt;Foo&gt;">&lt;Foo&gt;</abbr>), <em>array</em>('&lt;foo&gt;'), '&lt;bar&gt;', 123, true, '&lt;script&gt;'
    +object(Foo), array(&#039;foo&#039;, null), resource, &#039;bar&#039;, 123, true
    +object(&amp;lt;Foo&amp;gt;), array(&#039;&amp;lt;foo&amp;gt;&#039;), &#039;&amp;lt;bar&amp;gt;&#039;, 123, true, &#039;&amp;lt;script&amp;gt;&#039;
    +HTML;
    +
    +        $this->assertEquals($expected, $this->render($template, $data));
    +    }
    +
    +
    +    public function testFormatFileIntegration()
    +    {
    +        $template = <<<'TWIG'
    +{{ 'foo/bar/baz.php'|format_file(21) }}
    +{{ '<script>'|format_file('<script21>') }}
    +TWIG;
    +
    +        $expected = <<<'HTML'
    +<a href="proto://foo/bar/baz.php#&amp;line=21" title="Click to open this file" class="file_link">foo/bar/baz.php at line 21</a>
    +<a href="proto://&lt;script&gt;#&amp;line=0" title="Click to open this file" class="file_link">&lt;script&gt;</a>
    +HTML;
    +
    +        $this->assertEquals($expected, $this->render($template));
         }
     
    -    public function getMethodNameProvider()
    +    public function testFormatFileFromTextIntegration()
         {
    -        return [
    -            ['F\Q\N\Foo::Method', '<abbr title="F\Q\N\Foo">Foo</abbr>::Method()'],
    -            ['Bare::Method', '<abbr title="Bare">Bare</abbr>::Method()'],
    -            ['Closure', '<abbr title="Closure">Closure</abbr>'],
    -            ['Method', '<abbr title="Method">Method</abbr>()'],
    -        ];
    +        $template = <<<'TWIG'
    +{{ 'in "foo/bar/baz.php" at line 21'|format_file_from_text }}
    +{{ 'in &quot;foo/bar/baz.php&quot; on line 21'|format_file_from_text }}
    +{{ 'in "<script>" on line 21'|format_file_from_text }}
    +TWIG;
    +
    +        $expected = <<<'HTML'
    +in <a href="proto://foo/bar/baz.php#&amp;line=21" title="Click to open this file" class="file_link">foo/bar/baz.php at line 21</a>
    +in <a href="proto://foo/bar/baz.php#&amp;line=21" title="Click to open this file" class="file_link">foo/bar/baz.php at line 21</a>
    +in <a href="proto://&lt;script&gt;#&amp;line=21" title="Click to open this file" class="file_link">&lt;script&gt; at line 21</a>
    +HTML;
    +
    +        $this->assertEquals($expected, $this->render($template));
         }
     
         public function testGetName()
    @@ -71,4 +158,15 @@ protected function getExtension()
         {
             return new CodeExtension(new FileLinkFormatter('proto://%f#&line=%l&'.substr(__FILE__, 0, 5).'>foobar'), \DIRECTORY_SEPARATOR.'project', 'UTF-8');
         }
    +
    +    private function render(string $template, array $context = [])
    +    {
    +        $twig = new Environment(
    +            new ArrayLoader(['index' => $template]),
    +            ['debug' => true]
    +        );
    +        $twig->addExtension($this->getExtension());
    +
    +        return $twig->render('index', $context);
    +    }
     }
    
9da9a145ce57

[TwigBridge] Ensure CodeExtension's filters properly escape their input

https://github.com/symfony/symfonyNicolas GrekasNov 3, 2023via ghsa
1 file changed · +14 9
  • src/Symfony/Bridge/Twig/Extension/CodeExtension.php+14 9 modified
    @@ -48,8 +48,8 @@ public function __construct($fileLinkFormat, string $projectDir, string $charset
         public function getFilters()
         {
             return [
    -            new TwigFilter('abbr_class', [$this, 'abbrClass'], ['is_safe' => ['html']]),
    -            new TwigFilter('abbr_method', [$this, 'abbrMethod'], ['is_safe' => ['html']]),
    +            new TwigFilter('abbr_class', [$this, 'abbrClass'], ['is_safe' => ['html'], 'pre_escape' => 'html']),
    +            new TwigFilter('abbr_method', [$this, 'abbrMethod'], ['is_safe' => ['html'], 'pre_escape' => 'html']),
                 new TwigFilter('format_args', [$this, 'formatArgs'], ['is_safe' => ['html']]),
                 new TwigFilter('format_args_as_text', [$this, 'formatArgsAsText']),
                 new TwigFilter('file_excerpt', [$this, 'fileExcerpt'], ['is_safe' => ['html']]),
    @@ -95,22 +95,23 @@ public function formatArgs($args)
             $result = [];
             foreach ($args as $key => $item) {
                 if ('object' === $item[0]) {
    +                $item[1] = htmlspecialchars($item[1], \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
                     $parts = explode('\\', $item[1]);
                     $short = array_pop($parts);
                     $formattedValue = sprintf('<em>object</em>(<abbr title="%s">%s</abbr>)', $item[1], $short);
                 } elseif ('array' === $item[0]) {
    -                $formattedValue = sprintf('<em>array</em>(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]);
    +                $formattedValue = sprintf('<em>array</em>(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset));
                 } elseif ('null' === $item[0]) {
                     $formattedValue = '<em>null</em>';
                 } elseif ('boolean' === $item[0]) {
    -                $formattedValue = '<em>'.strtolower(var_export($item[1], true)).'</em>';
    +                $formattedValue = '<em>'.strtolower(htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset)).'</em>';
                 } elseif ('resource' === $item[0]) {
                     $formattedValue = '<em>resource</em>';
                 } else {
                     $formattedValue = str_replace("\n", '', htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset));
                 }
     
    -            $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue);
    +            $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", htmlspecialchars($key, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $formattedValue);
             }
     
             return implode(', ', $result);
    @@ -178,13 +179,17 @@ public function fileExcerpt($file, $line, $srcContext = 3)
         public function formatFile($file, $line, $text = null)
         {
             $file = trim($file);
    +        $line = (int) $line;
     
             if (null === $text) {
    -            $text = $file;
    -            if (null !== $rel = $this->getFileRelative($text)) {
    -                $rel = explode('/', $rel, 2);
    -                $text = sprintf('<abbr title="%s%2$s">%s</abbr>%s', $this->projectDir, $rel[0], '/'.($rel[1] ?? ''));
    +            if (null !== $rel = $this->getFileRelative($file)) {
    +                $rel = explode('/', htmlspecialchars($rel, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), 2);
    +                $text = sprintf('<abbr title="%s%2$s">%s</abbr>%s', htmlspecialchars($this->projectDir, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $rel[0], '/'.($rel[1] ?? ''));
    +            } else {
    +                $text = htmlspecialchars($file, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
                 }
    +        } else {
    +            $text = htmlspecialchars($text, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);
             }
     
             if (0 < $line) {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.