Templates that escape by default
Why it matters You will rarely glue HTML together by hand. Real sites use templates, and good template systems escape every value for you. Your job is mostly to not switch that off by accident, and to recognise the few switches that do.
An analogy: a car that brakes for you
Modern cars brake automatically if you are about to hit something. You can switch the feature off, and sometimes there is a good reason. But every time it is off, it is worth asking why. Auto-escaping is the same: on by default in good tools, with a few clearly named ways to turn it off.
Template: an HTML file with gaps, such as {{ name }}, that the server fills in for each page. The template engine is the code that fills the gaps.
Auto-escaping: the template engine escapes every value it puts into a gap, unless you tell it the value is already safe.
Django
Django templates escape every {{ value }} by default: <, >, &, " and ', the same five you escaped by hand. This site's course pages are built that way.
{# safe: escaped automatically #}
<p>{{ review.text }}</p>
<input name="q" value="{{ query }}">
{# the dangerous switches: each one turns escaping off #}
{{ review.text|safe }}
{% autoescape off %}{{ review.text }}{% endautoescape %}In Python code, mark_safe(text) tells Django "this is already HTML, do not escape it". Use it only on text your own code wrote. When you need to build a small piece of HTML in Python, use format_html, which escapes every value you pass in:
from django.utils.html import format_html
from django.utils.safestring import mark_safe
bad = mark_safe(f"<b>{user_name}</b>") # user_name is NOT escaped
good = format_html("<b>{}</b>", user_name) # user_name IS escapedTo pass data to your page's JavaScript, use the json_script filter instead of pasting values into a <script> block. It writes the data as JSON in a way that cannot close the script tag early.
Jinja (and Flask)
Jinja is another popular Python template engine. Its default is different: a plain Environment() does not escape. Switch it on, as Flask does for .html templates:
from jinja2 import Environment, select_autoescape
env = Environment(autoescape=select_autoescape(["html", "htm", "xml"]))
# Jinja's dangerous switches: the |safe filter, Markup(text) in Python,
# and {% autoescape false %} blocks.What auto-escaping still cannot do
- Unquoted attributes:
<div title={{ x }}>breaks with a single space. Always writetitle="{{ x }}". - Link addresses:
href="{{ url }}"is escaped, but ajavascript:address has nothing to escape. Check that links start withhttps://orhttp://before you store or show them. - Inside
<script>blocks: usejson_scriptordata-attributes instead of template gaps.
A habit for code review
Search your project for |safe, mark_safe, autoescape off, Markup( and, in front-end code, innerHTML and dangerouslySetInnerHTML. Each hit should have a one-line reason next to it. If the value could ever contain visitor text, it is a bug waiting to happen.
A common misconception
"Our framework escapes, so we cannot have XSS." Frameworks escape the gaps. They cannot protect a value you marked safe, an address you did not check, or front-end code that uses innerHTML. The next module adds a second layer, Content Security Policy, for the mistakes that still slip through.
Want to go deeper? PortSwigger's free Web Security Academy has a full XSS track with its own practice labs, built for learning safely.
In other languages
The same defaults, and the same dangerous switches, in other stacks:
// React: text in JSX is escaped for you
<p>{review.text}</p>
// the switch that turns it off (the name is a warning on purpose)
<div dangerouslySetInnerHTML={{ __html: review.text }} />
// Go: html/template escapes for the context (text, attribute, URL, script)
import "html/template" // escapes
// import "text/template" // does NOT escape: never use it for HTML
tmpl := template.Must(template.New("p").Parse(`<p>{{.}}</p>`))
// the switch: template.HTML(text) marks text as already safe
// PHP (Twig) auto-escapes; {{ text|raw }} turns it off
// C# (Razor) auto-escapes @text; @Html.Raw(text) turns it offRecap
- Django escapes every template value by default; Jinja only when you switch it on.
- The dangerous switches:
|safe,mark_safe,autoescape off,Markup,dangerouslySetInnerHTML, Go'stemplate.HTML. - Use
format_htmlto build HTML in Python andjson_scriptto hand data to JavaScript. - Escaping does not check link addresses or fix unquoted attributes: quote them and allow only http and https links.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.