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

Community Favorite
profile photo of semgrepsemgrep
Author
48,348
Download Count*

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://semgrep.dev/blog/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]
      vulnerability_class:
        - Cross-Site-Scripting (XSS)
    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"},
        )