Best Practices: Security
When to Use
Apply every item in this guide to every AJAX implementation. AJAX callbacks and routes are HTTP endpoints — they require the same security rigor as any web API.
Decision
| Vulnerability | Attack Vector | Prevention |
|---|---|---|
| XSS | Unsanitized user input in AJAX response | Use render arrays; Html::escape() for manual markup |
| CSRF | Forged requests to AJAX endpoints | Enable _csrf_token on routes; Form API handles automatically |
| Unauthorized access | Missing permission checks | Add _permission or _custom_access to every route |
| SQL injection | User input in queries | Use Entity Query API; never concatenate user input |
| File upload attacks | Malicious file uploads | Configure #upload_validators with extension, size, MIME |
Pattern
// 1. Access control on every route
my_module.ajax_endpoint:
requirements:
_permission: 'access content'
_csrf_token: 'TRUE'
// 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();
Common Mistakes
- Wrong: Skipping access checks on AJAX routes → Right: Always define
_permissionor_custom_access - Wrong: Using
'#markup' => $user_inputwithout sanitization → Right: Use$this->t('@var', ['@var' => $input])orHtml::escape() - Wrong: Accepting file uploads without validators → Right: Always validate extension, size, and MIME type
- Wrong: Using inline JavaScript in AJAX responses → Right: Attach libraries via
#attached; avoid inline scripts (CSP issues) - Wrong: Trusting client-side validation → Right: JavaScript validation can be bypassed; always validate server-side