Template autoescaping
Flask enables Jinja autoescaping by file extension. From Flask.select_jinja_autoescape:
if filename is None: return Truereturn filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg"))Five extensions. .j2 is not one of them, and neither is .jinja — the two
conventional extensions for Jinja templates.
Observed
Section titled “Observed”The same template body, the same variable, three filenames:
templates/page.html -> '<p><b>x</b></p>' autoescape=Truetemplates/page.j2 -> '<p><b>x</b></p>' autoescape=Falsetemplates/mail.txt -> 'Hello <b>x</b>' autoescape=Falsepage.html and page.j2 contain the identical string <p>{{ v }}</p>. One escapes
and one does not, and nothing in the application distinguishes them.
There is no warning, no configuration error and no visible difference until the value
contains markup. A codebase that names its templates index.html.j2 — a common
convention, since it tells editors the output format and the templating engine — has
autoescaping off everywhere.
from jinja2 import select_autoescape
app = Flask(__name__)app.jinja_env.autoescape = select_autoescape( enabled_extensions=("html", "htm", "xml", "xhtml", "svg", "j2", "jinja"), default_for_string=True, default=True, # escape by default, opt out per template)default=True inverts the rule: everything is escaped unless a template opts out with
{% autoescape false %}. That is the safer default, and it means adding a template with
a new extension does not silently create a hole.
# which template extensions does this application actually use?find . -path ./node_modules -prune -o -type f \ \( -name '*.j2' -o -name '*.jinja' -o -name '*.txt' -o -name '*.html*' \) -print \ | sed 's/.*\.\([a-z0-9]*\)$/\1/' | sort | uniq -c | sort -rnAnything in that list other than html, htm, xml, xhtml and svg is a template
rendering without escaping. Then check the live setting rather than the assumption:
python3 -c "from app import appfor name in ('a.html','a.j2','a.jinja','a.txt','a.html.j2'): print(f'{name:12}', app.select_jinja_autoescape(name))"a.html.j2 is the instructive row — it ends in .j2, so it is not escaped, despite
containing .html.
|safe and Markup are the deliberate opt-outs
Section titled “|safe and Markup are the deliberate opt-outs”Autoescaping is not the only lever. {{ value|safe }} disables escaping for that
expression, and markupsafe.Markup(value) marks a string as pre-escaped anywhere it is
used.
Both are legitimate and both are worth grepping for, because they are how escaping gets turned off one value at a time in a codebase that otherwise looks correct:
grep -rn '|safe' templates/ | head -20grep -rn 'Markup(' --include='*.py' . | head -20Each hit should be a value you produced, not one that came from a request or a database row a user can write to.
What autoescaping does not cover
Section titled “What autoescaping does not cover”Escaping is HTML-context aware only in the sense that it escapes HTML metacharacters. A
value interpolated inside a <script> block or into an event handler attribute is
still a hole, because the escaping produces valid HTML that is invalid as a JavaScript
string boundary.
<!-- escaped, and still wrong --><script>var user = "{{ name }}";</script>Pass data to JavaScript as JSON, which escapes for that context:
<script>var user = {{ name|tojson }};</script>The same applies to a value used as a URL — escaping does not stop javascript: from
being a scheme.
Turning on escaping for templates that previously had it off changes rendered output,
and the breakage is visual rather than an error: markup you intended to render starts
appearing as literal <b> on the page.
Every place that was relying on the old behaviour needs |safe, and finding them means
looking at rendered pages rather than at logs — nothing throws.
The manageable sequence is one extension at a time. Add j2 to the list, deploy, walk
the pages that use those templates, add |safe where the escaping was load-bearing,
then move to the next extension. Flipping default=True for everything at once on a
large template tree is how you get a bad afternoon.
Email templates are the ones to check by eye — a .txt body that starts rendering
& in place of & is a regression your users see and your tests do not.
Related
Section titled “Related”- Template injection — the worse failure, where the template source itself is user input
- Debug mode — the other place rendered output reaches a user unfiltered