CVE-2022-36108: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: TYPO3 is an open source PHP based web content management system released under the GNU GPL. It has been discovered that the `f:asset.css` view helper is vulnerable to cross-site scripting when user input is passed as variables to the CSS. Update to TYPO3 version 10.4.32 or 11.5.16 that fix the problem. There are no known workarounds for this issue.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2022-09-13T18:15Z
Repository: https://github.com/TYPO3/typo3
Language: PHP
Files changed: typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php, typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/CssViewHelperTest.php
Commit date: 2022-09-13 10:10:32 +0200
Commit message: [SECURITY] Encode child node variables in f:asset.css view helper

Variables in child nodes like `<f:asset.css>{value}</f:asset.css>`
were not encoded and allow cross-site scripting. In case values shall
be taken as is, corresponding `f:format.raw` instruction has to be used.

Resolves: #97900
Releases: main, 11.5, 10.4
Change-Id: Id843a41c42bbe1f74cdc4efbc117b24d20026b97
Security-Bulletin: TYPO3-CORE-SA-2022-010
Security-References: CVE-2022-36108
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/75719
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>

Diff: diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php
index 3de645353f40..320ac8412d1f 100644
--- a/typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php
+++ b/typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php
@@ -49,11 +49,11 @@ final class CssViewHelper extends AbstractTagBasedViewHelper
      * Rendered children string is passed as CSS code,
      * there is no point in HTML encoding anything from that.
      *
      * @var bool
      */
