CRM_Utils_REST - Allow certain authentication styles to bypass the XMLHttpRequest...
authorTim Otten <totten@civicrm.org>
Sat, 11 Dec 2021 06:36:13 +0000 (22:36 -0800)
committerTim Otten <totten@civicrm.org>
Mon, 24 Jan 2022 21:53:51 +0000 (13:53 -0800)
Docblocks indicate the theory behind which styles are allowed and which are prohibited.

CRM/Utils/REST.php

index bf8a0104c0591762f0809124ec53c5bc44af5cfc..b82866867d970edc5dab201181614e8b94e339fa 100644 (file)
@@ -630,12 +630,32 @@ class CRM_Utils_REST {
   /**
    * Does this request appear to be a web-service request?
    *
+   * This is used to mitigate CSRF risks.
+   *
    * @return bool
-   *   TRUE if the current request appears to be web-service request (ie AJAX).
-   *   FALSE if the current request appears to be a standalone browser page-view.
+   *   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.
    */
   protected static function isWebServiceRequest(): bool {
-    return array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
+    if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? NULL) === 'XMLHttpRequest') {
+      return TRUE;
+    }
+
+    $authx = \CRM_Core_Session::singleton()->get('authx');
+    $allowFlows = ['legacyrest', 'param', 'xheader'];
+    // <legacyrest> Current request has valid `?api_key=SECRET&key=SECRET` ==> Strong-secret params
+    // <param> Current request has valid `?_authx=SECRET` ==> Strong-secret param
+    // <xheader> Current request has valid `X-Civi-Auth:` ==> Custom header AND strong-secret param
+    // NOTE: Prohibited flows: `login`, `auto`, and `header` are driven by standard headers (`Cookie:`/`Authorization:`)
+    if (!empty($authx) && in_array($authx['flow'], $allowFlows)) {
+      return TRUE;
+    }
+
+    return FALSE;
   }
 
 }