From fb74f1d6687c08beb61d4301d11c20dd4bc299cb Mon Sep 17 00:00:00 2001
From: waldhacker <hello@waldhacker.dev>
Date: Tue, 13 Dec 2022 10:20:13 +0100
Subject: [PATCH] [SECURITY] Prohibit TypoScript in form yaml files

Only evaluate TypoScript-like instructions like

```
submitButtonLabel = TEXT
submitButtonLabel.value = Bar
```

defined within

`plugin.tx_form.settings.formDefinitionOverrides`
and
`plugin.tx_form.settings.yamlSettingsOverrides`

and **not** within form definition yaml files or
the form setup yaml files.

This is achieved by not searching the entire form
definition or form setup for TypoScript instructions,
but only the actual TypoScript.

Resolves: #98403
Releases: main, 11.5, 10.4
Change-Id: I7b066f109d6061715c2240b01ed15185c58fa9f5
Security-Bulletin: TYPO3-CORE-SA-2022-015
Security-References: CVE-2022-23503
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77092
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
---
 .../Controller/FormFrontendController.php     |   7 +-
 .../Configuration/ConfigurationManager.php    |  15 +-
 .../Controller/FormFrontendControllerTest.php |  95 ++++++++++++
 .../ConfigurationManagerTest.php              | 145 ++++++++++++++++++
 4 files changed, 252 insertions(+), 10 deletions(-)
 create mode 100644 typo3/sysext/form/Tests/Unit/Mvc/Configuration/ConfigurationManagerTest.php

diff --git a/typo3/sysext/form/Classes/Controller/FormFrontendController.php b/typo3/sysext/form/Classes/Controller/FormFrontendController.php
index 5bf22e77d6da..18a11fadd0b4 100644
--- a/typo3/sysext/form/Classes/Controller/FormFrontendController.php
+++ b/typo3/sysext/form/Classes/Controller/FormFrontendController.php
@@ -158,12 +158,13 @@ class FormFrontendController extends ActionController
             isset($this->settings['formDefinitionOverrides'][$formDefinition['identifier']])
             && !empty($this->settings['formDefinitionOverrides'][$formDefinition['identifier']])
         ) {
+            $formDefinitionOverrides = GeneralUtility::makeInstance(TypoScriptService::class)
+                ->resolvePossibleTypoScriptConfiguration($this->settings['formDefinitionOverrides'][$formDefinition['identifier']]);
+
             ArrayUtility::mergeRecursiveWithOverrule(
                 $formDefinition,
-                $this->settings['formDefinitionOverrides'][$formDefinition['identifier']]
+                $formDefinitionOverrides
             );
-            $formDefinition = GeneralUtility::makeInstance(TypoScriptService::class)
-                ->resolvePossibleTypoScriptConfiguration($formDefinition);
         }
         return $formDefinition;
     }
diff --git a/typo3/sysext/form/Classes/Mvc/Configuration/ConfigurationManager.php b/typo3/sysext/form/Classes/Mvc/Configuration/ConfigurationManager.php
index 20a0a6fd0687..4eab55ee23c2 100644
--- a/typo3/sysext/form/Classes/Mvc/Configuration/ConfigurationManager.php
+++ b/typo3/sysext/form/Classes/Mvc/Configuration/ConfigurationManager.php
@@ -133,17 +133,18 @@ class ConfigurationManager extends ExtbaseConfigurationManager implements Config
     {
         $typoScript = parent::getConfiguration(self::CONFIGURATION_TYPE_SETTINGS, $extensionName);
         if (is_array($typoScript['yamlSettingsOverrides'] ?? null) && !empty($typoScript['yamlSettingsOverrides'])) {
-            ArrayUtility::mergeRecursiveWithOverrule(
-                $yamlSettings,
-                $typoScript['yamlSettingsOverrides']
-            );
-
+            $yamlSettingsOverrides = $typoScript['yamlSettingsOverrides'];
             if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
                 && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
             ) {
-                $yamlSettings = GeneralUtility::makeInstance(TypoScriptService::class)
-                    ->resolvePossibleTypoScriptConfiguration($yamlSettings);
+                $yamlSettingsOverrides = GeneralUtility::makeInstance(TypoScriptService::class)
+                    ->resolvePossibleTypoScriptConfiguration($yamlSettingsOverrides);
             }
+
+            ArrayUtility::mergeRecursiveWithOverrule(
+                $yamlSettings,
+                $yamlSettingsOverrides
+            );
         }
         return $yamlSettings;
     }
