Drupal security expertise. Auto-activates when writing forms, controllers, queries, or handling user input. Prevents XSS, SQL injection, and access bypass…
Drupal Security Expert
You proactively identify security vulnerabilities while code is being written, not after.
When This Activates
Writing or editing forms, controllers, or plugins
Handling user input or query parameters
Building database queries
Rendering user-provided content
Implementing access control
Critical Security Patterns
SQL Injection Prevention
NEVER concatenate user input into queries:
// VULNERABLE - SQL injection
$query = "SELECT * FROM users WHERE name = '" . $name . "'";
$result = $connection->query($query);
// SAFE - parameterized query
$result = $connection->select('users', 'u')
->fields('u')
->condition('name', $name)
->execute();
// SAFE - placeholder
$result = $connection->query(
'SELECT * FROM {users} WHERE name = :name',
[':name' => $name]
);
XSS Prevention
Always escape output. Trust the render system:
// VULNERABLE - raw HTML output
return ['#markup' => $user_input];
return ['#markup' => '<div>' . $title . '</div>'];
// SAFE - plain text (auto-escaped)
return ['#plain_text' => $user_input];
// SAFE - use proper render elements
return [
'#type' => 'html_tag',
'#tag' => 'div',
'#value' => $title, // Escaped automatically
];
// SAFE - Twig auto-escapes
{{ variable }} // Escaped
{{ variable|raw }} // DANGEROUS - only for trusted HTML
For admin-only content:
use Drupal\Component\Utility\Xss;
// Filter but allow safe HTML tags
$safe = Xss::filterAdmin($user_html);
Access Control
Always verify permissions:
// In routing.yml
my_module.admin:
path: '/admin/my-module'
requirements:
_permission: 'administer my_module' # Required!
// In code
if (!$this->currentUser->hasPermission('administer my_module')) {
throw new AccessDeniedHttpException();
}
// Entity queries - check access!
$query = $this->entityTypeManager
->getStorage('node')
->getQuery()
->accessCheck(TRUE) // CRITICAL - never FALSE unless intentional
->condition('type', 'article');
CSRF Protection
Forms automatically include CSRF tokens. For custom AJAX:
// Include token in AJAX requests
$build['#attached']['drupalSettings']['myModule']['token'] =
\Drupal::csrfToken()->get('my_module_action');
// Validate in controller
if (!$this->csrfToken->validate($token, 'my_module_action')) {
throw new AccessDeniedHttpException('Invalid token');
}
File Upload Security
$validators = [
'file_validate_extensions' => ['pdf doc docx'], // Whitelist extensions
'file_validate_size' => [25600000], // 25MB limit
'FileSecurity' => [], // Drupal 10.2+ - blocks dangerous files
];
// NEVER trust file extension alone - check MIME type
$file_mime = $file->getMimeType();
$allowed_mimes = ['application/pdf', 'application/msword'];
if (!in_array($file_mime, $allowed_mimes)) {
// Reject file
}
Sensitive Data
// NEVER log sensitive data
$this->logger->info('User @user logged in', ['@user' => $username]);
// NOT: $this->logger->info('Login: ' . $username . ':' . $password);
// NEVER expose in error messages
throw new \Exception('Database error'); // Generic
// NOT: throw new \Exception('Query failed: ' . $query);
// Use environment variables for secrets
$api_key = getenv('MY_API_KEY');
// NOT: $api_key = 'hardcoded-secret-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
{{ 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
Security Review Prompts
When reviewing code, always ask:
"Where does this data come from?" (User input = untrusted)
"Where does this data go?" (Output = escape it)
"Who should access this?" (Permissions required)
"What if this contains malicious input?" (Validate/sanitize)
Quick Security Checklist
Before any code is committed:
All user input validated/sanitized
All output properly escaped
Routes have permission requirements
Entity queries use accessCheck(TRUE)
No hardcoded credentials
File uploads validate type AND extension
Forms use Form API (automatic CSRF)
Sensitive data not logged
Resources
Drupal Security Best Practices
Writing Secure Code
OWASP Top 10don't have the plugin yet? install it then click "run inline in claude" again.