# PYTHON-FLASK-SEC-014: Flask Server-Side Template Injection (SSTI)

> **Severity:** CRITICAL | **CWE:** CWE-1336 | **OWASP:** A03:2021

- **Language:** Python
- **Category:** Flask
- **URL:** https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-SEC-014
- **Detection:** `pathfinder scan --ruleset python/PYTHON-FLASK-SEC-014 --project .`

## Description

This rule detects Server-Side Template Injection (SSTI) in Flask applications where
user-controlled input from HTTP request parameters flows into render_template_string()
as part of the template source string. Unlike render_template() which loads templates
from files, render_template_string() compiles and renders a Jinja2 template from
a string argument at request time. When user input is embedded in that template
string before rendering, attackers inject Jinja2 template directives that execute
arbitrary Python code on the server.

SSTI in Jinja2 is especially powerful because Jinja2 templates have access to Python's
object model. Classic SSTI payloads traverse the class hierarchy:
  {{''.__class__.__mro__[1].__subclasses__()}}
to find and instantiate classes that provide file access, subprocess execution, and
network connectivity. More targeted payloads reach os.system() or subprocess.Popen()
directly through the template's global context.

The vulnerability occurs when developers use render_template_string() to dynamically
compose templates (e.g., inserting a user-supplied greeting into a template string
before rendering). The correct pattern is to pass user data as keyword arguments to
render_template_string() or render_template() where Jinja2 handles it as data, not
as template syntax: render_template_string('<h1>{{ name }}</h1>', name=user_name).

The taint analysis traces data from Flask request sources through string operations
and function calls to the template string argument of render_template_string() at
position 0. There are no recognized sanitizers because there is no transformation
that makes user input safe to use as Jinja2 template syntax.


## Vulnerable Code

```python
# --- file: app.py ---
from flask import Flask, request
from renderer import render_greeting

app = Flask(__name__)


@app.route('/greet')
def greet():
    name = request.args.get('name')
    html = render_greeting(name)
    return html

# --- file: renderer.py ---
from flask import render_template_string


def render_greeting(username):
    template = "<h1>Hello " + username + "</h1>"
    return render_template_string(template)
```

## Secure Code

