From 4f3bdadd0269b27f20b4fca224156d8584d6ecfd Mon Sep 17 00:00:00 2001 From: Helmut Hummel <typo3@helhum.io> Date: Sat, 25 Feb 2017 20:32:39 +0100 Subject: [PATCH] [BUGFIX] Respect IRRE parent config in Ajax calls The code to transfer the inline parent context to form engine in Ajax requests exists but is currently non functional in some situations. The config is stored as array, which is hashed by serializing the array, and building the hash on that string. However that string is not transferred over the wire, but the json encoded array. If a float value was present at some place in this array, json_encode and json_decode will add a slight offset to these numbers than if the value is serialized. To avoid such errors, the hmac is now calculated and checked against the json encoded value. We also clean up the code in this area to avoid duplication and improve the hash calculation and comparison. By doing so, we can clean up and simplify the flex form handling for IRRE fields as well. Resolves: #79999 Releases: master Change-Id: I049d699f9f30edad0a9c8b06bbc3970e2cdac417 Reviewed-on: https://review.typo3.org/51783 Tested-by: TYPO3com <no-reply@typo3.com> Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch> Tested-by: Christian Kuhn <lolli@schwarzbu.ch> Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de> Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de> --- .../Controller/FormInlineAjaxController.php | 64 ++++++++++--------- .../Form/Container/InlineControlContainer.php | 4 +- .../Classes/Form/InlineStackProcessor.php | 15 ++--- 3 files changed, 39 insertions(+), 44 deletions(-) diff --git a/typo3/sysext/backend/Classes/Controller/FormInlineAjaxController.php b/typo3/sysext/backend/Classes/Controller/FormInlineAjaxController.php index df048ed1bbe4..3eb58a97a2ef 100644 --- a/typo3/sysext/backend/Classes/Controller/FormInlineAjaxController.php +++ b/typo3/sysext/backend/Classes/Controller/FormInlineAjaxController.php @@ -45,6 +45,7 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController public function createAction(ServerRequestInterface $request, ResponseInterface $response) { $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax']; + $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); $domObjectId = $ajaxArguments[0]; $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); @@ -57,7 +58,7 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController /** @var InlineStackProcessor $inlineStackProcessor */ $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId); - $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']); + $inlineStackProcessor->injectAjaxConfiguration($parentConfig); $inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0); // Parent, this table embeds the child table @@ -77,14 +78,11 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController $vanillaUid = (int)$inlineFirstPid; } - $flexDataStructureIdentifier = $this->getFlexFormDataStructureIdentifierFromAjaxContext($ajaxArguments); - $processedTca = []; - if ($flexDataStructureIdentifier) { - $processedTca = $GLOBALS['TCA'][$parent['table']]; + $processedTca = $GLOBALS['TCA'][$parent['table']]; + $processedTca['columns'][$parentFieldName]['config'] = $parentConfig; + if (!empty($parentConfig['dataStructureIdentifier'])) { $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class); - $dataStructure = $flexFormTools->parseDataStructureByIdentifier($flexDataStructureIdentifier); - $processedTca['columns'][$parentFieldName]['config']['dataStructureIdentifier'] = $flexDataStructureIdentifier; - $processedTca['columns'][$parentFieldName]['config']['ds'] = $dataStructure; + $processedTca['columns'][$parentFieldName]['config']['ds'] = $flexFormTools->parseDataStructureByIdentifier($parentConfig['dataStructureIdentifier']); } $formDataCompilerInputForParent = [ @@ -242,12 +240,13 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController $domObjectId = $ajaxArguments[0]; $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); + $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); // Parse the DOM identifier, add the levels to the structure stack /** @var InlineStackProcessor $inlineStackProcessor */ $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId); - $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']); + $inlineStackProcessor->injectAjaxConfiguration($parentConfig); // Parent, this table embeds the child table $parent = $inlineStackProcessor->getStructureLevel(-1); @@ -258,14 +257,11 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController 'uid' => (int)$parent['uid'], ]; - $flexDataStructureIdentifier = $this->getFlexFormDataStructureIdentifierFromAjaxContext($ajaxArguments); - $processedTca = []; - if ($flexDataStructureIdentifier) { - $processedTca = $GLOBALS['TCA'][$parent['table']]; + $processedTca = $GLOBALS['TCA'][$parent['table']]; + $processedTca['columns'][$parentFieldName]['config'] = $parentConfig; + if (!empty($parentConfig['dataStructureIdentifier'])) { $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class); - $dataStructure = $flexFormTools->parseDataStructureByIdentifier($flexDataStructureIdentifier); - $processedTca['columns'][$parentFieldName]['config']['dataStructureIdentifier'] = $flexDataStructureIdentifier; - $processedTca['columns'][$parentFieldName]['config']['ds'] = $dataStructure; + $processedTca['columns'][$parentFieldName]['config']['ds'] = $flexFormTools->parseDataStructureByIdentifier($parentConfig['dataStructureIdentifier']); } $formDataCompilerInputForParent = [ @@ -350,12 +346,13 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController $ajaxArguments = isset($request->getParsedBody()['ajax']) ? $request->getParsedBody()['ajax'] : $request->getQueryParams()['ajax']; $domObjectId = $ajaxArguments[0]; $type = $ajaxArguments[1]; + $parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']); /** @var InlineStackProcessor $inlineStackProcessor */ $inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class); // Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config: $inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId); - $inlineStackProcessor->injectAjaxConfiguration($ajaxArguments['context']); + $inlineStackProcessor->injectAjaxConfiguration($parentConfig); $inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId); $jsonArray = false; @@ -364,6 +361,9 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController $parent = $inlineStackProcessor->getStructureLevel(-1); $parentFieldName = $parent['field']; + $processedTca = $GLOBALS['TCA'][$parent['table']]; + $processedTca['columns'][$parentFieldName]['config'] = $parentConfig; + // Child, a record from this table should be rendered $child = $inlineStackProcessor->getUnstableStructure(); @@ -375,6 +375,7 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController // TcaInlineExpandCollapseState needs this 'uid' => (int)$parent['uid'], ], + 'processedTca' => $processedTca, 'inlineFirstPid' => $inlineFirstPid, 'columnsToProcess' => [ $parentFieldName @@ -919,25 +920,26 @@ class FormInlineAjaxController extends AbstractFormEngineAjaxController } /** - * Inline fields within a flex form need the data structure identifier that - * specifies the specific flex form this inline element is in. Retrieve it from - * the context array. + * Validates the config that is transferred over the wire to provide the + * correct TCA config for the parent table * - * @param array $ajaxArguments The AJAX request arguments - * @return string Data structure identifier as json string + * @param string $contextString + * @return array + * @todo: Review this construct - Why can't the ajax call fetch these data on its own and transfers it to client instead? */ - protected function getFlexFormDataStructureIdentifierFromAjaxContext(array $ajaxArguments) + protected function extractSignedParentConfigFromRequest(string $contextString): array { - if (!isset($ajaxArguments['context'])) { - return ''; + if ($contextString === '') { + return []; } - - $context = json_decode($ajaxArguments['context'], true); - if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) { - return ''; + $context = json_decode($contextString, true); + if (empty($context['config'])) { + return []; } - - return $context['config']['flexDataStructureIdentifier'] ?? ''; + if (!\hash_equals(GeneralUtility::hmac(json_encode($context['config']), 'InlineContext'), $context['hmac'])) { + return []; + } + return $context['config']; } /** diff --git a/typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php b/typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php index 53a958a67d01..5149db79e145 100644 --- a/typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php +++ b/typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php @@ -134,7 +134,7 @@ class InlineControlContainer extends AbstractContainer if (!empty($newStructureItem['flexform']) && isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier']) ) { - $config['flexDataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier']; + $config['dataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier']; } // e.g. data[<table>][<uid>][<field>] @@ -169,7 +169,7 @@ class InlineControlContainer extends AbstractContainer ], 'context' => [ 'config' => $config, - 'hmac' => GeneralUtility::hmac(serialize($config)), + 'hmac' => GeneralUtility::hmac(json_encode($config), 'InlineContext'), ], ]; $this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack']; diff --git a/typo3/sysext/backend/Classes/Form/InlineStackProcessor.php b/typo3/sysext/backend/Classes/Form/InlineStackProcessor.php index be7cb0f41d70..95a68d6aacd9 100644 --- a/typo3/sysext/backend/Classes/Form/InlineStackProcessor.php +++ b/typo3/sysext/backend/Classes/Form/InlineStackProcessor.php @@ -108,26 +108,19 @@ class InlineStackProcessor /** * Injects configuration via AJAX calls. * This is used by inline ajax calls that transfer configuration options back to the stack for initialization - * The configuration is validated using HMAC to avoid hijacking. * - * @param string $contextString Given context string from ajax call + * @param array $config Given config extracted from ajax call * @return void * @todo: Review this construct - Why can't the ajax call fetch these data on its own and transfers it to client instead? */ - public function injectAjaxConfiguration($contextString = '') + public function injectAjaxConfiguration(array $config) { $level = $this->calculateStructureLevel(-1); - if (empty($contextString) || $level === false) { + if (empty($config) || $level === false) { return; } $current = &$this->inlineStructure['stable'][$level]; - $context = json_decode($contextString, true); - if (GeneralUtility::hmac(serialize($context['config'])) !== $context['hmac']) { - return; - } - // Remove the data structure pointers, only relevant for the FormInlineAjaxController - unset($context['flexDataStructurePointers']); - $current['config'] = $context['config']; + $current['config'] = $config; $current['localizationMode'] = BackendUtility::getInlineLocalizationMode( $current['table'], $current['config'] -- GitLab