Cross-site scripting (XSS)

Cross-site scripting (XSS) lets an attacker run their own JavaScript in your visitors' browsers; Drupal guards against it by escaping or filtering output.

Learn DrupalDrupal glossary

Cross-site scripting (XSS) is an attack in which an attacker gets a website to run malicious code as though it were part of the site. In Drupal, it means script placed in content such as a comment or a page body, which other users then load in their browsers. Drupal's protections include Twig's automatic escaping, the Html::escape() and Xss::filter() helpers, and text formats.

How XSS is handled in Drupal 11

MDN describes the problem in two steps: a site accepts input an attacker can craft, then prints it into a page without making it harmless. The injected code can read the page content and local storage, and it can send HTTP requests with the visitor's credentials. Drupal gives you several layers that stop the second step.

  • Twig. The Twig theme engine escapes everything it prints by default. The raw filter switches that off, and the Drupal documentation says to avoid it, particularly for data a user may have entered. Attributes must be quoted: class="{{ class }}" is safe, class={{ class }} is not.
  • Render arrays. Renderer::ensureMarkupIsSafe() filters #markup with Xss::filter(). Unless you set #allowed_tags, it uses the admin tag list. Setting #plain_text escapes the text instead.
  • PHP helpers. Use Html::escape() for plain text and Xss::filter() for text that may carry some HTML. By default, Xss::filter() keeps a, em, strong, cite, blockquote, code, ul, ol, li, dl, dt and dd. It also removes on* event handler attributes and style attributes, and checks URL attributes for protocols such as javascript:.
  • Translatable strings. t() with @ or % placeholders builds safe strings, and Drupal treats the result as safe when it renders it.
  • Text formats. Each text format is a series of filters that run on output; the stored text stays unchanged.

Example

A Twig template prints a value that a visitor may have typed. The first line is escaped by default. The second turns that protection off and is the pattern to avoid.

<p class="{{ label_class }}">{{ visitor_name }}</p>
        <p>{{ visitor_name|raw }}</p>

In PHP, a render array that must show a user's text as plain text uses #plain_text:

$build = ['#plain_text' => $visitor_name];

Common mistakes

  • Using |raw on user data. It does not escape the value.
  • Leaving an attribute unquoted. An unquoted value can add new attributes to the tag.
  • Using Xss::filterAdmin() for visitor input. The API documentation limits it to admin-only content, because its tag list is much broader. It still excludes script and style.
  • Calling check_markup() to sanitize text in a theme or module. The Drupal documentation says this is almost never correct, except for text areas that use a text format.
  • Giving a permissive text format to untrusted users. The format decides what survives on output.

For marketing and content teams

You rarely write code, but you decide who may use which text format. Keep the permissive formats for trusted editors and leave everyone else on a restrictive one. Install security updates promptly: the advisory list on drupal.org includes entries classified as cross-site scripting, in core and in contributed projects.

Sources

Questions about XSS in Drupal

Escaping turns HTML special characters into harmless text, so the visitor sees any tags as typed text. In PHP that is Html::escape(). Filtering keeps a short list of safe tags and strips the rest. In PHP that is Xss::filter(). Use escaping when no markup is needed.

In Twig templates, yes: the Twig theme engine escapes printed values by default, except where a template uses the raw filter. In render arrays, #markup is run through Xss::filter() and #plain_text is escaped.

Only trusted users. Each text format has its own permission, so you can keep permissive formats for trusted editors and leave untrusted users on a format such as Basic HTML, which filters out dangerous tags.

No. MDN presents a strict Content Security Policy as a backup if other defenses fail. The main defense is still encoding or sanitizing output, so the injected code never reaches the page as a working script.

Related terms

All Drupal terms from A to Z