Commit 7b5c2d29 authored by Oliver Hader's avatar Oliver Hader Committed by Benjamin Franzke
Browse files

[!!!][TASK] Deny inline JavaScript in FormEngine's requireJsModules

Custom FormEngine components loading additional RequireJS modules
must use corresponding JavaScriptModuleInstruction instances to
declare modules and optional method invocations.

Assigning scalar values to FormEngine node section 'requireJsModules'
(e.g. used for arbitrary inline JavaScript callbacks) is not possible
anymore and will throw a LogicException.

Resolves: #96221
Related: #95200
Releases: main
Change-Id: I8c487fa3914a46ae1ce2e75db0bf6a59756273d3
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72485


Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
parent 5547a732
......@@ -90,7 +90,7 @@ abstract class AbstractNode implements NodeInterface, LoggerAwareInterface
* is identical for *all* nodes. Parent will merge the return of a child with its
* own stuff and in itself return an array of the same structure.
*
* @return array
* @return array{string: list<string>, requireJsModules: list<\TYPO3\CMS\Core\Page\JavaScriptModuleInstruction>}
*/
protected function initializeResultArray(): array
{
......@@ -100,9 +100,7 @@ abstract class AbstractNode implements NodeInterface, LoggerAwareInterface
'additionalHiddenFields' => [],
'additionalInlineLanguageLabelFiles' => [],
'stylesheetFiles' => [],
// can hold strings or arrays,
// string = requireJS module,
// array = requireJS module + callback e.g. array('TYPO3/Foo/Bar', 'function() {}')
// holds list of `JavaScriptModuleInstruction` instances
'requireJsModules' => [],
'inlineData' => [],
'html' => '',
......
......@@ -75,7 +75,7 @@ class FormResultCompiler
* Array with requireJS modules, use module name as key, the value could be callback code.
* Use NULL as value if no callback is used.
*
* @var array
* @var list<JavaScriptModuleInstruction>
*/
protected $requireJsModules = [];
......@@ -97,40 +97,19 @@ class FormResultCompiler
foreach ($resultArray['additionalJavaScriptPost'] as $element) {
$this->additionalJavaScriptPost[] = $element;
}
if (!empty($resultArray['requireJsModules'])) {
foreach ($resultArray['requireJsModules'] as $module) {
if ($module instanceof JavaScriptModuleInstruction) {
$this->requireJsModules[] = $module;
continue;
}
// @deprecated Using requireJsModules via arrays is deprecated and will be removed in TYPO3 v12.0. Use JavaScriptModuleInstruction instead.
trigger_error('Using requireJsModules via arrays is deprecated and will be removed in TYPO3 v12.0. Use JavaScriptModuleInstruction instead.', E_USER_DEPRECATED);
$moduleName = null;
$callback = null;
if (is_string($module)) {
// if $module is a string, no callback
$moduleName = $module;
$callback = null;
} elseif (is_array($module)) {
// if $module is an array, callback is possible
$callback = reset($module);
$moduleName = key($module);
}
if ($moduleName !== null) {
if (!empty($this->requireJsModules[$moduleName]) && $callback !== null) {
$existingValue = $this->requireJsModules[$moduleName];
if (!is_array($existingValue)) {
$existingValue = [$existingValue];
}
$existingValue[] = $callback;
$this->requireJsModules[$moduleName] = $existingValue;
} else {
$this->requireJsModules[$moduleName] = $callback;
}
}
foreach ($resultArray['requireJsModules'] ?? [] as $module) {
if (!$module instanceof JavaScriptModuleInstruction) {
throw new \LogicException(
sprintf(
'Module must be a %s, type "%s" given',
JavaScriptModuleInstruction::class,
gettype($module)
),
1638264590
);
}
$this->requireJsModules[] = $module;
}
foreach ($resultArray['additionalHiddenFields'] as $element) {
$this->hiddenFieldAccum[] = $element;
}
......@@ -206,23 +185,8 @@ class FormResultCompiler
);
$this->requireJsModules[] = JavaScriptModuleInstruction::forRequireJS('TYPO3/CMS/Backend/FormEngineReview');
foreach ($this->requireJsModules as $moduleName => $callbacks) {
if ($callbacks instanceof JavaScriptModuleInstruction) {
$pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction($callbacks);
continue;
}
// legacy handling, in case callbacks for `TYPO3/CMS/Backend/FormEngine` were assigned already,
// previously the structure was like `[moduleName => [ callback-A, callback-B, ... ]]`
// @deprecated Using requireJsModules callbacks are deprecated and will be removed in TYPO3 v12.0. Use JavaScriptModuleInstruction instead.
trigger_error('Using requireJsModules callbacks are deprecated and will be removed in TYPO3 v12.0. Use JavaScriptModuleInstruction instead.', E_USER_DEPRECATED);
if (!is_array($callbacks)) {
$callbacks = [$callbacks];
}
foreach ($callbacks as $callback) {
$pageRenderer->loadRequireJsModule($moduleName, $callback);
}
foreach ($this->requireJsModules as $module) {
$pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction($module);
}
$pageRenderer->addJsFile('EXT:backend/Resources/Public/JavaScript/jsfunc.tbe_editor.js');
......
......@@ -42,7 +42,7 @@ render-types, ...) that provide inline JavaScript using `fieldChangeFunc`.
Migration
=========
:ref:`Previews deprecation ChangeLog documentation <Deprecation-91787-DeprecateInlineJavaScriptInFieldChangeFunc>`
:ref:`Previous deprecation ChangeLog documentation <Deprecation-91787-DeprecateInlineJavaScriptInFieldChangeFunc>`
provided migration details already. A complete and installable example is available with
`ext:demo_91787 <https://github.com/ohader/demo_91787>`__ as well.
......
.. include:: ../../Includes.txt
==========================================================================
Breaking: #96221 - Deny inline JavaScript in FormEngine's requireJsModules
==========================================================================
See :issue:`96221`
Description
===========
Custom :php:`FormEngine` components allowed to load RequireJS modules
with arbitrary inline JavaScript to initialize those modules. In favor
of introducing content security policy headers, the amount of inline
JavaScript shall be reduced and replaced by corresponding declarations.
Using callback functions as inline JavaScript is not possible anymore,
initializations have to be declared using an instance of
:php:`TYPO3\CMS\Core\Page\JavaScriptModuleInstruction`.
Impact
======
Using inline JavaScript to initialize RequireJS modules in `FormEngine`,
like shown in the the example below, will throw a corresponding
:php:`\LogicException`.
.. code-block:: php
$resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/Element/InputDateTimeElement' => '
// inline JavaScript code to initialize `InputDateTimeElement`
function(InputDateTimeElement) {
new InputDateTimeElement(' . GeneralUtility::quoteJSvalue($fieldId) . ');
}'
];
Affected Installations
======================
All instances that are using RequireJS modules with custom initializations
as inline JavaScript in `FormEngine`.
Migration
=========
:ref:`Previous deprecation ChangeLog documentation <Deprecation-95200-DeprecateRequireJSCallbacksAsInlineJavaScript>`
provided migration details already.
The following snippet shows the migrated source code of shown above - using
:php:`TYPO3\CMS\Core\Page\JavaScriptModuleInstruction` instead of inline JavaScript.
.. code-block:: php
// use use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
$resultArray['requireJsModules'][] = JavaScriptModuleInstruction::forRequireJS(
'TYPO3/CMS/Backend/FormEngine/Element/InputDateTimeElement'
)->instance($fieldId);
:php:`JavaScriptModuleInstruction` forwards arguments as `JSON` data - and thus
handles proper context-aware encoding implicitly (:php:`GeneralUtility::quoteJSvalue`
and similar custom encoding can be omitted in this case).
.. index:: Backend, JavaScript, NotScanned, ext:backend
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment