[FEATURE] Add API to CSRF protect Ajax calls in Backend 73/27873/8
authorHelmut Hummel <helmut.hummel@typo3.org>
Wed, 26 Feb 2014 14:47:15 +0000 (15:47 +0100)
committerHelmut Hummel <helmut.hummel@typo3.org>
Thu, 27 Feb 2014 20:04:00 +0000 (21:04 +0100)
This change adds API to register Ajax ids with
their handler and to get an Ajax URL for
a specific AjaxID.

A token check is added to the ajax.php dispatcher
script. To stay backwards compatible, the token
is only checked, if the AjaxId is registered not
using the new API.

The new API will be used by TYPO3 core in
consecutive changes.

Resolves: #56345
Documentation: #56347
Releases: 6.2
Change-Id: I188a9312b0f4239040e461ba09dc9c8f2b93a68b
Reviewed-on: https://review.typo3.org/27873
Reviewed-by: Wouter Wolters
Reviewed-by: Anja Leichsenring
Tested-by: Anja Leichsenring
Reviewed-by: Markus Klein
Tested-by: Markus Klein
Reviewed-by: Helmut Hummel
Tested-by: Helmut Hummel
NEWS.md
typo3/ajax.php
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php

diff --git a/NEWS.md b/NEWS.md
index d2da037..ec843f6 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
@@ -55,6 +55,19 @@ The options array (the fourth parameter) now can contain a 'label' to set a
 custom label for each category field.
 
 
+* Ajax API addition
+
+New API has been added to register an Ajax handler for the backend.
+\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerAjaxHandler('TxMyExt::process', '\Vendor\Ext\AjaxHandler->process');
+
+Along with that, new API has been added to get the Ajax URL for a given AjaxId.
+This URL will contain a CSRF protection token that will be checked
+in the ajax.php dispatcher:
+$ajaxUrl = \TYPO3\CMS\Core\Utility\BackendUtility::getAjaxUrl('TxMyExt::process');
+
+Registering an Ajax script the "old" way by just adding it to TYPO3_CONF_VARS has been deprecated,
+but no deprecation log is been written and the handler still work in a backwards compatible way.
+
 #### CSS Styled Content
 
 * Removed deprecated DB fields
index 672778a..7ea5112 100644 (file)
@@ -55,8 +55,21 @@ if (in_array($ajaxID, $noUserAjaxIDs)) {
 
 require __DIR__ . '/init.php';
 
-// finding the script path from the variable
-$ajaxScript = $TYPO3_CONF_VARS['BE']['AJAX'][$ajaxID];
+// Finding the script path from the registry
+$ajaxRegistryEntry = isset($GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxID]) ? $GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxID] : NULL;
+$ajaxScript = NULL;
+$csrfTokenCheck = FALSE;
+if ($ajaxRegistryEntry !== NULL) {
+       if (is_array($ajaxRegistryEntry)) {
+               if (isset($ajaxRegistryEntry['callbackMethod'])) {
+                       $ajaxScript = $ajaxRegistryEntry['callbackMethod'];
+                       $csrfTokenCheck = $ajaxRegistryEntry['csrfTokenCheck'];
+               }
+       } else {
+               // @Deprecated since 6.2 will be removed two versions later
+               $ajaxScript = $ajaxRegistryEntry;
+       }
+}
 
 // Instantiating the AJAX object
 $ajaxObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Http\\AjaxRequestHandler', $ajaxID);
