How do you secure a Drupal app (XSS, CSRF, SQLi) and patch?

Outline a Drupal security plan that blocks XSS/CSRF/SQL injection and prevents privilege escalation.
Design a practical Drupal security baseline: safe coding for XSS/CSRF/SQLi, least privilege, and automated dependency patching.

answer

A strong Drupal security posture combines safe coding, locked permissions, and disciplined patching. Prevent SQL injection by always using the database API/Entity API and parameterized queries. Stop XSS with Twig auto-escaping, |escape, and Xss::filter() for rich text. Defeat CSRF using route access checks and Form API tokens. Limit privilege escalation via least privilege roles, user_access() checks, and audit trails. Automate security updates (core/modules) with Composer, advisories, and CI.

Long Answer

Securing a Drupal application means treating the framework’s guardrails as your default and adding operational discipline around them. Your objective is simple: prevent injection and spoofing, keep privileges narrow, and patch dependencies before attackers do. Below is a practical blueprint that aligns with Drupal security best practices and works at scale.

1) Inputs, output, and SQL injection

Most SQL injection risks disappear when you never concatenate SQL. In Drupal, use the Database API (::select(), ::insert(), ::update()) with placeholders, or—better—use the Entity API and Views for data access. If a custom query is unavoidable, bind all parameters and whitelist column names (map from a constant set, never pass user input as identifiers). Validate and normalize all request input through Form API/validators and typed request parameters; reject unknown fields early.

2) Cross-Site Scripting (XSS) hygiene

By default, Twig auto-escapes variables. Break glass only when you know a string is safe: use |escape explicitly and avoid |raw unless the HTML has been sanitized. For rich text, configure Text formats to filter tags, disallow scripts, and use the CKEditor allowed content rules; store the chosen format with the field so renderers can sanitize consistently. For programmatic HTML, run content through Xss::filter() or Html::escape(). Never echo JSON inside <script> without Json::encode(); for attributes/URLs, prefer Url::fromRoute() and Link::fromTextAndUrl() to avoid injection.

3) CSRF protection and state-changing routes

CSRF is countered by tying every state-changing request to a secret the attacker cannot guess. In Drupal, use Form API (CSRF tokens baked in) or route CSRF tokens via \Drupal::service('csrf_token'). For REST endpoints, require a token header or cookie+header double-submit, and ensure the route uses the correct access check. Keep idempotent GET routes read-only; mutate only via POST/PATCH/DELETE. Combine with SameSite cookies for session protection and set session cookies to HttpOnly; Secure.

4) Access control and privilege escalation

Prevent privilege escalation by enforcing least privilege. Build roles from the bottom up: Authenticated gets minimal capabilities; editors inherit only what they need; admin privileges are split (content vs config). Avoid granting “Administer site configuration” widely. In code, guard critical operations with AccessResult and user_access() checks; never rely solely on UI permissions. For entity-level security, implement Entity Access Control Handlers so that read/update/delete checks run centrally. Log all role and permission changes; send alerts for assignments to powerful roles.

5) File, media, and upload safety

Assume uploads are untrusted. Store files in non-executable locations and offload to a bucket/CDN. Restrict allowed extensions and validate MIME types; generate deterministic filenames. For images, use Drupal image styles to create derivatives; never serve originals for user content without scanning (e.g., ClamAV). Strip SVG scripts or disallow SVG unless sanitized. Set Content Security Policy (CSP) to reduce the blast radius of any missed XSS.

6) Headers, session, and transport

