CSRF Protection
When to Use
Drupal's Form API handles CSRF automatically for
#ajaxcallbacks. For custom routes outside Form API, apply the correct requirement based on HTTP method.
Decision
| Endpoint type | CSRF requirement | How it works |
|---|---|---|
| Custom AJAX POST route | _csrf_request_header_token: 'TRUE' |
Validates X-CSRF-Token header Drupal AJAX sends automatically |
| Action link / GET with side-effects | _csrf_token: 'TRUE' |
Validates token query parameter appended by Url::toString(TRUE) |
Form API #ajax callback |
Automatic — no route requirement needed | FormBuilder embeds and validates form token in every AJAX request |
Pattern
// Form API: CSRF handled automatically — no route changes needed.
$form['trigger'] = [
'#type' => 'button',
'#ajax' => ['callback' => '::ajaxCallback', 'wrapper' => 'target'],
];
// Custom POST AJAX route: use _csrf_request_header_token.
// Drupal.ajax sends X-CSRF-Token header automatically on POST.
// my_module.routing.yml:
// my_module.ajax_endpoint:
// path: '/my-module/ajax/endpoint'
// defaults:
// _controller: '\Drupal\my_module\Controller\AjaxController::ajaxEndpoint'
// requirements:
// _permission: 'access content'
// _csrf_request_header_token: 'TRUE'
// GET action link with token (destructive action via link):
// my_module.delete_item:
// requirements:
// _csrf_token: 'TRUE' # token= query param validated
// (Url::toString(TRUE) appends the token automatically)
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
public function ajaxEndpoint(Request $request) {
// _csrf_request_header_token validates the token automatically.
// Additional check: verify it's actually an AJAX request.
if (!$request->isXmlHttpRequest()) {
throw new AccessDeniedHttpException('AJAX requests only.');
}
return new AjaxResponse();
}
Reference: core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php, core/lib/Drupal/Core/Access/CsrfAccessCheck.php
Common Mistakes
- Wrong: Using
_csrf_token: 'TRUE'on a POST AJAX route → Right:_csrf_tokenvalidates a URL query param, not the X-CSRF-Token header; use_csrf_request_header_token: 'TRUE'for POST routes - Wrong: Relying on X-Requested-With header alone → Right: Can be spoofed; it is not a substitute for token validation
- Wrong: Using GET for state-changing operations → Right: GET bypasses header-based CSRF checks; use POST for all data changes
- Wrong: Skipping CSRF entirely on custom routes → Right: Form API protects forms but custom routes need explicit requirements
- Wrong: Not configuring trusted_host_patterns → Right: Requests from untrusted origins; configure in settings.php