# PYTHON-FLASK-AUDIT-008: Flask render_template_string Usage

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

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

## Description

This rule detects any use of render_template_string() or flask.render_template_string() in
Flask applications. Unlike render_template(), which loads templates from files on disk,
render_template_string() compiles and renders a Jinja2 template from a Python string passed
at runtime. This creates a structural risk: if any portion of that string originates from
user input, the application has a Server-Side Template Injection (SSTI) vulnerability.

SSTI in Jinja2 is critical-severity. Jinja2 templates have access to Python's object model,
and attackers can use template expressions like {{ config }} to leak configuration, or
{{ ''.__class__.__mro__[1].__subclasses__() }} to traverse the class hierarchy and reach
os.system or subprocess for remote code execution.

This is an audit-grade rule. Not every render_template_string() call is vulnerable -- if the
template string is a hardcoded literal with no user-controlled components, there is no SSTI
risk. However, every use of render_template_string() is worth reviewing to confirm that the
template string is fully controlled by the developer and never interpolated with user data.

The detection uses Or(calls("render_template_string"), calls("flask.render_template_string"))
to catch both the directly imported function and the module-qualified call form.


## Vulnerable Code

```python
from flask import render_template_string

render_template_string("<h1>hello</h1>")
```

## Secure Code

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

app = Flask(__name__)

# SAFE: Use render_template() with a static template file.
# The template file is on disk under templates/ and never constructed from user input.
@app.route('/greet')
def greet():
    name = request.args.get('name', 'World')
    # User input is passed as a context variable, not embedded in the template string.
    # Jinja2 auto-escaping handles HTML encoding of 'name' in the template file.
    return render_template('greet.html', name=name)

# templates/greet.html:
# <h1>Hello, {{ name }}!</h1>  <- Jinja2 auto-escapes 'name' here

# UNSAFE (do not do this):
# return render_template_string(f"<h1>Hello, {name}!</h1>")  # SSTI risk
# return render_template_string("<h1>Hello, {{ name }}!</h1>", name=name)  # safe but risky pattern

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, Or, QueryType


@python_rule(
    id="PYTHON-FLASK-AUDIT-008",
    name="Flask render_template_string Usage",
    severity="MEDIUM",
    category="flask",
    cwe="CWE-1336",
    tags="python,flask,template,ssti,audit,CWE-1336",
    message="render_template_string() detected. Prefer render_template() with separate template files.",
    owasp="A03:2021",
)
def detect_flask_render_template_string():
    """Audit: Detects any usage of render_template_string()."""
    return Or(
        calls("render_template_string"),
        calls("flask.render_template_string"),
    )
```

## How to Fix

- Replace render_template_string() with render_template() using a static HTML file in the templates/ directory. User input is passed as context variables, not embedded in the template.
- If you must use render_template_string() for dynamically constructed templates (e.g., email templates stored in a database), ensure the template string comes from a trusted internal source and is never derived from user input, even partially.
- Audit every render_template_string() call to trace the origin of the template argument. If any f-string interpolation, .format(), or string concatenation with a user-derived value is present, the call is vulnerable.
- Enable Jinja2 sandboxing (SandboxedEnvironment) if you genuinely need to render user-provided template content.
- Add automated SSTI testing to your security test suite for any endpoint that uses render_template_string().

## Security Implications

- **Server-Side Template Injection Leading to Remote Code Execution:** If user input is included in the template string passed to render_template_string(),
an attacker can inject Jinja2 expressions that traverse Python's object hierarchy to
reach os.system, subprocess.Popen, or other code execution primitives. This is a
critical-severity vulnerability with widespread exploitation.

- **Configuration and Secret Leakage via Template Expressions:** Even without achieving code execution, an attacker can inject {{ config }} or
{{ config.SECRET_KEY }} to extract Flask's full configuration dictionary from the
running application, including secret keys, database URIs, and API credentials.

- **File System Read Access:** Jinja2 template injection can be chained to read arbitrary files: by reaching Python's
open() built-in through the class hierarchy, an attacker can read /etc/passwd,
application source code, or private key files.

- **Harder to Audit Than File-Based Templates:** Template strings defined in Python code are harder to audit than template files in
a dedicated directory. Security reviewers scanning the templates/ directory for unsafe
patterns will miss vulnerabilities in render_template_string() calls in Python source.


## FAQ

**Q: Is render_template_string() always vulnerable?**

No. If the template string is a hardcoded literal with no user-controlled components --
for example, render_template_string("<h1>Hello!</h1>") -- there is no SSTI risk. The
vulnerability only arises when user-controlled data appears inside the template string
itself (via f-strings, .format(), or concatenation). This rule flags all uses for
review; you must inspect each call to determine if user input can reach the template
string.


**Q: Why flag render_template_string() at all if it can be safe?**

render_template_string() is a pattern that grows riskier over time. Code starts safe
(hardcoded template), then someone adds f-string interpolation for convenience, and
SSTI is introduced. Auditing every use at review time prevents the safe-to-unsafe
drift. There is almost always a better alternative (render_template() with a file),
and the few legitimate uses of render_template_string() deserve explicit documentation
of why they are safe.


**Q: What does a Jinja2 SSTI attack payload look like?**

A basic payload to leak configuration: {{ config.SECRET_KEY }}. A payload to reach
code execution: {{ ''.__class__.__mro__[1].__subclasses__()[X]('id', shell=True,
stdout=-1).communicate() }}. The exact subclass index varies by Python version and
installed packages, but automated SSTI tools enumerate it quickly.


**Q: I store email templates in a database and render them with render_template_string(). What should I do?**

If the template content comes from an internal, trusted source (e.g., an admin-managed
database table with no user-editable content), document that clearly and consider
suppressing the finding with an inline comment. If users can edit template content,
use Jinja2's SandboxedEnvironment to restrict what expressions can be evaluated, and
treat all template content from the database as untrusted.


**Q: How do I run this rule in CI/CD?**

Run: pathfinder ci --ruleset python/flask/PYTHON-FLASK-AUDIT-008 --project .
The rule outputs SARIF, JSON, or CSV and can post inline pull request comments on GitHub.


**Q: What is the difference between this audit rule and a taint-analysis SSTI rule?**

This audit rule flags every render_template_string() call regardless of whether user
input reaches it -- it is a broad signal for manual review. A taint-analysis SSTI rule
would trace user input from Flask request parameters through function calls until it
reaches the template string argument of render_template_string(), producing only
confirmed-vulnerable findings. Both approaches are valuable: the audit rule catches
risky patterns early; the taint rule confirms exploitability.


## References

- [CWE-1336: Improper Neutralization of Directives in Statically Saved Code](https://cwe.mitre.org/data/definitions/1336.html)
- [OWASP Server-Side Template Injection](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server-Side_Template_Injection)
- [Flask render_template_string Documentation](https://flask.palletsprojects.com/en/stable/api/#flask.render_template_string)
- [PortSwigger SSTI Research](https://portswigger.net/research/server-side-template-injection)
- [Jinja2 Sandbox Documentation](https://jinja.palletsprojects.com/en/stable/sandbox/)
- [OWASP A03:2021 Injection](https://owasp.org/Top10/A03_2021-Injection/)

---

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