Skip to content

Commit

Permalink
[SECURITY] Filter disallowed properties in form editor
Browse files Browse the repository at this point in the history
The form editor save and preview actions now check the submitted
form definition against configured possibilities within the form
editor setup.

Releases: master, 8.7
Resolves: #85044
Security-Commit: bcf5957567fe680866303c0758c37b26afb2c58f
Security-Bulletin: TYPO3-CORE-SA-2018-003
Change-Id: Id3d260681419b992553c98a9a408280094191c27
Reviewed-on: https://review.typo3.org/57548
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
  • Loading branch information
waldhacker1 authored and ohader committed Jul 12, 2018
1 parent 71ff714 commit bca913e
Show file tree
Hide file tree
Showing 45 changed files with 6,155 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
.. include:: ../../Includes.txt

===============================================================
Important: #85044 - Filter disallowed properties in form editor
===============================================================

See :issue:`85044`

Description
===========

The form editor save and preview actions now check the submitted form definition against configured possibilities within the form editor setup.

If a form element property is defined in the form editor setup then it means that the form element property can be written by the form editor.
A form element property can be written if the property path is defined within the following form editor properties:

* :yaml:`formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath`
* :yaml:`formElementsDefinition.<formElementType>.formEditor.editors.<index>.*.propertyPath`
* :yaml:`formElementsDefinition.<formElementType>.formEditor.editors.<index>.additionalElementPropertyPaths`
* :yaml:`formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.additionalElementPropertyPaths`

If a form editor property :yaml:`templateName` is "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
it means that the form editor property :yaml:`propertyPath` is interpreted as a so called "multiValueProperty".
A "multiValueProperty" can contain any subproperties relative to the value from :yaml:`propertyPath` which are valid.
If :yaml:`formElementsDefinition.<formElementType>.formEditor.editors.<index>.templateName` = "Inspector-PropertyGridEditor" and :yaml:`formElementsDefinition.<formElementType>.formEditor.editors.<index>.propertyPath` = "options.xxx"
then (for example) "options.xxx.yyy" is a valid property path to write.

If a form elements finisher|validator property is defined in the form editor setup then it means that the form elements finisher|validator property can be written by the form editor.
A form elements finisher|validator property can be written if the property path is defined within the following form editor properties:

* :yaml:`formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath`
* :yaml:`formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.*.propertyPath`

If a form elements finisher|validator property :yaml:`templateName` is "Inspector-PropertyGridEditor" or "Inspector-MultiSelectEditor" or "Inspector-ValidationErrorMessageEditor"
it means that the form editor property :yaml:`propertyPath` is interpreted as a so called "multiValueProperty".
A "multiValueProperty" can contain any subproperties relative to the value from :yaml:`propertyPath` which are valid.
If :yaml:`formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.templateName` = "Inspector-PropertyGridEditor"
and :yaml:`formElementsDefinition.<formElementType>.formEditor.propertyCollections.<finishers|validators>.<index>.editors.<index>.propertyPath` = "options.xxx"
that (for example) "options.xxx.yyy" is a valid property path to write.