```python
from flask import Flask, request, render_template, render_template_string

app = Flask(__name__)

@app.route('/hello')
def hello():
    name = request.args.get('name', 'World')
    # SAFE Option 1: render_template() with a file-based template
    # User data is passed as a variable, never in the template string
    return render_template('hello.html', name=name)

@app.route('/greet')
def greet():
    name = request.args.get('name', 'World')
    # SAFE Option 2: render_template_string() with user data as a VARIABLE
    # The template string is hardcoded; {{ name }} is a Jinja2 variable reference
    # that receives the safe escaped value -- it cannot contain template directives
    return render_template_string('<h1>Hello, {{ name }}!</h1>', name=name)

# NEVER do this:
# template = f'<h1>Hello, {name}!</h1>'  # name injected INTO template source
# return render_template_string(template)  # now name is template syntax

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, flows, QueryType
from codepathfinder.presets import PropagationPresets

class FlaskModule(QueryType):
    fqns = ["flask"]


@python_rule(
    id="PYTHON-FLASK-SEC-014",
    name="Flask Server-Side Template Injection",
    severity="CRITICAL",
    category="flask",
    cwe="CWE-1336",
    tags="python,flask,ssti,template-injection,rce,OWASP-A03,CWE-1336",
    message="User input flows to render_template_string(). Use render_template() with separate template files.",
    owasp="A03:2021",
)
def detect_flask_ssti():
    """Detects Flask request data flowing to render_template_string()."""
    return flows(
        from_sources=[
            calls("request.args.get"),
            calls("request.form.get"),
            calls("request.values.get"),
            calls("request.get_json"),
        ],
        to_sinks=[
            FlaskModule.method("render_template_string").tracks(0),
            calls("render_template_string"),
        ],
        sanitized_by=[],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Never interpolate user input into the template string before passing it to render_template_string() -- always keep the template string as a hardcoded literal.
- Pass user-supplied values as keyword arguments to render_template() or render_template_string() -- Jinja2 treats these as data values and applies autoescaping, not as template syntax.
- Use render_template() with file-based templates (.html files in the templates/ directory) as the default pattern -- file-based templates cannot be modified by request parameters at runtime.
- If dynamic template generation is required (e.g., CMS-style templates stored in the database), use Jinja2's SandboxedEnvironment which restricts access to dangerous attributes and built-ins.
- Audit all render_template_string() calls and verify the template string argument is a hardcoded literal with no request parameter interpolation.

## Security Implications

- **Full Remote Code Execution via Jinja2 Object Traversal:** Jinja2's template syntax provides access to Python objects and their attributes.
A minimal SSTI payload like {{7*7}} confirms the injection. A full RCE payload
traverses __class__.__mro__[1].__subclasses__() to find Popen or subprocess.check_output
and executes arbitrary OS commands. The template engine runs with the same privileges
as the Flask application, meaning a successful SSTI compromises the entire server.

- **Secret Key and Credential Extraction:** Jinja2 templates have access to the application's config object through the
g, app, and config template globals. An attacker can extract Flask's SECRET_KEY,
database connection strings, API keys, and OAuth secrets directly from the
template context: {{config.SECRET_KEY}} or {{config['DATABASE_URL']}}.
These can be used to forge session cookies, access databases, and impersonate
the application to external services.

- **Authentication Bypass via Forged Session Cookies:** Flask's session is signed with SECRET_KEY using itsdangerous. An attacker who
extracts SECRET_KEY via SSTI can forge arbitrary session cookies, impersonating
any user including administrative accounts. This turns an SSTI finding into a
complete authentication bypass without needing separate session vulnerabilities.

- **Persistent Backdoor via Module Modification:** SSTI payloads can modify Python modules loaded in the current interpreter.
An attacker can inject code that patches application functions in memory,
creating a persistent backdoor in worker processes that survives across
requests without touching the filesystem.


## FAQ

**Q: render_template_string() is useful for dynamic templates. Can I use it safely?**

Yes, if you keep the template string hardcoded and pass user data as keyword arguments.
render_template_string('<h1>{{ name }}</h1>', name=user_name) is safe because
user_name is a template variable, not template syntax. Jinja2 autoescaping applies
to {{ name }} and user data cannot inject {{ }} directives. The vulnerability is
when you do render_template_string(f'<h1>{user_name}</h1>') -- the user input
becomes part of the template source before rendering.


**Q: Does Jinja2's SandboxedEnvironment fix SSTI?**

SandboxedEnvironment restricts access to dangerous attributes (__class__,
__mro__, etc.) and Python built-ins within templates. It significantly raises
the bar for exploitation and prevents most known SSTI payloads. However, sandbox
escapes have been found in Jinja2's sandbox in the past. For production use,
do not pass user data as template source -- use SandboxedEnvironment only if
user-defined templates are a required feature and you accept the residual risk.


**Q: Why is SSTI rated CRITICAL? Is it actually exploitable?**

SSTI in Jinja2 is consistently rated Critical because exploitation is reliable
and well-documented. Payloads that reach os.system() via __subclasses__() traversal
work in Flask/Jinja2 without requiring environment-specific knowledge. The Portswigger
SSTI tutorial demonstrates the technique step by step. Bug bounty programs
consistently award maximum payouts for confirmed SSTI findings.


**Q: We use render_template_string() in error handlers with hardcoded templates. Is that flagged?**

No. The rule only flags flows where the first argument to render_template_string()
is tainted from a Flask request source. Hardcoded template strings like
render_template_string('<h1>Error: {{ message }}</h1>', message=error_msg) -- where
error_msg comes from the application itself, not from request parameters -- do not
create a tainted flow and will not trigger the rule.


**Q: Our CMS stores user-authored templates in the database and renders them. How do we handle this?**

Use Jinja2's SandboxedEnvironment and restrict available filters and globals to
a safe subset. Treat template authors as untrusted users. Require template approval
before publishing. Consider using a restricted template language (Liquid, Mustache,
or a custom DSL) instead of Jinja2 for user-authored content -- these languages
do not have access to the Python object model.


**Q: Does this rule catch SSTI in template strings built across multiple lines?**

Yes. The taint analysis follows the value through multi-step string construction:
assignments, f-string builds, string concatenation, .format() calls, and joins.
If user input from request.args.get() is incorporated into a string that eventually
becomes the first argument to render_template_string(), the full chain is flagged
regardless of how many intermediate steps are involved.


**Q: How do I test for SSTI in a confirmed finding?**

Provide {{7*7}} as the parameter value. If the response contains 49 (not the
literal string {{7*7}}), template injection is confirmed. Then try
{{''.__class__}} -- if it returns <class 'str'>, Jinja2 attribute access is
available. From there, use the standard Jinja2 SSTI payload chain to verify
command execution. Document the full chain before reporting.


## References

- [CWE-1336: Static Code Injection / Template Injection](https://cwe.mitre.org/data/definitions/1336.html)
- [OWASP Server-Side Template Injection](https://owasp.org/www-community/attacks/Server_Side_Template_Injection)
- [Jinja2 SandboxedEnvironment documentation](https://jinja.palletsprojects.com/en/latest/sandbox/)
- [Flask render_template_string documentation](https://flask.palletsprojects.com/en/latest/api/#flask.render_template_string)
- [Portswigger SSTI Tutorial](https://portswigger.net/web-security/server-side-template-injection)
- [SSTI payload reference for Jinja2](https://pequalsnp-team.github.io/cheatsheet/flask-jinja2-ssti)

---

Source: https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-SEC-014
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
