
Drupal Security
Keep Drupal modules and custom PHP safe from SQL injection, XSS, and access bypass while you or your agent writes forms, controllers, and queries.
Install
npx skills add https://github.com/madsnorgaard/agent-resources --skill drupal-securityWhat is this skill?
- Auto-activates on forms, controllers, plugins, and user input handling in Drupal
- Parameterized queries and placeholders instead of concatenated SQL
- XSS guidance: #plain_text, render elements, and Twig auto-escape vs raw #markup
- Access control and safe handling of query parameters and user-provided content
- Critical security patterns documented with vulnerable vs safe PHP examples
Adoption & trust: 1 installs on skills.sh; 45 GitHub stars; 3/3 security scanners passed (skills.sh audits); trending (+100% hot-view momentum).
Recommended Skills
Journey fit
Security hardening is the canonical shelf for proactive vulnerability prevention, even though the skill activates during active Drupal development. Maps to application security patterns (parameterized queries, output escaping, access checks) rather than generic backend scaffolding alone.
Common Questions / FAQ
Is Drupal Security safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.
SKILL.md
READMESKILL.md - Drupal Security
# 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:** ```php // 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:** ```php // 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:** ```php use Drupal\Component\Utility\Xss; // Filter but allow safe HTML tags $safe = Xss::filterAdmin($user_html); ``` ### Access Control **Always verify permissions:** ```php // 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: ```php // 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 ```php $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 ```php // 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 | Rem