dev/core#3160 fix inability to import 'just contactid' and add to group
[civicrm-core.git] / CRM / Utils / REST.php
index 0604758d1a75c0bc132d9540b7e05f4e34f9c67e..c34b9e1be3ac51c1ece992a2b325134eb2d8be1c 100644 (file)
@@ -399,9 +399,7 @@ class CRM_Utils_REST {
     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 @@ class CRM_Utils_REST {
 
     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 @@ class CRM_Utils_REST {
     // 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 @@ class CRM_Utils_REST {
     }
   }
 
+  /**
+   * 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
+   *       <A HREF>, <IFRAME>, <IMG>, `Location:`, or similar CSRF vector.
+   */
+  public static function isWebServiceRequest(): bool {
+    if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? NULL) === 'XMLHttpRequest') {
+      return TRUE;
+    }
+
+    // If authx is enabled, and if the user gives a credential, it will store metadata.
+    $authx = \CRM_Core_Session::singleton()->get('authx');
+    $allowFlows = [
+      // Some flows are resistant to CSRF. Allow these:
+
+      // <legacyrest> Current request has valid `?api_key=SECRET&key=SECRET` ==> Strong-secret params
+      'legacyrest',
+
+      // <param> Current request has valid `?_authx=SECRET` ==> Strong-secret param
+      'param',
+
+      // <xheader> Current request has valid `X-Civi-Auth:` ==> Custom header AND strong-secret param
+      'xheader',
+
+      // Other flows are not resistant to CSRF on their own (need combo w/`X-Requested-With:`).
+      // Ignore these:
+      // <login> Relies on a session `Cookie:` (which browsers re-send automatically).
+      // <auto> First request might be resistant, but all others use session `Cookie:`.
+      // <header> Browsers often retain list of credentials and re-send automatically.
+    ];
+
+    if (!empty($authx) && in_array($authx['flow'], $allowFlows)) {
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
 }