ASVS 5.0 V1.3.7 (Level 2) requires protection against template injection by never allowing templates to be built from untrusted input. User data belongs in the context passed to a fixed template, never in the template text itself. Where dynamic template text is unavoidable, the untrusted part must be strictly validated or sanitized.
Why it matters
Template engines are small programming languages. If an attacker controls template source, they can evaluate expressions, read objects reachable from the template context and often reach the underlying runtime, which means remote code execution (CWE-1336). A probe of {{7*7}} that returns 49 is the classic sign.
The bug is subtle because the code looks like ordinary string handling:
# vulnerable
from flask import render_template_string
return render_template_string("<h1>Hello " + name + "</h1>")
Here name becomes part of the template. A value of {{ config }} prints application configuration, and worse payloads reach system calls.
How to implement it
Keep the template fixed, and pass data as variables.
from flask import render_template_string
# safe: name is data, not template source
return render_template_string("<h1>Hello {{ name }}</h1>", name=name)
Better, store templates as files in your repository and call render_template("hello.html", name=name). Auto escaping then protects the HTML output too.
The same applies in Node:
const tpl = Handlebars.compile('<h1>Hello {{name}}</h1>'); // fixed source
const html = tpl({ name: userName }); // user data as context
When users must author templates (email builders, report designers, marketing tools), treat that as a product feature with its own risk:
- Use a logic less or sandboxed engine such as Mustache, or Liquid in its safe mode, with no access to the host language.
- Give templates a minimal context of plain data objects, not framework objects, request objects or modules.
- Use the engine's sandbox features (Jinja2
SandboxedEnvironment), knowing sandboxes have been bypassed before, so also add process isolation, timeouts and low privileges. - Limit template size and rendering time.
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment(autoescape=True)
html = env.from_string(user_template).render(customer={"name": name})
How to test it
Submit probes into every field that might end up in a template, including names, subject lines, error messages and URL parameters: {{7*7}}, ${7*7}, <%= 7*7 %>, #{7*7} and {{7*'7'}}. The output should show the literal text, not 49 or 7777777. Different outputs help identify the engine, which is why a result like 49 is serious.
In code review, search for render_template_string, Template( with concatenation, from_string, compile( on engines, and f strings or concatenation that end up inside a template argument. A static scan can trace data from a request into those calls. The demo below walks through one.
Click through it at your own pace, or open it full screen.
Common mistakes
- Concatenating a variable into a template string "just for a title".
- Putting user text in an error message, then rendering that message as a template.
- Using a sandbox as the only defense for template authoring.
- Passing the whole request or application object into the template context.
- Storing user supplied template text in a database and rendering it in an admin screen.
Try it yourself
Create a minimal Flask app with the vulnerable pattern from this article and run it locally. Visit it with ?name={{7*7}} and confirm it prints 49. Then change only the template call so the name is passed as a variable, restart, and visit again. The page should now print the literal text {{7*7}}.
Next, extend your test suite with a parametrized test that sends a handful of engine probes to every route and asserts that none of the responses contain 49 or 7777777. Routes that render error pages, 404 pages and email previews are the ones people forget.
If you offer user authored templates, add a test that renders a hostile template, such as one that tries to read config or call __class__, and assert that rendering raises an error. Keep the sandbox environment creation in one helper so nobody builds a second, unprotected environment later.
Quick check
What is the difference between safe and unsafe use of a template engine?
Safe: fixed template source, user values passed as variables. Unsafe: user values joined into the source text before it is compiled.
What does a response of 49 to a probe of seven times seven show?
That the input was evaluated as a template expression, so template injection is present.