-    protected $escapeChildren = false;
+    protected $escapeChildren = true;
 
     protected AssetCollector $assetCollector;
 
     public function injectAssetCollector(AssetCollector $assetCollector): void
     {
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/CssViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/CssViewHelperTest.php
index be388d5a7ba9..0b2095c8df06 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/CssViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/CssViewHelperTest.php
@@ -16,10 +16,12 @@ declare(strict_types=1);
  */
 
 namespace TYPO3\CMS\Fluid\Tests\Functional\ViewHelpers\Asset;
 
 use TYPO3\CMS\Core\Page\AssetCollector;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Fluid\View\StandaloneView;
 use TYPO3\CMS\Fluid\ViewHelpers\Asset\CssViewHelper;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
 class CssViewHelperTest extends FunctionalTestCase
 {
@@ -77,6 +79,78 @@ class CssViewHelperTest extends FunctionalTestCase
         $viewHelper->initializeArgumentsAndRender();
         $collectedJavaScripts = $assetCollector->getStyleSheets();
         self::assertSame($collectedJavaScripts['test']['source'], 'my.css');
         self::assertSame($collectedJavaScripts['test']['attributes'], ['disabled' => 'disabled']);
     }
+
+    public static function childNodeRenderingIsCorrectDataProvider(): array
+    {
+        return [
+            // Double quotes
+            'variable with double quotes is encoded' => [
+                '</style>/* " ', // variable value
+                'body { color: #{color}; }', // inner template source
+                'body { color: #&lt;/style&gt;/* &quot; ; }', // expectation
+            ],
+            'variable with double quotes is encoded in single quotes' => [
+                '</style>/* " ', // variable value
+                'body { color: \'#{color}\'; }', // inner template source
+                'body { color: \'#&lt;/style&gt;/* &quot; \'; }', // expectation
+            ],
+            'variable with double quotes is encoded in double quotes' => [
+                '</style>/* " ', // variable value
+                'body { color: "#{color}"; }', // inner template source
+                'body { color: "#&lt;/style&gt;/* &quot; "; }', // expectation
+            ],
+            // Single quotes
+            'variable with single quotes is encoded' => [
+                '</style>/* \' ', // variable value
+                'body { color: #{color}; }', // inner template source
+                'body { color: #&lt;/style&gt;/* &#039; ; }', // expectation
+            ],
+            'variable with single quotes is encoded in single quotes' => [
+                '</style>/* \' ', // variable value
+                'body { color: \'#{color}\'; }', // inner template source
+                'body { color: \'#&lt;/style&gt;/* &#039; \'; }', // expectation
+            ],
+            'variable with single quotes is encoded in double quotes' => [
+                '</style>/* \' ', // variable value
+                'body { color: "#{color}"; }', // inner template source
+                'body { color: "#&lt;/style&gt;/* &#039; "; }', // expectation
+            ],
+            // Raw instruction
+            'raw instruction is passed' => [
+                '</style>/* " ',
+                'body { color: #{color -> f:format.raw()}; }',
+                'body { color: #</style>/* " ; }',
+            ],
+            'raw instruction is passed in sigle quotes' => [
+                '</style>/* " ',
+                'body { color: \'#{color -> f:format.raw()}\'; }',
+                'body { color: \'#</style>/* " \'; }',
+            ],
+            'raw instruction is passed in double quotes' => [
+                '</style>/* " ',
+                'body { color: "#{color -> f:format.raw()}"; }',
+                'body { color: "#</style>/* " "; }',
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider childNodeRenderingIsCorrectDataProvider
+     */
+    public function childNodeRenderingIsCorrect(string $value, string $source, string $expectation): void
+    {
+        $assetCollector = new AssetCollector();
+        GeneralUtility::setSingletonInstance(AssetCollector::class, $assetCollector);
+
+        $view = new StandaloneView();
+        $view->setTemplateSource(sprintf('<f:asset.css identifier="test">%s</f:asset.css>', $source));
+        $view->assign('color', $value);
+        $view->render();
+        GeneralUtility::removeSingletonInstance(AssetCollector::class, $assetCollector);
+
+        self::assertSame($expectation, $assetCollector->getInlineStyleSheets()['test']['source']);
+    }
 }

Vulnerable code:     protected $escapeChildren = false;
Fixed code:     protected $escapeChildren = true;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;

    public static function childNodeRenderingIsCorrectDataProvider(): array
    {
        return [
            // Double quotes
            'variable with double quotes is encoded' => [
                '</style>/* " ', // variable value
                'body { color: #{color}; }', // inner template source
                'body { color: #&lt;/style&gt;/* &quot; ; }', // expectation
            ],
            'variable with double quotes is encoded in single quotes' => [
                '</style>/* " ', // variable value
                'body { color: \'#{color}\'; }', // inner template source
                'body { color: \'#&lt;/style&gt;/* &quot; \'; }', // expectation
            ],
            'variable with double quotes is encoded in double quotes' => [
                '</style>/* " ', // variable value
                'body { color: "#{color}"; }', // inner template source
                'body { color: "#&lt;/style&gt;/* &quot; "; }', // expectation
            ],
            // Single quotes
            'variable with single quotes is encoded' => [
                '</style>/* \' ', // variable value
                'body { color: #{color}; }', // inner template source
                'body { color: #&lt;/style&gt;/* &#039; ; }', // expectation
            ],
            'variable with single quotes is encoded in single quotes' => [
                '</style>/* \' ', // variable value
                'body { color: \'#{color}\'; }', // inner template source
                'body { color: \'#&lt;/style&gt;/* &#039; \'; }', // expectation
            ],
            'variable with single quotes is encoded in double quotes' => [
                '</style>/* \' ', // variable value
                'body { color: "#{color}"; }', // inner template source
                'body { color: "#&lt;/style&gt;/* &#039; "; }', // expectation
            ],
            // Raw instruction
            'raw instruction is passed' => [
                '</style>/* " ',
                'body { color: #{color -> f:format.raw()}; }',
                'body { color: #</style>/* " ; }',
            ],
            'raw instruction is passed in sigle quotes' => [
                '</style>/* " ',
                'body { color: \'#{color -> f:format.raw()}\'; }',
                'body { color: \'#</style>/* " \'; }',
            ],
            'raw instruction is passed in double quotes' => [
                '</style>/* " ',
                'body { color: "#{color -> f:format.raw()}"; }',
                'body { color: "#</style>/* " "; }',
            ],
        ];
    }

    /**
     * @test
     * @dataProvider childNodeRenderingIsCorrectDataProvider
     */
    public function childNodeRenderingIsCorrect(string $value, string $source, string $expectation): void
    {
        $assetCollector = new AssetCollector();
        GeneralUtility::setSingletonInstance(AssetCollector::class, $assetCollector);

        $view = new StandaloneView();
        $view->setTemplateSource(sprintf('<f:asset.css identifier="test">%s</f:asset.css>', $source));
        $view->assign('color', $value);
        $view->render();
        GeneralUtility::removeSingletonInstance(AssetCollector::class, $assetCollector);

        self::assertSame($expectation, $assetCollector->getInlineStyleSheets()['test']['source']);
    }