From e4fb92a85bed8067d45d2c25c4b559642f206248 Mon Sep 17 00:00:00 2001
From: Oliver Hader <oliver@typo3.org>
Date: Tue, 12 May 2020 11:22:10 +0200
Subject: [PATCH] [SECURITY] Avoid insecure deserialization of $BE_USER->uc
 properties

General and unscoped collection of user settings in $BE_USER->uc is
vulnerable to insecure deserialization, triggered by lots of different
consumers invoking `unserialize()`.

Class deserialization is denied by using option
`['allowed_classes' => false]`.

Resolves: #90313
Releases: master, 9.5
Change-Id: Ic969441bcd4e85fcdbbde23f539bfbcb629ffbb4
Security-Bulletin: TYPO3-CORE-SA-2020-005
Security-References: CVE-2020-11067
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64469
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
---
 .../AbstractUserAuthentication.php            |  2 +-
 .../Core/Widget/AjaxWidgetContextHolder.php   | 34 +++++++--
 .../Classes/Core/Widget/WidgetContext.php     | 69 ++++++++++++++++---
 .../Unit/Core/Widget/WidgetContextTest.php    |  4 +-
 .../Controller/Remote/ActionHandler.php       |  4 +-
 5 files changed, 91 insertions(+), 22 deletions(-)

diff --git a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php
index 5620c51b97bb..7b30845cf628 100644
--- a/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php
+++ b/typo3/sysext/core/Classes/Authentication/AbstractUserAuthentication.php
@@ -1161,7 +1161,7 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
     public function unpack_uc($theUC = '')
     {
         if (!$theUC && isset($this->user['uc'])) {
-            $theUC = unserialize($this->user['uc']);
+            $theUC = unserialize($this->user['uc'], ['allowed_classes' => false]);
         }
         if (is_array($theUC)) {
             $this->uc = $theUC;
diff --git a/typo3/sysext/fluid/Classes/Core/Widget/AjaxWidgetContextHolder.php b/typo3/sysext/fluid/Classes/Core/Widget/AjaxWidgetContextHolder.php
index 7ec47ceb2769..24edbcbdd36c 100644
--- a/typo3/sysext/fluid/Classes/Core/Widget/AjaxWidgetContextHolder.php
+++ b/typo3/sysext/fluid/Classes/Core/Widget/AjaxWidgetContextHolder.php
@@ -32,7 +32,7 @@ class AjaxWidgetContextHolder implements SingletonInterface
      * An array $ajaxWidgetIdentifier => $widgetContext
      * which stores the widget context.
      *
-     * @var array
+     * @var WidgetContext[]
      */
     protected $widgetContexts = [];
 
@@ -55,9 +55,13 @@ class AjaxWidgetContextHolder implements SingletonInterface
     protected function loadWidgetContexts()
     {
         if (isset($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
-            $this->widgetContexts = unserialize($GLOBALS['TSFE']->fe_user->getKey('ses', $this->widgetContextsStorageKey));
+            $this->widgetContexts = $this->buildWidgetContextsFromArray(
+                json_decode($GLOBALS['TSFE']->fe_user->getKey('ses', $this->widgetContextsStorageKey ?? null), true) ?? []
+            );
         } else {
-            $this->widgetContexts = isset($GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey]) ? unserialize($GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey]) : [];
+            $this->widgetContexts = $this->buildWidgetContextsFromArray(
+                json_decode($GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey] ?? '', true) ?? []
+            );
             $GLOBALS['BE_USER']->writeUC();
         }
     }
@@ -66,7 +70,7 @@ class AjaxWidgetContextHolder implements SingletonInterface
      * Get the widget context for the given $ajaxWidgetId.
      *
      * @param string $ajaxWidgetId
