Best Practices: Security
When to Use
Every AJAX implementation requires security considerations.
Critical Security Measures
Critical Security Measures:
- Access Control
- Every AJAX callback and route needs
_permissionor_custom_access - Check triggering element accessibility in Form API callbacks
-
Never trust client-sent data; validate server-side
-
Input Validation
- Validate and sanitize all user input
- Use FormStateInterface validation, not JavaScript-only
-
Apply upload validators for file uploads (extension, size, MIME type)
-
CSRF Protection
- Drupal handles CSRF for Form API automatically
- Add
_csrf_request_header_token: 'TRUE'to custom POST AJAX routes (validates the X-CSRF-Token header Drupal AJAX sends automatically;_csrf_token: 'TRUE'validates a URL query param instead — see CSRF Protection) -
Verify
$request->isXmlHttpRequest()to prevent direct calls -
XSS Prevention
- Always return render arrays, not HTML strings
- Use
Html::escape()for user input in markup - MessageCommand and AnnounceCommand auto-escape content
-
Never use
'#markup' => $user_inputwithout sanitization -
SQL Injection Prevention
- Use Entity Query API, not direct database queries
- Never concatenate user input into queries
-
Use parameterized queries for custom database operations
-
Content Security Policy (CSP)
- Avoid inline JavaScript in AJAX responses
- Use attached libraries instead of inline scripts
- Configure CSP headers in settings.php
Common Vulnerabilities
Common Vulnerabilities:
| Vulnerability | Attack Vector | Prevention |
|---|---|---|
| XSS | Unsanitized user input in AJAX response | Use render arrays, escape manually if needed |
| CSRF | Forged requests to AJAX endpoints | Form API handles automatically; add _csrf_request_header_token: 'TRUE' on custom POST AJAX routes |
| Unauthorized access | Missing permission checks | Add _permission or _custom_access |
| SQL injection | User input in queries | Use Entity Query API |
| File upload attacks | Malicious file uploads | Configure upload validators |
Pattern
// 1. Access control on every route
my_module.ajax_endpoint:
requirements:
_permission: 'access content'
_csrf_request_header_token: 'TRUE' // POST AJAX route: validates X-CSRF-Token header
// 2. Return render arrays, not HTML strings
// BAD:
return '<div>' . $user_input . '</div>';
// GOOD:
return [
'#markup' => $this->t('@content', ['@content' => $user_input]),
];
// 3. Validate upload files
$form['file'] = [
'#type' => 'managed_file',
'#upload_validators' => [
'FileExtension' => ['extensions' => 'jpg jpeg png'],
'FileSizeLimit' => ['fileLimit' => '2M'],
],
];
// 4. Verify triggering element
$triggering_element = $form_state->getTriggeringElement();
if (!$triggering_element) {
throw new AccessDeniedHttpException();
}
// 5. Use Entity Query for database operations
$nids = $this->entityTypeManager->getStorage('node')->getQuery()
->condition('type', 'article')
->condition('field_value', $user_input) // Safe: parameterized
->accessCheck(TRUE)
->execute();