Template Rendering in Flask
In Flask, rendering HTML templates is done using the render_template() function with Jinja2 templating engine.
This allows you to create dynamic web pages by injecting variables and control structures into your HTML.
How it works?
Flask looks for templates inside a folder named templates.
your_app/
│
├── app.py
└── templates/
├── index.html
└── user.html
Index.html template
<!doctype html>
<title>Home</title>
<h1>Hello, {{ name }}!</h1>
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('index.html', name='Alice')
if __name__ == '__main__':
app.run(debug=True)
Jinja2 Features in Templates
Loops
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
Conditionals
{% if user %}
<p>Welcome, {{ user }}</p>
{% else %}
<p>Please log in.</p>
{% endif %}
Other ways to render templates
Render template using render_template_string()
- Renders a template from a string, not a file
- Useful for quick testing or dynamic template generation
from flask import render_template_string
html = "<h1>Hello {{ name }}!</h1>"
return render_template_string(html, name='Bob')
Why template rendering matters from an offensive security perspective
There is a hard line between the two functions on this page, and it is the single most important thing to internalize.
render_template('index.html', name=user_input) is safe. The template is a fixed file; name is data passed into it, and Jinja2 auto-escapes it on the way out. The user controls a value, never the template.
render_template_string(user_input) is where it goes wrong. The first argument is the template source. If any attacker-controlled string reaches it, the attacker is writing Jinja2, not filling in a blank. Jinja2 expressions are evaluated, so {{ 7*7 }} renders as 49 and {{ ... }} over Python’s object graph reaches arbitrary code execution. That is server-side template injection (SSTI), and it is one of the highest-impact bugs in Flask apps.
# DANGEROUS: user input concatenated into the template SOURCE.
name = request.args.get("name")
return render_template_string(f"<h1>Hello {name}!</h1>") # ?name={{7*7}} -> 49 -> RCE
The tell to grep for is any render_template_string whose argument is built from request data with an f-string, +, .format(), or %. The same trap appears anywhere a template engine compiles a string built from user input, including AI prompt templates. The full exploitation chain, from {{7*7}} to a shell through __subclasses__(), is in Server Side Template Injection (SSTI) in Flask Application and Python Jinja2 Server Side Template Injection; the prompt-template variant is in Server Side Template Injection (SSTI) in AI Prompt Templates.
Mitigation
Render fixed template files with render_template and pass user input as named data, so the template source is always something you wrote and Jinja2’s autoescaping handles the values on the way out. Reserve render_template_string for templates that are entirely static and developer-authored, and never build its first argument from request data through an f-string, +, .format(), or %. If you genuinely need user-driven formatting, keep it out of the template engine entirely and substitute values with ordinary string methods that do not evaluate expressions.
from flask import render_template
@app.route("/")
def home():
name = request.args.get("name", "Guest")
return render_template("index.html", name=name) # name is data, template source is fixed