-     * @return \TYPO3\CMS\Fluid\Core\Widget\WidgetContext
+     * @return WidgetContext
      */
     public function get($ajaxWidgetId)
     {
@@ -80,7 +84,7 @@ class AjaxWidgetContextHolder implements SingletonInterface
      * Stores the WidgetContext inside the Context, and sets the
      * AjaxWidgetIdentifier inside the Widget Context correctly.
      *
-     * @param \TYPO3\CMS\Fluid\Core\Widget\WidgetContext $widgetContext
+     * @param WidgetContext $widgetContext
      */
     public function store(WidgetContext $widgetContext)
     {
@@ -96,11 +100,27 @@ class AjaxWidgetContextHolder implements SingletonInterface
     protected function storeWidgetContexts()
     {
         if (isset($GLOBALS['TSFE']) && $GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
-            $GLOBALS['TSFE']->fe_user->setKey('ses', $this->widgetContextsStorageKey, serialize($this->widgetContexts));
+            $GLOBALS['TSFE']->fe_user->setKey('ses', $this->widgetContextsStorageKey, json_encode($this->widgetContexts));
             $GLOBALS['TSFE']->fe_user->storeSessionData();
         } else {
-            $GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey] = serialize($this->widgetContexts);
+            $GLOBALS['BE_USER']->uc[$this->widgetContextsStorageKey] = json_encode($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
+    {
+        $widgetContexts = [];
+        foreach ($data as $widgetId => $widgetContextData) {
+            $widgetContexts[$widgetId] = WidgetContext::fromArray($widgetContextData);
+        }
+        return $widgetContexts;
+    }
 }
diff --git a/typo3/sysext/fluid/Classes/Core/Widget/WidgetContext.php b/typo3/sysext/fluid/Classes/Core/Widget/WidgetContext.php
index 0049f86a4a58..5f2035182ade 100644
--- a/typo3/sysext/fluid/Classes/Core/Widget/WidgetContext.php
+++ b/typo3/sysext/fluid/Classes/Core/Widget/WidgetContext.php
@@ -29,8 +29,19 @@ use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
  *
  * @internal It is a purely internal class which should not be used outside of Fluid.
  */
-class WidgetContext
+class WidgetContext implements \JsonSerializable
 {
+    protected const JSON_SERIALIZABLE_PROPERTIES = [
+        'widgetIdentifier',
+        'ajaxWidgetIdentifier',
+        'widgetConfiguration',
+        'controllerObjectName',
+        'parentPluginNamespace',
+        'parentExtensionName',
+        'parentPluginName',
+        'widgetViewHelperClassName',
+    ];
+
     /**
      * Uniquely identifies a Widget Instance on a certain page.
      *
@@ -47,7 +58,8 @@ class WidgetContext
 
     /**
      * User-supplied widget configuration, available inside the widget
-     * controller as $this->widgetConfiguration.
+     * controller as $this->widgetConfiguration. Array items must be
+     * null, scalar, array or implement \JsonSerializable interface.
      *
      * @var array
      */
@@ -97,6 +109,51 @@ class WidgetContext
      */
     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
      */
@@ -266,12 +323,4 @@ class WidgetContext
     {
         return $this->viewHelperChildNodeRenderingContext;
     }
-
-    /**
-     * @return array
-     */
-    public function __sleep()
-    {
-        return ['widgetIdentifier', 'ajaxWidgetIdentifier', 'widgetConfiguration', 'controllerObjectName', 'parentPluginNamespace', 'parentExtensionName', 'parentPluginName', 'widgetViewHelperClassName'];
-    }
 }
diff --git a/typo3/sysext/fluid/Tests/Unit/Core/Widget/WidgetContextTest.php b/typo3/sysext/fluid/Tests/Unit/Core/Widget/WidgetContextTest.php
index 1adc67137ad6..14035d27ede4 100644
--- a/typo3/sysext/fluid/Tests/Unit/Core/Widget/WidgetContextTest.php
+++ b/typo3/sysext/fluid/Tests/Unit/Core/Widget/WidgetContextTest.php
@@ -129,14 +129,14 @@ class WidgetContextTest extends UnitTestCase
     /**
      * @test
      */
-    public function sleepReturnsExpectedPropertyNames()
+    public function jsonEncodeContainsExpectedPropertyNames()
     {
         self::assertEquals(
             [
                 'widgetIdentifier', 'ajaxWidgetIdentifier', 'widgetConfiguration', 'controllerObjectName',
                 'parentPluginNamespace', 'parentExtensionName', 'parentPluginName', 'widgetViewHelperClassName'
             ],
-            $this->widgetContext->__sleep()
+            array_keys(json_decode(json_encode($this->widgetContext), true))
         );
     }
 }
diff --git a/typo3/sysext/workspaces/Classes/Controller/Remote/ActionHandler.php b/typo3/sysext/workspaces/Classes/Controller/Remote/ActionHandler.php
index dfad0e82d5bb..2b05be4e7247 100644
--- a/typo3/sysext/workspaces/Classes/Controller/Remote/ActionHandler.php
+++ b/typo3/sysext/workspaces/Classes/Controller/Remote/ActionHandler.php
@@ -368,7 +368,7 @@ class ActionHandler
             }
             $beUserRecord = BackendUtility::getRecord('be_users', (int)$userUid);
             if (is_array($beUserRecord) && $beUserRecord['email'] !== '') {
-                $uc = $beUserRecord['uc'] ? unserialize($beUserRecord['uc']) : [];
+                $uc = $beUserRecord['uc'] ? unserialize($beUserRecord['uc'], ['allowed_classes' => false]) : [];
                 $recipients[$beUserRecord['email']] = [
                     'email' => $beUserRecord['email'],
                     'lang' => $uc['lang'] ?? $beUserRecord['lang']
@@ -386,7 +386,7 @@ class ActionHandler
                     continue;
                 }
                 if (!isset($recipients[$preselectedBackendUser['email']])) {
-                    $uc = (!empty($preselectedBackendUser['uc']) ? unserialize($preselectedBackendUser['uc']) : []);
+                    $uc = (!empty($preselectedBackendUser['uc']) ? unserialize($preselectedBackendUser['uc'], ['allowed_classes' => false]) : []);
                     $recipients[$preselectedBackendUser['email']] = [
                         'email' => $preselectedBackendUser['email'],
                         'lang' => $uc['lang'] ?? $preselectedBackendUser['lang']
-- 
GitLab