PyFu

Template Rendering in Flask

Python Web Development Frameworks

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