@@ -68,8 +81,19 @@ if (empty($ajaxID)) {
 } elseif (empty($ajaxScript)) {
        $ajaxObj->setError('No backend function registered for ajaxID "' . $ajaxID . '".');
 } else {
-       $ret = \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($ajaxScript, $ajaxParams, $ajaxObj, FALSE, TRUE);
-       if ($ret === FALSE) {
+       $success = TRUE;
+       $tokenIsValid = TRUE;
+       if ($csrfTokenCheck) {
+               $tokenIsValid = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->validateToken(\TYPO3\CMS\Core\Utility\GeneralUtility::_GP('ajaxToken'), 'ajaxCall', $ajaxID);
+       }
+       if ($tokenIsValid) {
+               // Cleanup global variable space
+               unset($csrfTokenCheck, $ajaxRegistryEntry, $tokenIsValid, $success);
+               $success = \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction($ajaxScript, $ajaxParams, $ajaxObj, FALSE, TRUE);
+       } else {
+               $ajaxObj->setError('Invalid CSRF token detected for ajaxID "' . $ajaxID . '"!');
+       }
+       if ($success === FALSE) {
                $ajaxObj->setError('Registered backend function for ajaxID "' . $ajaxID . '" was not found.');
        }
 }
index c726662..2fd87a7 100644 (file)
@@ -3003,15 +3003,38 @@ class BackendUtility {
                        return FALSE;
                }
                if ($backPathOverride === FALSE) {
-                       $backPath = $GLOBALS['BACK_PATH'];
+                       $backPath = isset($GLOBALS['BACK_PATH']) ? $GLOBALS['BACK_PATH'] : '';
                } else {
                        $backPath = $backPathOverride;
                }
-               $allUrlParameters = array();
-               $allUrlParameters['M'] = $moduleName;
-               $allUrlParameters['moduleToken'] = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->generateToken('moduleCall', $moduleName);
-               $allUrlParameters = array_merge($allUrlParameters, $urlParameters);
-               $url = 'mod.php?' . ltrim(GeneralUtility::implodeArrayForUrl('', $allUrlParameters, '', TRUE, TRUE), '&');
+               $urlParameters['M'] = $moduleName;
+               $urlParameters['moduleToken'] = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->generateToken('moduleCall', $moduleName);
+               $url = 'mod.php?' . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters, '', TRUE, TRUE), '&');
+               if ($returnAbsoluteUrl) {
+                       return GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR') . $url;
+               } else {
+                       return $backPath . $url;
+               }
+       }
+
+       /**
+        * Returns the Ajax URL for a given AjaxID including a CSRF token.
+        *
+        * @param string $ajaxIdentifier Identifier of the AJAX callback
+        * @param array $urlParameters URL parameters that should be added as key value pairs
+        * @param bool/string $backPathOverride Backpath that should be used instead of the global $BACK_PATH
+        * @param bool $returnAbsoluteUrl If set to TRUE, the URL returned will be absolute, $backPathOverride will be ignored in this case
+        * @return string Calculated URL
+        */
+       static public function getAjaxUrl($ajaxIdentifier, array $urlParameters = array(), $backPathOverride = FALSE, $returnAbsoluteUrl = FALSE) {
+               if ($backPathOverride) {
+                       $backPath = $backPathOverride;
+               } else {
+                       $backPath = isset($GLOBALS['BACK_PATH']) ? $GLOBALS['BACK_PATH'] : '';
+               }
+               $urlParameters['ajaxID'] = $ajaxIdentifier;
+               $urlParameters['ajaxToken'] = \TYPO3\CMS\Core\FormProtection\FormProtectionFactory::get()->generateToken('ajaxCall', $ajaxIdentifier);
+               $url = 'ajax.php?' . ltrim(GeneralUtility::implodeArrayForUrl('', $urlParameters, '', TRUE, TRUE), '&');
                if ($returnAbsoluteUrl) {
                        return GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR') . $url;
                } else {
index 39c0b22..cb0e638 100644 (file)
@@ -27,8 +27,6 @@ namespace TYPO3\CMS\Core\Utility;
  *  This copyright notice MUST APPEAR in all copies of the script!
  ***************************************************************/
 
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
 /**
  * Extension Management functions
  *
@@ -883,6 +881,20 @@ class ExtensionManagementUtility {
        }
 
        /**
+        * Registers an Ajax Handler
+        *
+        * @param string $ajaxId Identifier of the handler, that is used in the request
+        * @param string $callbackMethod TYPO3 callback method (className->methodName).
+        * @param bool $csrfTokenCheck Only set this to FALSE if you are sure that the registered handler does not modify any data!
+        */
+       static public function registerAjaxHandler($ajaxId, $callbackMethod, $csrfTokenCheck = TRUE) {
+               $GLOBALS['TYPO3_CONF_VARS']['BE']['AJAX'][$ajaxId] = array(
+                       'callbackMethod' => $callbackMethod,
+                       'csrfTokenCheck' => $csrfTokenCheck
+               );
+       }
+
+       /**
         * Adds a module path to $GLOBALS['TBE_MODULES'] for used with the module dispatcher, mod.php
         * Used only for modules that are not placed in the main/sub menu hierarchy by the traditional mechanism of addModule()
         * Examples for this is context menu functionality (like import/export) which runs as an independent module through mod.php