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