[SECURITY] Limit user access in workspace previews 10/47610/2
authorNicole Cordes <typo3@cordes.co>
Tue, 12 Apr 2016 09:11:26 +0000 (11:11 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Tue, 12 Apr 2016 09:11:30 +0000 (11:11 +0200)
To view a preview of a workspace page a backend user is simulated.
Currently the user who created the preview link is taken into account.
This patch creates a limited backend user to be able to process the
web request.

Resolves: #28175
Releases: master, 7.6, 6.2
Security-Commit: f0445be5322b4c0e4b1c0900542aca4e00a39f46
Security-Bulletins: TYPO3-CORE-SA-2016-009, 010, 011, 012
Change-Id: I80dd9168b22bf62b2a2ed4a264240d07f056cc73
Reviewed-on: https://review.typo3.org/47610
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
typo3/sysext/version/Classes/Hook/PreviewHook.php
typo3/sysext/version/ext_localconf.php

index 1cb869f..a84d841 100644 (file)
@@ -15,6 +15,9 @@ namespace TYPO3\CMS\Version\Hook;
  */
 
 use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Type\Bitmask\Permission;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 
@@ -46,6 +49,14 @@ class PreviewHook implements \TYPO3\CMS\Core\SingletonInterface
     protected $previewConfiguration = false;
 
     /**
+     * Defines whether to force read permissions on pages.
+     *
+     * @var bool
+     * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getPagePermsClause
+     */
+    protected $forceReadPermissions = false;
+
+    /**
      * hook to check if the preview is activated
      * right now, this hook is called at the end of "$TSFE->connectToDB"
      *
@@ -94,34 +105,58 @@ class PreviewHook implements \TYPO3\CMS\Core\SingletonInterface
      */
     public function initializePreviewUser(&$params, &$pObj)
     {
-        if ((is_null($params['BE_USER']) || $params['BE_USER'] === false)
-            && $this->previewConfiguration !== false
-            && $this->previewConfiguration['BEUSER_uid'] > 0
-        ) {
-            // New backend user object
-            $BE_USER = GeneralUtility::makeInstance(FrontendBackendUserAuthentication::class);
-            $BE_USER->userTS_dontGetCached = 1;
-            $BE_USER->setBeUserByUid($this->previewConfiguration['BEUSER_uid']);
-            $BE_USER->unpack_uc('');
-            if ($BE_USER->user['uid']) {
-                $BE_USER->fetchGroupData();
+        // if there is a valid BE user, and the full workspace should be previewed, the workspacePreview option should be set
+        $workspaceUid = $this->previewConfiguration['fullWorkspace'];
+        $workspaceRecord = null;
+        if ((is_null($params['BE_USER']) || $params['BE_USER'] === false) && $this->previewConfiguration !== false && $this->previewConfiguration['BEUSER_uid'] > 0) {
+            // First initialize a temp user object and resolve usergroup information
+            /** @var FrontendBackendUserAuthentication $tempBackendUser */
+            $tempBackendUser = $this->createFrontendBackendUser();
+            $tempBackendUser->userTS_dontGetCached = 1;
+            $tempBackendUser->setBeUserByUid($this->previewConfiguration['BEUSER_uid']);
+            if ($tempBackendUser->user['uid']) {
+                $tempBackendUser->unpack_uc('');
+                $tempBackendUser->fetchGroupData();
+                // Handle degradation of admin users
+                if ($tempBackendUser->isAdmin() && ExtensionManagementUtility::isLoaded('workspaces')) {
+                    $workspaceRecord = $this->getDatabaseConnection()->exec_SELECTgetSingleRow(
+                        'uid, adminusers, reviewers, members, db_mountpoints',
+                        'sys_workspace',
+                        'pid=0 AND uid=' . (int)$workspaceUid . BackendUtility::deleteClause('sys_workspace')
+                    );
+                    // Either use configured workspace mount or current page id, if admin user does not have any page mounts
+                    if (empty($tempBackendUser->groupData['webmounts'])) {
+                        $tempBackendUser->groupData['webmounts'] = !empty($workspaceRecord['db_mountpoints']) ? $workspaceRecord['db_mountpoints'] : $pObj->id;
+                    }
+                    // Force add degraded admin user as member of this workspace
+                    $workspaceRecord['members'] = 'be_users_' . $this->previewConfiguration['BEUSER_uid'];
+                    // Force read permission for degraded admin user
+                    $this->forceReadPermissions = true;
+                }
+                // Store only needed information in the real simulate backend
+                $BE_USER = $this->createFrontendBackendUser();
+                $BE_USER->userTS_dontGetCached = 1;
+                $BE_USER->user = $tempBackendUser->user;
+                $BE_USER->user['admin'] = 0;
+                $BE_USER->groupData['webmounts'] = $tempBackendUser->groupData['webmounts'];
+                $BE_USER->groupList = $tempBackendUser->groupList;
+                $BE_USER->userGroups = $tempBackendUser->userGroups;
+                $BE_USER->userGroupsUID = $tempBackendUser->userGroupsUID;
                 $pObj->beUserLogin = true;
             } else {
                 $BE_USER = null;
                 $pObj->beUserLogin = false;
             }
+            unset($tempBackendUser);
             $params['BE_USER'] = $BE_USER;
         }
-        // if there is a valid BE user, and the full workspace should be
-        // previewed, the workspacePreview option shouldbe set
-        $workspaceUid = $this->previewConfiguration['fullWorkspace'];
         if ($pObj->beUserLogin
             && is_object($params['BE_USER'])
             && MathUtility::canBeInterpretedAsInteger($workspaceUid)
         ) {
             if ($workspaceUid == 0
                 || $workspaceUid >= -1
-                && $params['BE_USER']->checkWorkspace($workspaceUid)
+                && $params['BE_USER']->checkWorkspace($workspaceRecord ?: $workspaceUid)
                 && $params['BE_USER']->isInWebMount($pObj->id)
             ) {
                 // Check Access to workspace. Live (0) is OK to preview for all.
@@ -134,6 +169,42 @@ class PreviewHook implements \TYPO3\CMS\Core\SingletonInterface
     }
 
     /**
+     * Overrides the page permission clause in case an admin
+     * user has been degraded to a regular user without any user
+     * group assignments. This method is used as hook callback.
+     *
+     * @param array $parameters
+     * @return string
+     * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getPagePermsClause
+     */
+    public function overridePagePermissionClause(array $parameters)
+    {
+        $clause = $parameters['currentClause'];
+        if ($parameters['perms'] & 1 && $this->forceReadPermissions) {
+            $clause = ' 1=1';
+        }
+        return $clause;
+    }
+
+    /**
+     * Overrides the row permission value in case an admin
+     * user has been degraded to a regular user without any user
+     * group assignments. This method is used as hook callback.
+     *
+     * @param array $parameters
+     * @return int
+     * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::calcPerms
+     */
+    public function overridePermissionCalculation(array $parameters)
+    {
+        $permissions = $parameters['outputPermissions'];
+        if (!($permissions & Permission::PAGE_SHOW) && $this->forceReadPermissions) {
+            $permissions |= Permission::PAGE_SHOW;
+        }
+        return $permissions;
+    }
+
+    /**
      * Looking for an ADMCMD_prev code, looks it up if found and returns configuration data.
      * Background: From the backend a request to the frontend to show a page, possibly with
      * workspace preview can be "recorded" and associated with a keyword.
@@ -173,8 +244,8 @@ class PreviewHook implements \TYPO3\CMS\Core\SingletonInterface
                 die(sprintf($message, htmlspecialchars(preg_replace('/\\&?' . $this->previewKey . '=[[:alnum:]]+/', '', $returnUrl))));
             }
             // Look for keyword configuration record:
-            $where = 'keyword=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($inputCode, 'sys_preview') . ' AND endtime>' . $GLOBALS['EXEC_TIME'];
-            $previewData = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow('*', 'sys_preview', $where);
+            $where = 'keyword=' . $this->getDatabaseConnection()->fullQuoteStr($inputCode, 'sys_preview') . ' AND endtime>' . $GLOBALS['EXEC_TIME'];
+            $previewData = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('*', 'sys_preview', $where);
             // Get: Backend login status, Frontend login status
             // - Make sure to remove fe/be cookies (temporarily);
             // BE already done in ADMCMD_preview_postInit()
@@ -265,7 +336,7 @@ class PreviewHook implements \TYPO3\CMS\Core\SingletonInterface
                 'BEUSER_uid' => $backendUserUid
             ))
         );
-        $GLOBALS['TYPO3_DB']->exec_INSERTquery('sys_preview', $fieldData);
+        $this->getDatabaseConnection()->exec_INSERTquery('sys_preview', $fieldData);
         return $fieldData['keyword'];
     }
 
@@ -281,4 +352,22 @@ class PreviewHook implements \TYPO3\CMS\Core\SingletonInterface
         $ttlHours = (int)$GLOBALS['BE_USER']->getTSConfigVal('options.workspaces.previewLinkTTLHours');
         return $ttlHours ? $ttlHours : 24 * 2;
     }
+
+    /**
+     * @return \TYPO3\CMS\Core\Database\DatabaseConnection
+     */
+    protected function getDatabaseConnection()
+    {
+        return $GLOBALS['TYPO3_DB'];
+    }
+
+    /**
+     * @return FrontendBackendUserAuthentication
+     */
+    protected function createFrontendBackendUser()
+    {
+        return GeneralUtility::makeInstance(
+            FrontendBackendUserAuthentication::class
+        );
+    }
 }
index df60adc..55f4b4c 100644 (file)
@@ -8,6 +8,8 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['move
 // Register hook to check for the preview mode in the FE
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['connectToDB']['version_preview'] = \TYPO3\CMS\Version\Hook\PreviewHook::class . '->checkForPreview';
 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['postBeUser']['version_preview'] = \TYPO3\CMS\Version\Hook\PreviewHook::class . '->initializePreviewUser';
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause']['version_preview'] = \TYPO3\CMS\Version\Hook\PreviewHook::class . '->overridePagePermissionClause';
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms']['version_preview'] = \TYPO3\CMS\Version\Hook\PreviewHook::class . '->overridePermissionCalculation';
 
 if (TYPO3_MODE === 'BE') {
     // add default notification options to every page