From 2b7de0448ed5cd4731be30fc22e33d977e245d44 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 5 Jan 2015 16:41:33 -0800 Subject: [PATCH] CRM-15578 - civicrm/ajax/attachment - Fix for attachments when debugging is disabled. The isAJAX() XSS check doesn't work correctly with the Angular file-upload client. This wasn't previously observed because most development was done in debug mode. Remove the isAJAX() XSS check. Instead, protect against XSS with a secure token. --- CRM/Core/Page/AJAX/Attachment.php | 61 ++++++++++++++++++++++++------- CRM/Core/Page/Angular.php | 3 ++ js/angular-crmAttachment.js | 2 +- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/CRM/Core/Page/AJAX/Attachment.php b/CRM/Core/Page/AJAX/Attachment.php index c2437b637a..158d7b67d8 100644 --- a/CRM/Core/Page/AJAX/Attachment.php +++ b/CRM/Core/Page/AJAX/Attachment.php @@ -31,6 +31,7 @@ * To upload a new file, submit a POST (multi-part encoded) to "civicrm/ajax/attachment". Inputs: * - POST['entity_table']: string * - POST['entity_id']: int + * - POST['attachment_token']: string * - FILES[*]: all of the files to attach to the entity * * The response is a JSON document. Foreach item in FILES, there's a corresponding record in the response which @@ -40,15 +41,23 @@ */ class CRM_Core_Page_AJAX_Attachment { + const ATTACHMENT_TOKEN_TTL = 10800; // 3hr; 3*60*60 + + /** + * (Page Callback) + */ public static function attachFile() { $result = self::_attachFile($_POST, $_FILES, $_SERVER); self::sendResponse($result); } /** - * @param array $post (like global $_POST) - * @param array $files (like global $_FILES) - * @param array $server (like global $_SERVER) + * @param array $post + * Like global $_POST. + * @param array $files + * Like global $_FILES. + * @param array $server + * Like global $_SERVER. * @return array */ public static function _attachFile($post, $files, $server) { @@ -56,9 +65,9 @@ class CRM_Core_Page_AJAX_Attachment { $results = array(); foreach ($files as $key => $file) { - if (!$config->debug && !self::isAJAX($server)) { + if (!$config->debug && !self::checkToken($post['crm_attachment_token'])) { require_once 'api/v3/utils.php'; - $results[$key] = civicrm_api3_create_error("SECURITY ALERT: Ajax requests can only be issued by javascript clients, eg. CRM.api3().", + $results[$key] = civicrm_api3_create_error("SECURITY ALERT: Attaching files via AJAX requires a recent, valid token.", array( 'IP' => $server['REMOTE_ADDR'], 'level' => 'security', @@ -114,15 +123,8 @@ class CRM_Core_Page_AJAX_Attachment { } /** - * @param array $server (like global $_SERVER) - * @return bool - */ - public static function isAJAX($server) { - return array_key_exists('HTTP_X_REQUESTED_WITH', $server) && $server['HTTP_X_REQUESTED_WITH'] == "XMLHttpRequest"; - } - - /** - * @param array $result list of API responses, keyed by file + * @param array $result + * List of API responses, keyed by file. */ public static function sendResponse($result) { $isError = FALSE; @@ -144,4 +146,35 @@ class CRM_Core_Page_AJAX_Attachment { echo json_encode(array_merge($result)); CRM_Utils_System::civiExit(); } + + /** + * @return string + */ + public static function createToken() { + $signer = new CRM_Utils_Signer(CRM_Core_Key::privateKey(), array('for', 'ts')); + $ts = CRM_Utils_Time::getTimeRaw(); + return $signer->sign(array( + 'for' => 'crmAttachment', + 'ts' => $ts, + )) . ';;;' . $ts; + } + + /** + * @param string $token + * A token supplied by the user. + * @return bool + * TRUE if the token is valid for submitting attachments + * @throws Exception + */ + public static function checkToken($token) { + list ($signature, $ts) = explode(';;;', $token); + $signer = new CRM_Utils_Signer(CRM_Core_Key::privateKey(), array('for', 'ts')); + if (!is_numeric($ts) || CRM_Utils_Time::getTimeRaw() > $ts + self::ATTACHMENT_TOKEN_TTL) { + return FALSE; + } + return $signer->validate($signature, array( + 'for' => 'crmAttachment', + 'ts' => $ts, + )); + } } diff --git a/CRM/Core/Page/Angular.php b/CRM/Core/Page/Angular.php index fac9ea5584..c11188e82f 100644 --- a/CRM/Core/Page/Angular.php +++ b/CRM/Core/Page/Angular.php @@ -37,6 +37,9 @@ class CRM_Core_Page_Angular extends CRM_Core_Page { 'angular' => array( 'modules' => array_merge(array('ngRoute'), array_keys($modules)), ), + 'crmAttachment' => array( + 'token' => CRM_Core_Page_AJAX_Attachment::createToken(), + ), ); }); diff --git a/js/angular-crmAttachment.js b/js/angular-crmAttachment.js index bc0ec6c4c3..815ae36b84 100644 --- a/js/angular-crmAttachment.js +++ b/js/angular-crmAttachment.js @@ -76,7 +76,7 @@ var newItems = crmAttachments.uploader.getNotUploadedItems(); if (newItems.length > 0) { _.each(newItems, function (item) { - item.formData = [_.extend({}, target, item.crmData)]; + item.formData = [_.extend({crm_attachment_token: CRM.crmAttachment.token}, target, item.crmData)]; }); crmAttachments.uploader.onCompleteAll = function onCompleteAll() { delete crmAttachments.uploader.onCompleteAll; -- 2.25.1