Skip to content
Snippets Groups Projects
Commit 18e3f4f7 authored by Oliver Hader's avatar Oliver Hader Committed by Andreas Fernandez
Browse files

[BUGFIX] Allow arbitrary objects in widget context

TYPO3-CORE-SA-2020-005 caused side-effects on Fluid AJAX widgets which
unfortunatelly support any class instance to be temporarily stored in
the current user-session. With mentioned change to address an insecure
deserialization vulnerability it was limited to items that could be
JSON-serialized.

This limitation is removed again by switching back to `unserialize()`,
but using an encryption-key-based HMAC signature on the payload.
Due to its architecture there is no better approach available.

This partially reverts commit e4fb92a8.

Resolves: #91382
Releases: master, 9.5
Change-Id: I68cbd15e7df2f536180f174fa63cf27f8a19cfcd
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64501


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: default avatarJonas Götze <jonnsn@gmail.com>
Tested-by: default avatarAlexander Schnitzler <git@alexanderschnitzler.de>
Tested-by: default avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: default avatarSusanne Moog <look@susi.dev>
Reviewed-by: default avatarAlexander Schnitzler <git@alexanderschnitzler.de>
Reviewed-by: default avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: default avatarSusanne Moog <look@susi.dev>
parent 794c2286
Branches
Tags
No related merge requests found
......@@ -16,6 +16,8 @@
namespace TYPO3\CMS\Fluid\Core\Widget;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Security\Cryptography\HashService;
use TYPO3\CMS\Fluid\Core\Widget\Exception\WidgetContextNotFoundException;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
......@@ -41,11 +43,17 @@ class AjaxWidgetContextHolder implements SingletonInterface
*/
protected $widgetContextsStorageKey = 'TYPO3\\CMS\\Fluid\\Core\\Widget\\AjaxWidgetContextHolder_widgetContexts';
/**
* @var HashService
*/
protected $hashService;
/**
* Constructor
*/
public function __construct()
{
$this->hashService = GeneralUtility::makeInstance(HashService::class);
$this->loadWidgetContexts();
}
......@@ -55,13 +63,9 @@ class AjaxWidgetContextHolder implements SingletonInterface
protected function loadWidgetContexts()
{
if (isset($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
$this->widgetContexts = $this->buildWidgetContextsFromArray(
json_decode($GLOBALS['TSFE']->fe_user->getKey('ses', $this->widgetContextsStorageKey ?? null), true) ?? []
);
$this->widgetContexts = $this->unserializeWithHmac($GLOBALS['TSFE']->fe_user->getKey('ses', $this->widgetContextsStorageKey));
} else {
$this->widgetContexts = $this->buildWidgetContextsFromArray(
json_decode($GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey] ?? '', true) ?? []
);
$this->widgetContexts = isset($GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey]) ? $this->unserializeWithHmac($GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey]) : [];
$GLOBALS['BE_USER']->writeUC();
}
}
......@@ -100,27 +104,33 @@ class AjaxWidgetContextHolder implements SingletonInterface
protected function storeWidgetContexts()
{
if (isset($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
$GLOBALS['TSFE']->fe_user->setKey('ses', $this->widgetContextsStorageKey, json_encode($this->widgetContexts));
$GLOBALS['TSFE']->fe_user->setKey('ses', $this->widgetContextsStorageKey, $this->serializeWithHmac($this->widgetContexts));
$GLOBALS['TSFE']->fe_user->storeSessionData();
} else {
$GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey] = json_encode($this->widgetContexts);
$GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey] = $this->serializeWithHmac($this->widgetContexts);
$GLOBALS['BE_USER']->writeUC();
}
}
/**
* Builds WidgetContext instances from JSON representation,
* this is basically required for AJAX widgets only.
*
* @param array $data
* @return WidgetContext[]
*/
protected function buildWidgetContextsFromArray(array $data): array
protected function serializeWithHmac(array $widgetContexts): string
{
$widgetContexts = [];
foreach ($data as $widgetId => $widgetContextData) {
$widgetContexts[$widgetId] = WidgetContext::fromArray($widgetContextData);
$data = serialize($widgetContexts);
return $this->hashService->appendHmac($data);
}
protected function unserializeWithHmac(?string $data): array
{
if ($data === null || $data === '') {
return [];
}
try {
$data = $this->hashService->validateAndStripHmac($data);
// widget contexts literally can contain everything, that why using
// HMAC-signed `unserialize()` is the only option here unless Extbase
// structures have been refactored further
$widgetContexts = unserialize($data);
} finally {
return is_array($widgetContexts ?? null) ? $widgetContexts : [];
}
return $widgetContexts;
}
}
......@@ -29,19 +29,8 @@ use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
*
* @internal It is a purely internal class which should not be used outside of Fluid.
*/
class WidgetContext implements \JsonSerializable
class WidgetContext
{
protected const JSON_SERIALIZABLE_PROPERTIES = [
'widgetIdentifier',
'ajaxWidgetIdentifier',
'widgetConfiguration',
'controllerObjectName',
'parentPluginNamespace',
'parentExtensionName',
'parentPluginName',
'widgetViewHelperClassName',
];
/**
* Uniquely identifies a Widget Instance on a certain page.
*
......@@ -58,8 +47,7 @@ class WidgetContext implements \JsonSerializable
/**
* User-supplied widget configuration, available inside the widget
* controller as $this->widgetConfiguration. Array items must be
* null, scalar, array or implement \JsonSerializable interface.
* controller as $this->widgetConfiguration.
*
* @var array
*/
......@@ -109,51 +97,6 @@ class WidgetContext implements \JsonSerializable
*/
protected $widgetViewHelperClassName;
public static function fromArray(array $data): WidgetContext
{
$target = new WidgetContext();
foreach ($data as $propertyName => $propertyValue) {
if (!in_array($propertyName, self::JSON_SERIALIZABLE_PROPERTIES, true)) {
continue;
}
if (!property_exists($target, $propertyName)) {
continue;
}
$target->{$propertyName} = $propertyValue;
}
return $target;
}
public function jsonSerialize()
{
$properties = array_fill_keys(self::JSON_SERIALIZABLE_PROPERTIES, true);
$data = array_intersect_key(get_object_vars($this), $properties);
if (is_array($data['widgetConfiguration'] ?? null)) {
$data['widgetConfiguration'] = array_filter(
$data['widgetConfiguration'],
function ($value): bool {
if ($value === null || is_scalar($value) || is_array($value)
|| is_object($value) && $value instanceof \JsonSerializable
) {
return true;
}
throw new Exception(
sprintf(
'Widget configuration items either must be null, scalar, array or JSON serializable, got "%s"',
is_object($value) ? get_class($value) : gettype($value)
),
1588178538
);
}
);
} else {
$data['widgetConfiguration'] = null;
}
return $data;
}
/**
* @return string
*/
......@@ -323,4 +266,12 @@ class WidgetContext implements \JsonSerializable
{
return $this->viewHelperChildNodeRenderingContext;
}
/**
* @return array
*/
public function __sleep()
{
return ['widgetIdentifier', 'ajaxWidgetIdentifier', 'widgetConfiguration', 'controllerObjectName', 'parentPluginNamespace', 'parentExtensionName', 'parentPluginName', 'widgetViewHelperClassName'];
}
}
......@@ -129,14 +129,14 @@ class WidgetContextTest extends UnitTestCase
/**
* @test
*/
public function jsonEncodeContainsExpectedPropertyNames()
public function sleepReturnsExpectedPropertyNames()
{
self::assertEquals(
[
'widgetIdentifier', 'ajaxWidgetIdentifier', 'widgetConfiguration', 'controllerObjectName',
'parentPluginNamespace', 'parentExtensionName', 'parentPluginName', 'widgetViewHelperClassName'
],
array_keys(json_decode(json_encode($this->widgetContext), true))
$this->widgetContext->__sleep()
);
}
}
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