When user input is glued into a template instead of handed in as data, the template engine runs the user's text as code.
A template mixes static markup with dynamic values — "Hello {{ name }}" renders to "Hello Ada" once name is supplied. The engine treats {{ … }} as expressions to evaluate, and everything else as literal text.
The danger is in where the input goes. If you build the template string itself by concatenating user input, the user's characters become part of the template — so {{ 7*7 }} typed by an attacker is no longer data the page displays, it is template code the engine executes. That is server-side template injection.
The fix is a one-line mental rule: user input is data, not template source. Pass it as a context variable into a fixed template file and let autoescape handle the output. Never build the template string by gluing input into it.
# BAD — input becomes part of the template source.
# Whatever the user typed is parsed as template code.
from flask import request
from jinja2 import Template
name = request.args["name"]
html = Template("Hi " + name).render() # {{7*7}} -> 49
# or, equivalently dangerous:
# render_template_string("Hi " + name)
# SAFE — input is passed as a value, template is fixed.
from flask import render_template
name = request.args["name"]
html = render_template("hi.html", name=name) # prints {{7*7}} literally
# hi.html contains: Hi {{ name }} (autoescape on)
The classic detection probe is {{ 7*7 }}: if the response shows 49, the engine evaluated it, so injection is live. From there an attacker escalates through built-in objects — {{ config }}, then attribute traversal like {{ ''.__class__.__mro__ }} reaching __subclasses__ — until they find something that runs OS commands. That is the path from a math trick to remote code execution.
| Dimension | What it means |
|---|---|
| Impact | Ranges from information leak ({{ config }} exposes secrets) up to full remote code execution on the server. |
| Why it happens | Input is spliced into the template rather than handed in as data, so the engine parses and evaluates it. |
| Mitigation cost | Low — pass data as context variables, use fixed template files, keep autoescape on. For user-authored templates, use a logic-less or sandboxed engine. |
| Residual risk | Sandboxes can be escaped; a logic-less engine (no expression evaluation) removes the class of bug entirely. |
Template("Hi " + name) or render_template_string("..." + input) is the root cause, not a detail.__class__, __mro__, and __subclasses__ are well known and reach dangerous built-ins.A "personalized greeting" feature builds its message by concatenating the name field straight into a Jinja template: Template("Hello " + name + "!").render(). A curious attacker submits {{7*7}} as their name and the page replies "Hello 49!" — proof the engine is evaluating their input.
They escalate to {{config['SECRET_KEY']}} and the greeting now leaks the application's signing secret; with more traversal it becomes command execution. The fix is a single change: stop concatenating and pass the value in — render_template("greeting.html", name=name), where greeting.html is Hello {{ name }}!. Now {{7*7}} renders as the literal text {{7*7}}, because it is data, not code.
An app does render_template_string("Welcome " + user_bio), and user_bio is fully attacker-controlled. Where does the risk come from?
You submit {{7*7}} into a form and the response contains 49. What does that tell you?