drupal-security

Drupal security expertise. Auto-activates when writing forms, controllers, queries, or handling user input. Prevents XSS, SQL injection, and access bypass…

INSTALLATION
npx skills add https://github.com/madsnorgaard/agent-resources --skill drupal-security
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card