mcpField
D
drupal-security

drupal-security

github.com/madsnorgaardSecurity49842 installsUpdated 6w ago
Repository

About drupal-security

drupal-security is a security-focused skill for developers writing or reviewing Drupal code. It highlights common risks such as SQL injection, XSS, missing access controls, and unsafe file handling, with safer patterns and a pre-commit checklist.

Key features

  1. 01Identify SQL injection risks
  2. 02Prevent XSS in rendered output
  3. 03Check permissions and CSRF controls
  4. 04Review file upload validation
  5. 05Flag exposed credentials and unsafe code

Use cases

  1. 01Review Drupal forms and controllers
  2. 02Check user-input and database handling
  3. 03Audit routes and access permissions
  4. 04Inspect file upload code before commit

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/madsnorgaard/drupal-agent-resources --skill drupal-security

After installing, type/drupal-securityin Claude Code to confirm it loaded.

Details

From the project README

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:

PatternRiskFix
String concatenation in SQLSQL injectionUse query builder
#markup with variablesXSSUse #plain_text
accessCheck(FALSE)Access bypassUse accessCheck(TRUE)
Missing _permission in routesUnauthorized accessAdd permission
{{ var|raw }} in TwigXSSRemove |raw
Hardcoded passwords/keysCredential exposureUse env vars
eval() or exec()Code injectionAvoid entirely
unserialize() on user dataObject injectionUse JSON

Security Review Prompts

When reviewing code, always ask:

  1. "Where does this data come from?" (User input = untrusted)
  2. "Where does this data go?" (Output = escape it)
  3. "Who should access this?" (Permissions required)
  4. "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

Comments

Markdown supported