Syllabus Lesson 63 of 156 · Script injection (XSS)
Script injection (XSS)

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 escaped

To 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 write title="{{ x }}".
  • Link addresses: href="{{ url }}" is escaped, but a javascript: address has nothing to escape. Check that links start with https:// or http:// before you store or show them.
  • Inside <script> blocks: use json_script or data- 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 off

Recap

  • 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's template.HTML.
  • Use format_html to build HTML in Python and json_script to hand data to JavaScript.
  • Escaping does not check link addresses or fix unquoted attributes: quote them and allow only http and https links.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output