Flask · Server-side template injection (SSTI)

Server-side template injection (SSTI) in Flask

Flask exposes Jinja through two functions that look alike, and template injection is the result of using render_template_string() with a string that already has request data inside it.

Class
Server-side template injection (SSTI) in Python
CWE
CWE-1336, CWE-94
web framework
Flask documentation
On this page

How it arises in Flask

render_template(name, **context) loads a file from the templates folder. render_template_string(source, **context) takes, in the words of the API reference, the source code of the template to render. The first argument is code in both cases. Only in the second can a view build that code at runtime.

Where it actually shows up:

Custom error handlers. A 404 handler that wants to show the missing URL writes it into the page with an f-string. request.path is attacker-chosen and needs no authentication, no form and no valid route, so this is frequently the first SSTI a scanner finds.

Single-file apps. Projects without a templates directory keep HTML in Python strings. Adding a variable by string formatting, not by context, is one keystroke away.

Flash and status messages. render_template_string("<p>" + message + "</p>"), where message came from a query parameter after a redirect.

User-designed content. Invoice layouts or email bodies that customers edit, rendered with the app's main Jinja environment.

Jinja expressions are far more capable than Django's. An expression can read attributes, index, and call. From any object in reach, an attribute walk leads to Python internals and from there to running code on the server. Flask also adds request, session, g and config to every template context, so an injected {{ config }} discloses the secret key before any escalation.

Vulnerable and fixed

from flask import Flask, render_template_string, request

app = Flask(__name__)


@app.errorhandler(404)
def not_found(error):
    page = f"<h1>Not found</h1><p>No page at {request.path}</p>"
    return render_template_string(page), 404

Keep the source constant and pass the path as a variable. Jinja then treats it as data, and Flask's autoescaping makes it safe to display as well:

from flask import Flask, render_template_string, request

app = Flask(__name__)

NOT_FOUND = "<h1>Not found</h1><p>No page at {{ path }}</p>"


@app.errorhandler(404)
def not_found(error):
    return render_template_string(NOT_FOUND, path=request.path), 404

What Flask and Jinja already do for you

Flask configures Jinja to escape values automatically, which is why passing data through the context is safe for HTML as well as for the template. Jinja ships SandboxedEnvironment for templates written by users: it prevents access to private and internal attributes and raises SecurityError on unsafe access. Its documentation is careful about the limits. The sandbox alone is not a solution for perfect security, templates can still consume large amounts of CPU and memory, and you should pass only the data the template needs. ImmutableSandboxedEnvironment additionally stops templates from modifying lists and dictionaries.

What es-python adds

es-python reports at the call that turns text into a template. For Flask that is one place: render_template_string() and jinja2.Template both go through Environment.from_string, and untrusted text arriving there is recorded as ssti jinja2, tagged ssti and graded Critical for data from the network. The trace names the error handler or view that built the string.

Two behaviours are worth knowing before you read the events. First, the check is on the template source. The fixed handler passes request.path in the context, so nothing is reported; values rendered through {{ }} are data. Second, the event does not wait for a payload. A request for /missing with no braces in it still shows that the path flows into template source, so you find the bug from ordinary traffic. When EyalSec can prove the text held nothing Jinja would read as syntax, the event is suppressed and stays out of the default view.

User-authored templates rendered in a SandboxedEnvironment are still reported, because an outside author is still writing template code; if that is a product feature, mark that call site with a rule. The Block network code execution template, or any Raise rule on ssti jinja2, makes the view fail with a RuntimeError instead of rendering.

Review checklist

  1. Search for render_template_string( and check that the first argument is a constant.
  2. Read every errorhandler function.
  3. Move inline HTML with variables into template files.
  4. Render customer-authored templates in a sandboxed environment with a minimal context.
  5. Never place config, request or model objects in a context for user templates.

References

See it on your own Flask code

es-python runs your application unchanged and reports when request data reaches the operation, with where the data came from and the line that used it. A live demo walks through it on a real application.

EyalSec Pricing Docs Security Research Vulnerabilities Security glossary About Contact עברית Login Book a live demo