Enforce HTTPS everywhere; terminate TLS at the edge and redirect HTTP→HTTPS. Enable Security Kit/HTTP headers: HSTS, X-Frame-Options/frame-ancestors, X-Content-Type-Options, and CSP. For sessions, rotate IDs on login, set short lifetimes for admins, and restrict cookies to secure, HttpOnly, and SameSite. Rate-limit login attempts and protect /user/* with CAPTCHA or throttling after failures.

7) Configuration and deployment safety

Disable installing/uninstalling modules in production for non-admins. Keep $settings['trusted_host_patterns'] strict to stop host header attacks. Separate config per environment; import via CI, never through live clicks. Lock the file editor (no code editing via UI) and keep secrets in environment variables. Maintain read-only production deployments to shrink the attack surface.

8) Dependency patching and supply chain

Most real incidents come from old modules. Run Drupal core and contrib modules via Composer, pin with a lockfile, and subscribe to security advisories. In CI, run composer audit, check for core/contrib security updates, and auto-open PRs. Stage, test, then promote to production; never update by clicking on live. For custom modules/themes, run static analysis (PHPStan), coding standards, and minimal unit/kernel tests to catch regressions. Periodically prune unused modules; fewer dependencies mean fewer CVEs.

9) Monitoring, logging, and detection

Enable database/syslog with structured context (user, route, IP). Ship logs to a central store; alert on spikes in 403/404, failed logins, permission changes, or sudden cache bypass. Add security.txt, admin contact, and a plain vulnerability disclosure policy. Backups must be encrypted, rotated, and tested; DR drills ensure you can restore safely after an incident.

10) Process: reviews and threat modeling

Bake Drupal security checks into PR reviews: look for escaping, access checks, and safe DB usage. Run a lightweight threat model per feature (assets, data paths, actors). Rehearse incident playbooks (XSS found, credential leak, abused role). Security is a muscle: repeatable checklists beat ad-hoc heroics.

Put together, these practices secure a Drupal application against SQL injection, XSS, CSRF, and privilege escalation, while automated dependency patching keeps the attack surface shrinking instead of growing.

Table

Aspect Safe-by-default choice Why it blocks risk Notes
SQL injection DB/Entity API, placeholders No string concat → SQL injection dies Whitelist identifiers
XSS Twig auto-escape, `escape`, Text formats Encodes output; strips scripts
CSRF Form API tokens, route CSRF tokens Attacker lacks secret token No state change via GET
Access control Roles + Entity Access Handlers Stops privilege escalation Log role changes
Uploads Non-exec storage, MIME checks Blocks file RCE & HTML XSS Sanitize SVG or block
Headers HSTS, CSP, frame-ancestors Reduces XSS/clickjack Add SameSite cookies
Config Trusted hosts, read-only prod Prevents host header & drift No UI code edits
Patching Composer + advisories in CI Fast security updates Lockfile + staging tests
Monitoring Central logs, alerts Detects abuse early Watch 403/401 spikes

Common Mistakes

Granting broad “administer” permissions to editors, enabling silent privilege escalation. Writing raw SQL or concatenating WHERE clauses from user input instead of using placeholders. Sprinkling |raw in Twig to “fix formatting,” creating stored XSS. Allowing uploads into executable paths or serving user SVGs without sanitization. Mutating state via GET, or building custom forms that skip Form API tokens, inviting CSRF. Leaving trusted_host_patterns open, so host header attacks bypass protections. Clicking updates on production without Composer or staging, then breaking sites or missing security updates. Disabling cache/page protection during debugging and forgetting to re-enable. Storing secrets in settings.php under version control. Ignoring log spikes and failed logins; no alerts, no response.

Sample Answers

Junior:
“I use Entity/Database APIs with parameters to avoid SQL injection, rely on Twig auto-escape and |escape to stop XSS, and let the Form API handle CSRF tokens. Roles follow least privilege, and I keep modules updated with Composer in staging before production.”

Mid:
“My Drupal security baseline: Text formats + CKEditor rules for rich text, Url::fromRoute() for safe links, and AccessResult checks on controllers. Files land in non-exec storage; SVGs sanitized. I enforce trusted_host_patterns, CSP/HSTS, and SameSite cookies. Patching runs via Composer audit in CI with auto-PRs.”

Senior:
“I treat security as code: DTO-validated inputs, entity access handlers, and route CSRF tokens for REST mutations. Admin privileges are split; role changes are logged and alerted. Uploads go to a bucket; image styles serve derivatives only. Headers: CSP with strict sources, HSTS, and frame-ancestors. Security updates flow through a staging pipeline with tests; logs/metrics alert on abuse patterns.”

Evaluation Criteria

  • SQLi defense: Uses DB/Entity APIs with bound params; no concatenation; identifier whitelists.
  • XSS defense: Twig escaping, safe link builders, Text formats for rich text, no |raw misuse.
  • CSRF defense: Form API or route tokens; no state change via GET; SameSite/HttpOnly cookies.
  • Privilege control: Role design with least privilege; entity access handlers; audited role changes.
  • Uploads & CSP: Non-exec storage, MIME/extension validation, sanitized SVGs, CSP/HSTS headers.
  • Config safety: Trusted hosts, read-only prod, no UI code edits; secrets outside VCS.
  • Patching: Composer-managed core/modules, advisory scans, staged security updates with tests.
  • Monitoring: Central logs, alerts on auth anomalies and permission changes; backups tested.
    Red flags: |raw everywhere, raw SQL with user input, wide admin roles, prod clicks for updates, open host patterns, missing CSRF tokens.

Preparation Tips

Set up a demo site and deliberately create one of each flaw in a sandbox, then fix it: convert raw SQL to parameterized Database API; remove |raw and add Text formats; retrofit CSRF tokens on a custom route. Add trusted_host_patterns, HSTS, and a minimal CSP; verify no breakage. Configure uploads to non-exec storage and test SVG sanitization. Implement an Entity Access Handler for a sensitive content type and unit test allow/deny. Build a Composer workflow: composer outdated, composer audit, and an update PR; deploy first to staging with config import and smoke tests. Add log shipping and an alert for permission changes or failed logins. Finally, create a one-page Drupal security checklist and use it in code reviews so new features can’t regress.

Real-world Context

Public sector portal: Removing |raw and enforcing Text formats killed two stored XSS vectors in legacy nodes. CSP + HSTS reduced risk and passed an external audit. Composer-based security updates cut patch time from weeks to hours.
Media network: A custom search used concatenated SQL; swapping to the Database API with placeholders fixed intermittent errors and blocked potential SQL injection. Adding Redis + headers stabilized admin sessions behind WAF.
Higher-ed site: Students uploaded SVGs with scripts; enabling sanitization and serving only image-style derivatives neutralized the issue. Entity Access Handlers stopped cross-department content reads.
Finance microsite: A POST-less GET endpoint mutated state, letting CSRF work; converting to Form API with a token and adding SameSite cookies closed the door. Logs now alert on role grants and failed admin logins.

Key Takeaways

  • Use Entity/DB APIs and placeholders to eliminate SQL injection.
  • Let Twig escape; sanitize rich text; never rely on |raw.
  • Require CSRF tokens for all state changes; don’t mutate on GET.
  • Enforce least privilege with access handlers; log and alert role changes.
  • Patch via Composer in staging; lock trusted_host_patterns and ship CSP/HSTS.

Practice Exercise

Scenario:
You’re inheriting a busy Drupal application with sporadic XSS reports, occasional cache-bypass hotfixes, and manual core/module updates done live. The security team wants a provable plan that blocks SQL injection, XSS, CSRF, and privilege escalation, and guarantees fast security updates.

Tasks:

  1. Audit: find raw SQL, |raw in Twig, routes that change state via GET, open trusted_host_patterns, and wide admin roles. Document each with examples.
  2. Fixes: refactor queries to Database/Entity API; add validators and Text formats; replace inline links with Url::fromRoute(); convert state-changing GETs to POST with Form API or route CSRF tokens.
  3. Access: implement an Entity Access Handler for the most sensitive type; shrink roles; add alerts on role/permission changes.
  4. Uploads: move files to non-exec storage, restrict extensions/MIME, and sanitize SVG; serve images via image styles only.
  5. Headers: enable HSTS, CSP (self + vetted CDNs), frame-ancestors, and SameSite cookies.
  6. Patching: Composer lock, composer audit in CI, auto-PRs for security updates, staging smoke tests, and scheduled production promotion.
  7. Monitoring: ship logs centrally; alert on failed logins, 403 spikes, and config/role changes; rehearse a rollback.

Deliverable:
A short remediation plan (before/after diff snippets, checklist, and CI screenshots) that proves the Drupal security posture is now durable and auditable.

Still got questions?

Privacy Preferences

Essential cookies
Required
Marketing cookies
Personalization cookies
Analytics cookies
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.