If you use a custom form editor JavaScript "inspector editor" implementation (see https://docs.typo3.org/typo3cms/extensions/form/Concepts/FormEditor/Index.html#inspector)
which does not define the writable property paths by one of the above described inspector editor properties (e.g :yaml:`propertyPath`) within the form setup,
you must provide the writable property paths with a hook. Otherwise the editor will fail when saving.


Connect to the hook:

.. code-block:: yaml
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['buildFormDefinitionValidationConfiguration'][] = \Vendor\YourNamespace\YourClass::class;
Use the hook:

The hook must return an array with a set of ValidationDto objects.

.. code-block:: yaml
/**
* @param \TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto $validationDto
* @return array
*/
public function addAdditionalPropertyPaths(\TYPO3\CMS\Form\Domain\Configuration\FormDefinition\Validators\ValidationDto $validationDto): array
{
// Create a ValidationDto object for the form element type "Form" (:yaml:`formElementsDefinition.<formElementType>`).
$formValidationDto = $validationDto->withFormElementType('Form');
// Create a ValidationDto object for the finishers for the form element type "Form".
$formFinishersValidationDto = $formValidationDto->withPropertyCollectionName('finishers');
// Create a ValidationDto object for the form element type "Text" (:yaml:`formElementsDefinition.<formElementType>`).
$textValidationDto = $validationDto->withFormElementType('Text');
// Create a ValidationDto object for the validators for the form element type "Text".
$textValidatorsValidationDto = $textValidationDto->withPropertyCollectionName('validators');
// Create a ValidationDto object for the form element type "Date" (:yaml:`formElementsDefinition.<formElementType>`).
$dateValidationDto = $validationDto->withFormElementType('Date');
$propertyPaths = [
// Register the property :yaml:`renderingOptions.my.custom.property` for the form element type "Form".
// This property can now be written by the form editor.
$formValidationDto->withPropertyPath('renderingOptions.my.custom.property'),
// Register the property :yaml:`options.custom.property` for the finisher "MyCustomFinisher" for the form element type "Form".
// "MyCustomFinisher" must be equal to the identifier property from
// your custom inspector editor (:yaml:`formElementsDefinition.Form.formEditor.propertyCollections.finishers.<index>.editors.<index>.identifier`)
// This property can now be written by the form editor.
$formFinishersValidationDto->withPropertyCollectionElementIdentifier('MyCustomFinisher')->withPropertyPath('options.custom.property'),
// Register the properties :yaml:`properties.my.custom.property` and :yaml:`properties.my.other.custom.property` for the form element type "Text".
// This property can now be written by the form editor.
$textValidationDto->withPropertyPath('properties.my.custom.property'),
$textValidationDto->withPropertyPath('properties.my.other.custom.property'),
// Register the property :yaml:`options.custom.property` for the validator "CustomValidator" for the form element type "Text".
// "CustomValidator" must be equal to the identifier property from
// your custom inspector editor (:yaml:`formElementsDefinition.Text.formEditor.propertyCollections.validators.<index>.editors.<index>.identifier`)
// This property can now be written by the form editor.
$textValidatorsValidationDto->withPropertyCollectionElementIdentifier('CustomValidator')->withPropertyPath('options.custom.property'),
$textValidatorsValidationDto->withPropertyCollectionElementIdentifier('AnotherCustomValidator')->withPropertyPath('options.other.custom.property'),
$dateValidationDto->withPropertyPath('properties.custom.property'),
// ..
];
return $propertyPaths;
}
.. index:: Backend, ext:form
52 changes: 41 additions & 11 deletions typo3/sysext/form/Classes/Controller/FormEditorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
use TYPO3\CMS\Fluid\View\TemplateView;
use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService;
use TYPO3\CMS\Form\Domain\Exception\RenderingException;
use TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory;
use TYPO3\CMS\Form\Exception;
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
use TYPO3\CMS\Form\Service\TranslationService;
use TYPO3\CMS\Form\Type\FormDefinitionArray;
Expand Down Expand Up @@ -73,15 +75,21 @@ public function indexAction(string $formPersistenceIdentifier, string $prototype
throw new PersistenceManagerException('Edit a extension formDefinition is not allowed.', 1478265661);
}

$configurationService = $this->objectManager->get(ConfigurationService::class);
$formDefinition = $this->formPersistenceManager->load($formPersistenceIdentifier);
$formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
if (empty($prototypeName)) {
$prototypeName = isset($formDefinition['prototypeName']) ? $formDefinition['prototypeName'] : 'standard';

if ($prototypeName === null) {
$prototypeName = $formDefinition['prototypeName'] ?? 'standard';
} else {
// Loading a form definition with another prototype is currently not implemented but is planned in the future.
// This safety check is a preventive measure.
$selectablePrototypeNames = $configurationService->getSelectablePrototypeNamesDefinedInFormEditorSetup();
if (!in_array($prototypeName, $selectablePrototypeNames, true)) {
throw new Exception(sprintf('The prototype name "%s" is not configured within "formManager.selectablePrototypesConfiguration" ', $prototypeName), 1528625039);
}
}
$formDefinition['prototypeName'] = $prototypeName;
$formDefinition = $this->filterEmptyArrays($formDefinition);

$configurationService = $this->objectManager->get(ConfigurationService::class);
$formDefinition['prototypeName'] = $prototypeName;
$this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($prototypeName);

$formDefinition = $this->transformFormDefinitionForFormEditor($formDefinition);
Expand Down Expand Up @@ -154,7 +162,7 @@ public function initializeSaveFormAction()
public function saveFormAction(string $formPersistenceIdentifier, FormDefinitionArray $formDefinition)
{
$formDefinition = $formDefinition->getArrayCopy();
$formDefinition = $this->filterEmptyArrays($formDefinition);

if (
isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'])
&& is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['beforeFormSave'])
Expand All @@ -176,6 +184,10 @@ public function saveFormAction(string $formPersistenceIdentifier, FormDefinition

try {
$this->formPersistenceManager->save($formPersistenceIdentifier, $formDefinition);
$configurationService = $this->objectManager->get(ConfigurationService::class);
$this->prototypeConfiguration = $configurationService->getPrototypeConfiguration($formDefinition['prototypeName']);
$formDefinition = $this->transformFormDefinitionForFormEditor($formDefinition);
$response['formDefinition'] = $formDefinition;
} catch (PersistenceManagerException $e) {
$response = [
'status' => 'error',
Expand All @@ -184,8 +196,6 @@ public function saveFormAction(string $formPersistenceIdentifier, FormDefinition
];
}

$response['formDefinition'] = $formDefinition;

$this->view->assign('response', $response);
// saveFormAction uses the extbase JsonView::class.
// That's why we have to set the view variables in this way.
Expand All @@ -209,12 +219,14 @@ public function renderFormPageAction(FormDefinitionArray $formDefinition, int $p
if (empty($prototypeName)) {
$prototypeName = isset($formDefinition['prototypeName']) ? $formDefinition['prototypeName'] : 'standard';
}
$formDefinition = $formDefinition->getArrayCopy();

$formFactory = $this->objectManager->get(ArrayFormFactory::class);
$formDefinition = $formFactory->build($formDefinition->getArrayCopy(), $prototypeName);
$formDefinition = $formFactory->build($formDefinition, $prototypeName);
$formDefinition->setRenderingOption('previewMode', true);
$form = $formDefinition->bind($this->request, $this->response);
$form->overrideCurrentPage($pageIndex);

return $form->render();
}

Expand Down Expand Up @@ -431,6 +443,7 @@ protected function renderFormEditorTemplates(array $formEditorDefinitions): stri
}

/**
* @todo move this to FormDefinitionConversionService
* @param array $formDefinition
* @return array
*/
Expand All @@ -448,7 +461,16 @@ protected function transformFormDefinitionForFormEditor(array $formDefinition):
}
}

return $this->transformMultiValueElementsForFormEditor($formDefinition, $multiValueProperties);
$formDefinition = $this->filterEmptyArrays($formDefinition);

// @todo: replace with rte parsing
$formDefinition = ArrayUtility::stripTagsFromValuesRecursive($formDefinition);
$formDefinition = $this->transformMultiValueElementsForFormEditor($formDefinition, $multiValueProperties);

$formDefinitionConversionService = $this->getFormDefinitionConversionService();
$formDefinition = $formDefinitionConversionService->addHmacData($formDefinition);

return $formDefinition;
}

/**
Expand Down Expand Up @@ -546,6 +568,14 @@ protected function filterEmptyArrays(array $array): array
return $array;
}

/**
* @return FormDefinitionConversionService
*/
protected function getFormDefinitionConversionService(): FormDefinitionConversionService
{
return GeneralUtility::makeInstance(FormDefinitionConversionService::class);
}

/**
* Returns the current BE user.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Form\Domain\Configuration\ArrayProcessing;

/*
* 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!
*/

/**
* Helper for array processing
*
* Scope: frontend / backend
*/
class ArrayProcessing
{

/**
* @var string
*/
protected $identifier;

/**
* @var string
*/
protected $expression;

/**
* @var callable
*/
protected $processor;

/**
* @param string $identifier
* @param string $expression
* @param callable $processor
*/
public function __construct(string $identifier, string $expression, callable $processor)
{
$this->identifier = $identifier;
$this->expression = $expression;
$this->processor = $processor;
}

/**
* @return string
*/
public function getIdentifier(): string
{
return $this->identifier;
}

/**
* @return string
*/
public function getExpression(): string
{
return $this->expression;
}

/**
* @return callable
*/
public function getProcessor(): callable
{
return $this->processor;
}
}
Loading

0 comments on commit bca913e

Please sign in to comment.