python.flask.security.unescaped-template-extension.unescaped-template-extension
Community Favorite

Author
48,348
Download Count*
License
Flask does not automatically escape Jinja templates unless they have .html, .htm, .xml, or .xhtml extensions. This could lead to XSS attacks. Use .html, .htm, .xml, or .xhtml for your template extensions. See https://flask.palletsprojects.com/en/1.1.x/templating/#jinja-setup for more information.
Run Locally
Run in CI
Defintion
rules:
- id: unescaped-template-extension
message: Flask does not automatically escape Jinja templates unless they have
.html, .htm, .xml, or .xhtml extensions. This could lead to XSS attacks.
Use .html, .htm, .xml, or .xhtml for your template extensions. See
https://flask.palletsprojects.com/en/1.1.x/templating/#jinja-setup for
more information.
metadata:
cwe:
- "CWE-79: Improper Neutralization of Input During Web Page Generation
('Cross-site Scripting')"
owasp:
- A07:2017 - Cross-Site Scripting (XSS)
- A03:2021 - Injection
source-rule-url: https://pypi.org/project/flake8-flask/
references:
- https://flask.palletsprojects.com/en/1.1.x/templating/#jinja-setup
- https://blog.r2c.dev/2020/bento-check-unescaped-template-extensions-in-flask/
- https://bento.dev/checks/flask/unescaped-file-extension/
category: security
technology:
- flask
cwe2022-top25: true
cwe2021-top25: true
subcategory:
- audit
likelihood: LOW
impact: MEDIUM
confidence: LOW
license: Commons Clause License Condition v1.0[LGPL-2.1-only]
patterns:
- pattern-not: flask.render_template("=~/.+\.html$/", ...)
- pattern-not: flask.render_template("=~/.+\.xml$/", ...)
- pattern-not: flask.render_template("=~/.+\.htm$/", ...)
- pattern-not: flask.render_template("=~/.+\.xhtml$/", ...)
- pattern-not: flask.render_template($X + "=~/\.html$/", ...)
- pattern-not: flask.render_template($X + "=~/\.xml$/", ...)
- pattern-not: flask.render_template($X + "=~/\.htm$/", ...)
- pattern-not: flask.render_template($X + "=~/\.xhtml$/", ...)
- pattern-not: flask.render_template("=~/.+\.html$/" % $X, ...)
- pattern-not: flask.render_template("=~/.+\.xml$/" % $X, ...)
- pattern-not: flask.render_template("=~/.+\.htm$/" % $X, ...)
- pattern-not: flask.render_template("=~/.+\.xhtml$/" % $X, ...)
- pattern-not: flask.render_template("=~/.+\.html$/".format(...), ...)
- pattern-not: flask.render_template("=~/.+\.xml$/".format(...), ...)
- pattern-not: flask.render_template("=~/.+\.htm$/".format(...), ...)
- pattern-not: flask.render_template("=~/.+\.xhtml$/".format(...), ...)
- pattern-not: flask.render_template($TEMPLATE)
- pattern-either:
- pattern: flask.render_template("...", ...)
- pattern: flask.render_template($X + "...", ...)
- pattern: flask.render_template("..." % $Y, ...)
- pattern: flask.render_template("...".format(...), ...)
languages:
- python
severity: WARNING
Examples
unescaped-template-extension.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/unsafe")
def unsafe():
# ruleid: unescaped-template-extension
return render_template("unsafe.txt", name=request.args.get("name"))
@app.route("/really_unsafe")
def really_unsafe():
name = request.args.get("name")
age = request.args.get("age")
# ruleid: unescaped-template-extension
return render_template("unsafe.txt", name=name, age=age)
@app.route("/no_extension")
def no_extension():
# ruleid: unescaped-template-extension
return render_template("will-crash-without-extension", name=request.args.get("name"))
# Test a bunch at the same time
evil = "<script>alert('blah')</script>"
@app.route("/one")
def one():
# ruleid: unescaped-template-extension
return render_template("unsafe.unsafe", name=evil)
@app.route("/two")
def two():
# ruleid: unescaped-template-extension
return render_template("unsafe.email", name=evil)
@app.route("/three")
def three():
# ruleid: unescaped-template-extension
return render_template("unsafe.jinja2", name=evil)
@app.route("/four")
def four():
# ruleid: unescaped-template-extension
return render_template("unsafe.template", name=evil)
@app.route("/five")
def five():
# ruleid: unescaped-template-extension
return render_template("unsafe.asdlfkjasdlkjf", name=evil)
@app.route("/six")
def six():
# ruleid: unescaped-template-extension
return render_template("unsafe.html.j2", name=evil)
@app.route("no_vars")
def no_vars():
# ok: unescaped-template-extension
return render_template("unsafe.txt")
@app.route("/escaped_extensions")
def escaped_extensions():
# ok: unescaped-template-extension
return render_template("safe.html", name=request.args.get("name"))
@app.route("/concat")
def concat():
# ruleid: unescaped-template-extension
msg.body = render_template(template + '.txt', **kwargs)
# ok: unescaped-template-extension
msg.html = render_template(template + '.html', **kwargs)
# ruleid: unescaped-template-extension
return render_template('%s.txt' % style, **kwargs).replace('<table>', table)
@app.route("/format")
def format():
name = "world"
# ruleid: unescaped-template-extension
return render_template("{}.txt".format("hello"), name)
@app.route("/format-ok")
def format():
name = "world"
# ok: unescaped-template-extension
return render_template("{}.html".format("hello"), name)
from library import render_template
def not_flask():
from library import render_template
# ok: unescaped-template-extension
return render_template("hello.txt")
@app.route("/what_if")
def what_if():
cond = request.args.get("cond")
if cond:
template = "unsafe.txt"
else:
template = "safe.html"
return render_template(template, cond=cond)
# Real-world code
@app.route("/opml")
def opml():
sort_key = flask.request.args.get("sort", "(unread > 0) DESC, snr")
if sort_key == "feed_title":
sort_key = "lower(feed_title)"
order = flask.request.args.get("order", "DESC")
with dbop.db() as db:
rows = dbop.opml(db)
return (
# ruleid: unescaped-template-extension
flask.render_template("opml.opml", atom_content=atom_content, rows=rows),
200,
{"Content-Type": "text/plain"},
)
Short Link: https://sg.run/x1Rg