09/02/2026
Twig autoescaping and the raw filter
Twig escapes everything it prints, for HTML, unless told otherwise. In a Symfony application that is on by default and it is the reason template XSS is rare in Twig codebases and common in ones that concatenate strings.
So {{ user.displayName }} is safe even when the display name is
<script>alert(1)</script> — it renders as visible text rather than as a
script. Nothing to do.
The trouble starts when output arrives already containing markup you meant
to keep, an escaped <em> shows up literally on the page, and the fix
that makes the symptom disappear is:
{{ user.bio|raw }}
|raw does not "fix the display". It turns escaping off for that value,
which is fine for a string your own code built and is a stored XSS the
moment the value came from a person.
The question that decides it
Not "does this contain HTML" but "who wrote this string". Markup you generated is safe to print raw. Markup a user submitted is not, no matter how much you would like their bold tags to work.
If users are genuinely allowed to submit formatting, the value has to be
sanitized on the way in or on the way out — allow-listing the tags and
attributes you accept. Symfony's HtmlSanitizer component exists for this
and gives you a sanitize_html filter:
{{ user.bio|sanitize_html }}
That is a different operation from |raw. It still removes things; it
just removes fewer things than escaping does.
Autoescaping is HTML escaping, and that is not always what you need
This is the part that catches people who know about |raw and think they
are safe. The default strategy escapes for the HTML body. Four
contexts on the same page need something else:
{# inside a <script> block -- HTML escaping is not enough #}
<script>
const name = {{ user.displayName|e('js') }};
</script>
{# an attribute value with no quotes around it #}
<div data-name={{ user.displayName|e('html_attr') }}>
{# a URL component, not a whole URL #}
<a href="/search?q={{ term|e('url') }}">
{# a value dropped into a style #}
<div style="width: {{ width|e('css') }}px">
The five strategies are html, js, css, url and html_attr. The
docs are explicit that url is for a subcomponent of a URI and not
for escaping an entire URI, which is a distinction worth keeping: escaping
a whole URL mangles it, and escaping none of a query parameter is an
injection.
For JSON specifically, prefer building the whole object in PHP and
printing it through json_encode rather than escaping fields one at a
time inside a script tag.
Turning it off for a block, and why to avoid it
{% autoescape false %}
{{ trustedHtml }}
{% endautoescape %}
This is |raw applied to everything inside the block, including the
variables somebody adds to it next year without noticing the tag six lines
above. A per-value |raw is worse-looking and safer, because it names
exactly which value is being trusted.
What to grep for
|raw and autoescape false. Every occurrence should be a value your own
code produced, and each one is worth being able to say that about out
loud. It is a short list in most projects and it is the whole of the
template attack surface.