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();
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'],
// 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().",
[
}
}
+ /**
+ * 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;
+ }
+
}