Safeguard
Application Security•

ASVS 5.0 V1.3.7: Never Build Templates From User Input

ASVS V1.3.7 forbids building templates from untrusted input. See the vulnerable pattern in Jinja2 and Node, the safe fix, and how to test for SSTI.

Safeguard Security Team
4 min read

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:

  1. Use a logic less or sandboxed engine such as Mustache, or Liquid in its safe mode, with no access to the host language.
  2. Give templates a minimal context of plain data objects, not framework objects, request objects or modules.
  3. Use the engine's sandbox features (Jinja2 SandboxedEnvironment), knowing sandboxes have been bypassed before, so also add process isolation, timeouts and low privileges.
  4. 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.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.