security_scan
Security Scan reviews custom Drupal code for common security mistakes before it ships — the access-control gaps, XSS sinks, injection, unsafe deserialization and committed secrets that slip into hand-written and, increasingly, AI-generated modules.
It is a command-line developer/CI tool, not a site feature: there is no UI and nothing runs on a live request. You point it at a module (or any path), it reads the code statically, and it prints a prioritised list of things to review — with a machine-readable mode built for CI and for AI coding agents.
Why this module
AI assistants now write a large share of custom Drupal code, and they reliably reintroduce the same well-known vulnerability classes: routes with no access requirement, #markup and |raw XSS, missing CSRF tokens, unserialize() on untrusted data, and hardcoded credentials. Security Scan gives both humans and agents a fast, repeatable check for exactly these patterns, mapped to the OWASP Top 10 (2025) and grounded in real Drupal security-advisory fixes.
What it checks
Two tiers, run by one Drush command.
Deterministic checks — always on, native PHP, no external tools:
Area Detects Access control (A01) Routes with norequirements, _access: 'TRUE', login-only gating, entity queries without an explicit ->accessCheck()
CSRF (A01)
State-changing routes with no _csrf_token requirement
XSS (A03)
Twig |raw and {% autoescape false %}; #markup assigned from a variable
SQL injection (A03)
Queries built by string concatenation
Integrity / RCE (A08)
Callable render keys (#pre_render, #lazy_builder, …) assigned from a variable
Deserialization (A08)
unserialize() without ['allowed_classes' => FALSE]
Secrets (A04)
Hardcoded AWS keys, committed private keys, credential-shaped literals
Deep taint pass — optional: if the drupal-security/scanner Psalm engine is installed, the same command also traces untrusted input across functions and files to find cross-function XSS, SQL injection, command injection, path traversal, SSRF and insecure deserialization. When it is absent, the deterministic checks still run and the report says the deep pass was skipped.
Requirements
- Drupal 10.3+ or Drupal 11
- Drush 12 or later (the module ships a Drush command)
- PHP 8.1 or later
- (Optional, for the deep taint pass)
drupal-security/scannerandvimeo/psalm, installed as dev dependencies
Installation
Install as you would any Drupal module. As a developer/CI tool it is typically a dev dependency:
composer require drupal/security_scan --dev drush en security_scan
To enable the optional deep taint pass, also add the scanner engine — the module detects it automatically:
composer require --dev drupal-security/scannerFor local or monorepo development, where the engine lives outside the site's vendor/ directory, point the module at a checkout instead:
SECURITY_SCAN_PSALM_ROOT=/path/to/scanner drush security:scan my_moduleThe path must contain vendor/bin/psalm and the plugin's autoloader.
Usage
The module has no settings form; behaviour is controlled per run via command options.
# Scan a module by machine name. drush security:scan my_module # Scan any path. drush security:scan web/modules/custom/my_module # Machine-readable output for CI or an AI agent. drush security:scan my_module --format=json
The command exits non-zero when any error-severity finding is present, so it drops into CI pipelines and pre-commit hooks. secscan is a shorter alias.
---------- ------------------- ------------------------------- --------------------------------------------------------- Severity Class Location Finding ---------- ------------------- ------------------------------- --------------------------------------------------------- ERROR A01 Access control my_module.routing.yml Route 'my_module.report' has no access requirements. ERROR A03 SQL injection src/Controller/Report.php SQL string built with concatenation of a variable. WARNING A03 XSS templates/report.html.twig The '|raw' filter disables auto-escaping. ---------- ------------------- ------------------------------- --------------------------------------------------------- 37 file(s) scanned — 2 error, 1 warning, 0 info.
For AI coding workflows
--format=json returns each finding with a check, severity, OWASP class, file, line and a concrete recommendation. An agent can read its own scan output, apply the recommended fixes, and re-scan until the error count reaches zero — a closed correction loop. This module is the executable half of a secure-coding workflow; a companion agent skill decides when to run it and how to read the results.
An honest limit
A finding means review this, not this is broken — static review has false positives. And a clean result does not prove the code is secure: access-control correctness is a reasoning problem these syntactic checks cannot see, and it still needs human review. Treat Security Scan as a first pass that frees human attention for the judgement calls, not a certificate.