Change record status: 
Introduced in branch: 

Table of Contents

Overview (#)

The Twig theme engine now autoescapes string variables in the template. That means that every string printed from a Twig template (anything between {{ }}) gets escaped. Because of this, it is possible for a string of markup to become double-escaped. For example, if a module or a preprocess function happens to escape a string before passing it to a template, it could result in a double escape (to make this change notice easier to find: doubleescape, double-escape) issue.

Strings sanitized by t() are \Drupal\Core\StringTranslation\TranslatableMarkup objects which are instances of \Drupal\Component\Render\MarkupInterface. Twig does not autoescape those objects, because they are markup, similar to the result of render arrays. (See the Renderer service).

When module developers use the appropriate sanitization functions, or output their content using the theme and render systems, double-escaping will not be an issue. Using the theme and render systems will also allow output to be themed, sanitized and altered properly.

Concatenating strings in and outside of Twig templates (#)

The most common cause of unwanted escaping or double-escaping is when previously escaped strings are being concatenated by PHP code outside of the render, theme, or translation systems (or other systems designed for handling safe concatenation). For example:

$a = t('Here is a %foo', ['%foo' => $foo]);
$b = '<div class="bar">' . $a . '<div>'; // This will cause unwanted escaping, and double-escaping for $foo.
$output = [
  '#type' => 'details',
  '#description' => $b,

With this code, when $output gets rendered, $b gets escaped (and therefore, everything in $foo gets double escaped). The solution to this is to use a sanitization-aware concatenation technique for generating $b.

Most themes will use Twig templates. However, because of this change, theme functions and theme engines must also perform their own escaping to ensure the output is safe. See 2575445 for more information.

Option one: Twig templates (#)

Ideally, all HTML would be output from Twig templates, in order to take advantage of Twig's automatic escaping (and avoid markup being escaped). For example:

In foo.module

function foo_theme() {
  return [
    'foo_wrapper' => ['variables' => ['attributes' => [], 'content' => NULL]],

In foo/templates/foo-wrapper.html.twig

 * @file
 * Default theme implementation for wrapping foo output.
 * Available variables:
 * - content: The foo content being wrapped.
 * - attributes: HTML attributes for the foo.
 * @ingroup themeable
<span{{ attributes }}>{{ content }}</span>

For more information, see @todo Twig resource.

There is a small performance cost associated with a Twig template file (usually a few ms, but sometimes higher), so in some cases or for functionality in the critical path, converting to a Twig template is impractical. (For example, see #1939102: Convert theme_indentation() to Twig). And sometimes, a full conversion isn't practical due to the amount of work that is necessary. In these cases, use one of the other options listed below.

Option two: Render arrays (#)

After using Twig templates directly, the next best option is to use a render array. Render arrays can be considered the base units of output for the rendered page and are supported automatically in many contexts, including for anything passed to template variables (e.g. in theme preprocess functions), form controllers and other page controllers, link text for generated links, hook_requirements() 'description' elements, and so on.

#markup (#)

When markup needs to be printed from the PHP code inside a render array, #markup should be used. #markup will automatically sanitize content, removing non-whitelisted elements in cases where the content has not already been sanitized.

$markup = "alert('I don't get the chance to be run.') however this link is still working because we know that a element is safe";
$build = ['#markup' => $markup];

By default, #markup performs this automatic sanitization by passing the content through \Drupal\Component\Utility\Xss::filterAdmin() to prevent XSS vectors. A different filtering strategy can be used with the #allowed_tags key. See the change record for the addition of #allowed_tags for more information.

#markup strings that have already been sanitized via another mechanism (for example, strings from t()) are not sanitized again. This means that adding several #markup elements to a single render array is an effective way of combining multiple translatable strings conditionally and without unneeded overhead. For example:

$build = [
  ['#markup' => t('This is the first string.')],
  ['#prefix' => ' ', '#markup' => t('This is the second string with a %placeholder.', ['%placeholder' => $variable])],
if ($condition) {
  $build[] =   ['#prefix' => ' ', '#markup' => t('This is the conditionally added string')]];

@todo Also mention upcoming possible changes for arrays of #markup

#plain_text (#)

If only plain text needs to be printed via a render array, #plain_text should be used instead of #markup:

$value = "The HTML from this string will get escaped.";
$render = ['#plain_text' => $value];

This will output: <marquee>The HTML from this string will get escaped.</marquee>

Item lists (#)

To output a list of items, the item_list Twig template can be used:

$items = ['foo', 'bar', 'baz'];
$item_list = [
  '#theme' => 'item_list',
  '#items' => $items,

This will output:

Unsafe items will be autoescaped by Twig.

We now also have a comma-separated context available for item lists, which is preferred over imploding or joining things with a comma:

$items = ['foo', 'bar', 'baz'];
$item_list = [
  '#theme' => 'item_list',
  '#items' => $items,
  '#context' => ['list_style' => 'comma-list'],

This will output a <ul>-based list that is styled in CSS to look like a comma-separated list. I.e. it will look identical to "foo, bar, baz", except it is a more semantic and accessible use of HTML as it reinforces that the list is an actual list, and allows people with disabilities to navigate the lists more efficiently. Note that some minor formatting issues with inline item lists will be addressed in #2557367: Fix inline list CSS.

Inline templates (#)

The next best option for a small snippet of markup is to use an inline_template render element, which provides easy access to Twig's automatic escaping capability. An example:

$build['string'] = [
  '#type' => 'inline_template',
  '#template' => '{{ var }}',
  '#context' => [
    'var' => $possible_unsafe_var,

Because this is a render element, markup can still be altered through hooks making changes to #template until the element is rendered. Keeping HTML markup within an inline_template render element -- rather than sending markup rendered into a string as a variable into a Twig template -- prevents that markup from being escaped.

If you run into a situation where there are a lot of inline_template render elements, that may be a sign that a conversion to Twig templates should happen at a higher level.

Don't render early (#)

A frequent cause of escaping bugs with render arrays is early rendering. In Drupal 7, it was common to call drupal_render() on a render array anywhere in code. In Drupal 8, early rendering should be avoided because it interferes with proper caching and asset handling, leads to redundant escaping and filtering, and prevents the rendered output from being altered. So, in general, the Renderer (or the deprecated drupal_render()) should usually not be used directly.

However, sometimes, render arrays are useful to build simple HTML output in a situation where only strings are supported. For example, drupal_set_message() does not support render arrays because the messages are stored in the session. In such cases, RendererInterface::renderPlain() should be used (not RendererInterface::render() nor RendererInterface::renderRoot() nor the deprecated drupal_render()):

$message[] = ['#markup' => t('Review the errors listed below and then resubmit the form once they are resolved.')];
$message[] = [
  '#theme' => 'item_list',
  '#items' => $list_items,
drupal_set_message(\Drupal::service('renderer')->renderPlain($message), 'error'););

Option three: FormattableMarkup (#)

For simple concatenations, where you don't need all of the features of Twig or render arrays, you can also use new FormattableMarkup($string, $args);

// This will return an object that implements MarkupInterface with a sanitized
// string that will not be autoescaped. 
new FormattableMarkup('@var', array('@var' => $var));

Other usecases (#)

If you have a genuine use-case for script tags or other markup stripped by Xss::filerAdmin(), you will need to use proper templates or other APIs such as the Library API. See the #markup section under Render Arrays in the Render API overview.

If you are implementing your own custom rendering pipeline (rare), implement the internal MarkupInterface for your renderable output. See ViewsRenderPipelineMarkup for an example of when this is correct. Most modules should not implement this interface directly, and care must be taken to sanitize any implementations of this interface, since it will bypass all other text sanitization including Twig autoescape.

If you are creating non-HTML output (for example, a JSON response), you must ensure any unsafe input is escaped, because it will not go through the full HTML rendering process nor Twig's autoescape functionality. See #2503963: XSS in Quick Edit: entity title is not safely encoded for an example of the type of XSS bugs that may result if non-HTML output is not sanitized.

Double-escaping issues in core (#)

If you find an instance of a double-escaped string in Drupal 8 core following this change, file an issue at: #2297711: Fix HTML escaping due to Twig autoescape

Render arrays

String formatting and t()

Links and URLs

Theme system

Deprecated/removed SafeMarkup BC layer (#)


Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Not done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other updates done