| name | drupal-security |
| description | Drupal security for routes, controllers, forms, and queries. Add route permissions, access checks, CSRF protection, XSS prevention, SQL injection prevention, and secure file upload validation. |
Drupal Security
Critical Security Patterns
SQL Injection Prevention
NEVER concatenate user input into queries:
$query = "SELECT * FROM users WHERE name = '" . $name . "'";
$result = $connection->query($query);
$result = $connection->select('users', 'u')
->fields('u')
->condition('name', $name)
->execute();
$result = $connection->query(
'SELECT * FROM {users} WHERE name = :name',
[':name' => $name]
);
XSS Prevention
Always escape output. Trust the render system:
return ['#markup' => $user_input];
return ['#markup' => '<div>' . $title . '</div>'];
return ['#plain_text' => $user_input];
return [
'#type' => 'html_tag',
'#tag' => 'div',
'#value' => $title,
];
{{ variable }}
{{ variable|raw }}
For admin-only content:
use Drupal\Component\Utility\Xss;
$safe = Xss::filterAdmin($user_html);
Note: Never pass untrusted user input into #markup. Prefer #plain_text, Twig auto-escaping, or safe render elements.
Access Control
Always verify permissions:
my_module.admin:
path: '/admin/my-module'
requirements:
_permission: 'administer my_module'
if (!$this->currentUser->hasPermission('administer my_module')) {
throw new AccessDeniedHttpException();
}
$query = $this->entityTypeManager
->getStorage('node')
->getQuery()
->accessCheck(TRUE)
->condition('type', 'article');
CSRF Protection
Forms automatically include CSRF tokens. For custom AJAX:
$build['#attached']['drupalSettings']['myModule']['token'] =
\Drupal::csrfToken()->get('my_module_action');
Note: Prefer dependency injection over direct \Drupal::service() or \Drupal:: calls in classes.
if (!$this->csrfToken->validate($token, 'my_module_action')) {
throw new AccessDeniedHttpException('Invalid token');
}
File Upload Security
$validators = [
'file_validate_extensions' => ['pdf doc docx'],
'file_validate_size' => [25600000],
'FileSecurity' => [],
];
$file_mime = $file->getMimeType();
$allowed_mimes = ['application/pdf', 'application/msword'];
if (!in_array($file_mime, $allowed_mimes)) {
}
Sensitive Data
$this->logger->info('User @user logged in', ['@user' => $username]);
throw new \Exception('Database error');
$api_key = getenv('MY_API_KEY');
Red Flags to Watch For
When you see these patterns, immediately warn:
| Pattern | Risk | Fix |
|---|
| String concatenation in SQL | SQL injection | Use query builder |
#markup with variables | XSS | Use #plain_text |
accessCheck(FALSE) | Access bypass | Use accessCheck(TRUE) |
Missing _permission in routes | Unauthorized access | Add permission |
| Custom controller with no access check | Unauthorized access | Add route permission or access callback |
{{ var|raw }} in Twig | XSS | Remove |raw |
| Hardcoded passwords/keys | Credential exposure | Use env vars |
eval() or exec() | Code injection | Avoid entirely |
unserialize() on user data | Object injection | Use JSON |
| Missing CSRF validation in custom endpoints | CSRF attack | Validate token using csrf_token service |
| File upload without validation | Malicious file upload | Validate extension, size, and MIME type |
Direct \Drupal:: usage in classes | Hard to test / bad practice | Use dependency injection |
| Logging sensitive data | Information disclosure | Mask or avoid logging sensitive fields |