diff --git a/CRM/Api4/Page/AJAX.php b/CRM/Api4/Page/AJAX.php index a368b20c4c83..64edf46f44f8 100644 --- a/CRM/Api4/Page/AJAX.php +++ b/CRM/Api4/Page/AJAX.php @@ -22,10 +22,7 @@ class CRM_Api4_Page_AJAX extends CRM_Core_Page { */ public function run() { $config = CRM_Core_Config::singleton(); - if (!$config->debug && (!array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) || - $_SERVER['HTTP_X_REQUESTED_WITH'] != "XMLHttpRequest" - ) - ) { + if (!$config->debug && !CRM_Utils_REST::isWebServiceRequest()) { $response = [ 'error_code' => 401, 'error_message' => "SECURITY ALERT: Ajax requests can only be issued by javascript clients, eg. CRM.api4().", diff --git a/CRM/Core/Page/AJAX.php b/CRM/Core/Page/AJAX.php index 17a0fe8f6ef9..4f1fd1b6407b 100644 --- a/CRM/Core/Page/AJAX.php +++ b/CRM/Core/Page/AJAX.php @@ -167,7 +167,9 @@ public static function returnJsonResponse($response) { $output = json_encode($response); // CRM-11831 @see http://www.malsup.com/jquery/form/#file-upload - if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { + // COMMENT: Wouldn't the `Accept:` header be more appropriate? Only use `X-Requested-With:` as a + // fallback where `Accept:` is missing? + if (CRM_Utils_REST::isWebServiceRequest()) { CRM_Utils_System::setHttpHeader('Content-Type', 'application/json'); } else { diff --git a/CRM/Utils/REST.php b/CRM/Utils/REST.php index 0604758d1a75..c34b9e1be3ac 100644 --- a/CRM/Utils/REST.php +++ b/CRM/Utils/REST.php @@ -399,9 +399,7 @@ public static function loadTemplate() { unset($param['q']); $smarty->assign_by_ref("request", $param); - if (!array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) || - $_SERVER['HTTP_X_REQUESTED_WITH'] != "XMLHttpRequest" - ) { + if (!self::isWebServiceRequest()) { $smarty->assign('tplFile', $tpl); $config = CRM_Core_Config::singleton(); @@ -434,10 +432,7 @@ public static function ajaxJson() { require_once 'api/v3/utils.php'; $config = CRM_Core_Config::singleton(); - if (!$config->debug && (!array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) || - $_SERVER['HTTP_X_REQUESTED_WITH'] != "XMLHttpRequest" - ) - ) { + if (!$config->debug && !self::isWebServiceRequest()) { $error = civicrm_api3_create_error("SECURITY ALERT: Ajax requests can only be issued by javascript clients, eg. CRM.api3().", [ 'IP' => $_SERVER['REMOTE_ADDR'], @@ -499,11 +494,7 @@ public static function ajax() { // restrict calls to this etc // the request has to be sent by an ajax call. First line of protection against csrf $config = CRM_Core_Config::singleton(); - if (!$config->debug && - (!array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) || - $_SERVER['HTTP_X_REQUESTED_WITH'] != "XMLHttpRequest" - ) - ) { + if (!$config->debug && !self::isWebServiceRequest()) { require_once 'api/v3/utils.php'; $error = civicrm_api3_create_error("SECURITY ALERT: Ajax requests can only be issued by javascript clients, eg. CRM.api3().", [ @@ -636,4 +627,51 @@ public function loadCMSBootstrap() { } } + /** + * Does this request appear to be a web-service request? + * + * It is important to distinguish regular browser-page-loads from web-service-requests. Regular + * page-loads can be CSRF vectors, and we don't web-services to run via CSRF. + * + * @return bool + * TRUE if the current request appears to either XMLHttpRequest or non-browser-based. + * Indicated by either (a) custom headers like `X-Request-With`/`X-Civi-Auth` + * or (b) strong-secret-params that could theoretically appear in URL bar but which + * cannot be meaningfully forged for CSRF purposes (like `?api_key=SECRET` or `?_authx=SECRET`). + * FALSE if the current request looks like a standard browser request. This request may be generated by + * ,