Bento check: Flask template files that aren’t autoescaped by default

by Grayson Hardaway

> get this check now in Bento: pip3 install bento-cli && bento init

Get this check in Semgrep:

brew install semgrep && \
semgrep --config "https://semgrep.dev/p/python-flask"

Sometimes... Jinja templates will bite you.

Some of us at r2c occasionally have a personal coding project going. A few months ago, I built a classified ads-style web application for splitting the cost of rooms at an upcoming event I'm planning. I chose to write the app using Flask to get inspiration for the custom Flask checks going in to our program analysis tool, Bento.

The app itself was simple, consisting of create, read, update, and delete routes for room-sharers, using DynamoDB for storage. It used only two templates — a landing page template with all the ”ads“ and an email template that included edit and delete links, similar to how Craigslist works.

Meanwhile at r2c, checking the security of Jinja templates was highly requested. Jinja templates can be scary, especially given that autoescaping is not enabled by default. Flask, which internally uses Jinja, enables autoescaping to mitigate cross-site scripting (XSS) attacks. However, this line in the Flask documentation gave me a shock:

Unless customized, Jinja2 is configured by Flask as follows: autoescaping is enabled for all templates ending in .html, .htm, .xml as well as .xhtml when using render_template().

Huh. My landing page template had a .html extension, but my email template had a .txt extension...

XSS in my Flask app due to an unescaped template file extension

Welp. 😐 Since the mail library I used was sending an HTML email, it turns out I could XSS my little app. And so, allow me to introduce this check which looks for calls to render_template() that don't use one of the four valid extensions!

The check will detect these cases:

# Unescaped template extension
@app.route("/unsafe")
def unsafe():
    return render_template("unsafe.txt", name=request.args.get("name"))
# Unescaped template extension
@app.route("/another_unsafe")
def another_unsafe():
    return render_template("unsafe.jinja2", name=request.args.get("name"))

The check considers these cases acceptable:

# Autoescaped template extension
@app.route("/safe")
def safe():
    return render_template("safe.html", name=request.args.get("name"))
# No variables
@app.route("no_vars")
def no_vars():
    return render_template("unsafe.txt")

As always, we use our program analysis platform to see what the check looks like when run over real code. Of the 715 Flask apps on GitHub we used as our test set, the check fired on 13 repos. Here's a breakdown of the template extensions which fired:

Extension Number of repos
None 2
.j2 3
.jinja2 2
.js 2
.jade 1
.mail 1
.svg 1
.txt 1

Of these, one was a typo, and at least one was vulnerable to XSS. We submitted pull requests to the owners to fix these issues.

You can check your own codebase right now with Bento:

$ pip3 install bento-cli && bento init

References