diff --git a/typo3/sysext/form/Tests/Unit/Controller/FormFrontendControllerTest.php b/typo3/sysext/form/Tests/Unit/Controller/FormFrontendControllerTest.php
index 58fe0a34b4a9..5386ae10226b 100644
--- a/typo3/sysext/form/Tests/Unit/Controller/FormFrontendControllerTest.php
+++ b/typo3/sysext/form/Tests/Unit/Controller/FormFrontendControllerTest.php
@@ -594,4 +594,99 @@ class FormFrontendControllerTest extends UnitTestCase
 
         self::assertSame($expected, $mockController->_call('overrideByTypoScriptSettings', $input));
     }
+
+    /**
+     * @test
+     */
+    public function overrideByTypoScriptSettingsDoesNotEvaluateTypoScriptLookalikeInstructionsFromYamlSettings(): void
+    {
+        $formDefinitionYaml = [
+            'identifier' => 'ext-form-identifier',
+            'prototypeName' => 'standard',
+            'label' => [
+                'value' => 'Label override',
+                '_typoScriptNodeValue' => 'TEXT',
+            ],
+            'renderables' => [
+                0 => [
+                    'identifier' => 'page-1',
+                    'type' => 'Page',
+                    'label' => 'Label',
+                    'renderables' => [
+                        0 => [
+                            'identifier' => 'text-1',
+                            'type' => 'Text',
+                            'label' => 'Label',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $formTypoScript = [
+            'formDefinitionOverrides' => [
+                'ext-form-identifier' => [
+                    'renderables' => [
+                        0 => [
+                            'renderables' => [
+                                0 => [
+                                    'label' => [
+                                        'value' => 'Label override',
+                                        '_typoScriptNodeValue' => 'TEXT',
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $evaluatedFormTypoScript = [
+            'renderables' => [
+                0 => [
+                    'renderables' => [
+                        0 => [
+                            'label' => 'Label override',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $expected = [
+            'identifier' => 'ext-form-identifier',
+            'prototypeName' => 'standard',
+            'label' => [
+                'value' => 'Label override',
+                '_typoScriptNodeValue' => 'TEXT',
+            ],
+            'renderables' => [
+                0 => [
+                    'identifier' => 'page-1',
+                    'type' => 'Page',
+                    'label' => 'Label',
+                    'renderables' => [
+                        0 => [
+                            'identifier' => 'text-1',
+                            'type' => 'Text',
+                            'label' =>  'Label override',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $controllerMock = $this->getAccessibleMock(FormFrontendController::class, [
+            'dummy',
+        ], [], '', false);
+
+        $typoScriptServiceProphecy = $this->prophesize(TypoScriptService::class);
+        $typoScriptServiceProphecy->resolvePossibleTypoScriptConfiguration($formTypoScript['formDefinitionOverrides']['ext-form-identifier'])->willReturn($evaluatedFormTypoScript);
+        GeneralUtility::addInstance(TypoScriptService::class, $typoScriptServiceProphecy->reveal());
+
+        $controllerMock->_set('settings', $formTypoScript);
+
+        self::assertSame($expected, $controllerMock->_call('overrideByTypoScriptSettings', $formDefinitionYaml));
+    }
 }
diff --git a/typo3/sysext/form/Tests/Unit/Mvc/Configuration/ConfigurationManagerTest.php b/typo3/sysext/form/Tests/Unit/Mvc/Configuration/ConfigurationManagerTest.php
new file mode 100644
index 000000000000..2623b673cb3e
--- /dev/null
+++ b/typo3/sysext/form/Tests/Unit/Mvc/Configuration/ConfigurationManagerTest.php
@@ -0,0 +1,145 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Form\Tests\Unit\Mvc\Configuration;
+
+use Prophecy\PhpUnit\ProphecyTrait;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Configuration\FrontendConfigurationManager;
+use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManager;
+use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
+use TYPO3\CMS\Form\Mvc\Configuration\TypoScriptService;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class ConfigurationManagerTest extends UnitTestCase
+{
+    use ProphecyTrait;
+
+    /**
+     * @var bool
+     */
+    protected $resetSingletonInstances = true;
+
+    public function tearDown(): void
+    {
+        unset($GLOBALS['TYPO3_REQUEST']);
+        parent::tearDown();
+    }
+
+    /**
+     * @test
+     */
+    public function getConfigurationDoesNotEvaluateTypoScriptLookalikeInstructionsFromYamlSettingsInFrontendContext(): void
+    {
+        $yamlSettings = [
+            'prototypes' => [
+                'standard' => [
+                    'formElementsDefinition' => [
+                        'Form' => [
+                            'renderingOptions' => [
+                                'submitButtonLabel' => 'Foo',
+                                'templateVariant' => 'version1',
+                                'addQueryString' => [
+                                    'value' => 'Baz',
+                                    '_typoScriptNodeValue' => 'TEXT',
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $formTypoScript = [
+            'settings' => [
+                'yamlSettingsOverrides' => [
+                    'prototypes' => [
+                        'standard' => [
+                            'formElementsDefinition' => [
+                                'Form' => [
+                                    'renderingOptions' => [
+                                        'submitButtonLabel' => [
+                                            'value' => 'Bar',
+                                            '_typoScriptNodeValue' => 'TEXT',
+                                        ],
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $evaluatedFormTypoScript = [
+            'prototypes' => [
+                'standard' => [
+                    'formElementsDefinition' => [
+                        'Form' => [
+                            'renderingOptions' => [
+                                'submitButtonLabel' => 'Bar',
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $expected = [
+            'prototypes' => [
+                'standard' => [
+                    'formElementsDefinition' => [
+                        'Form' => [
+                            'renderingOptions' => [
+                                'submitButtonLabel' => 'Bar',
+                                'templateVariant' => 'version1',
+                                'addQueryString' => [
+                                    'value' => 'Baz',
+                                    '_typoScriptNodeValue' => 'TEXT',
+                                ],
+                            ],
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        $configurationManagerMock = $this->getAccessibleMock(ConfigurationManager::class, [
+            'getTypoScriptSettings',
+            'getYamlSettingsFromCache',
+        ], [], '', false);
+        $configurationManagerMock->method('getYamlSettingsFromCache')->with(self::anything())->willReturn($yamlSettings);
+
+        $serverRequestProphecy = $this->prophesize(ServerRequestInterface::class);
+        $serverRequestProphecy->getAttribute('applicationType')->willReturn(SystemEnvironmentBuilder::REQUESTTYPE_FE);
+        $GLOBALS['TYPO3_REQUEST'] = $serverRequestProphecy->reveal();
+
+        $typoScriptServiceProphecy = $this->prophesize(TypoScriptService::class);
+        $typoScriptServiceProphecy->resolvePossibleTypoScriptConfiguration($formTypoScript['settings']['yamlSettingsOverrides'])->willReturn($evaluatedFormTypoScript);
+        GeneralUtility::addInstance(TypoScriptService::class, $typoScriptServiceProphecy->reveal());
+
+        $concreteConfigurationManagerProphecy = $this->prophesize(FrontendConfigurationManager::class);
+        $concreteConfigurationManagerProphecy->getConfiguration('form', null)->willReturn($formTypoScript);
+
+        $configurationManagerMock->_set('concreteConfigurationManager', $concreteConfigurationManagerProphecy->reveal());
+        $configurationManagerMock->method('getTypoScriptSettings')->with(self::anything())->willReturn([]);
+
+        self::assertSame($expected, $configurationManagerMock->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_YAML_SETTINGS, 'form'));
+    }
+}
-- 
GitLab