Flask XSS Protection Disabled
ID |
python.flask_xss_protection_disabled |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Misconfiguration |
Language |
Python |
Tags |
ASVS50:v1.3.1, CWE:80, NIST.SP.800-53, OWASP:2025:A02, OWASP:2025:A05, PCI-DSS:6.5.7, flask |
Description
Improper configurations or misuse of Flask’s templating system can disable XSS protection, exposing applications to cross-site scripting attacks.
Rationale
Cross-Site Scripting (XSS) is a vulnerability that occurs when an application includes untrusted data in a web page without proper validation or escaping.
Templating systems are designed with built-in protections to prevent cross-site scripting (XSS) vulnerabilities. These protections automatically escape HTML input, ensuring that user-supplied content is not executed as code on the client side.
However, developers may inadvertently disable these protections by marking content as "safe" or bypassing the template engine, increasing the risk of XSS attacks. Some common misconfigurations and practices can lead to such vulnerabilities:
Markup
Using Markup in Flask to handle HTML content requires careful attention to prevent XSS vulnerabilities. Markup is used to mark a string as safe, meaning it should not be escaped when rendered. This is useful for trusted content but can be dangerous with user-generated input.
from flask import Flask, render_template_string, Markup
app = Flask(__name__)
@app.route('/')
def home():
# Mock potentially dangerous user input
user_input = "<script>alert('XSS');</script>"
# Dangerous: Marking untrusted content as safe (do not do this)
content = Markup(user_input)
# Rendering a simple template with the unsafe content
template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Unsafe Example</title>
</head>
<body>
<!-- Directly rendering potentially dangerous content -->
<div>{{ content }}</div>
</body>
</html>
"""
return render_template_string(template, content=content)
if __name__ == '__main__':
app.run(debug=True)
What is not reported
Markup over text the author wrote suppresses no escaping decision, so it is not reported. That covers a
literal, a translated literal, and any concatenation or format of those:
Markup('<code>100,200,300</code>') # not reported
Markup(_('Assigned tagged VLAN IDs, encased with double quotes')) # not reported
Markup(_('Example:') + ' <code>"100,200,300"</code>') # not reported
It also covers an expression that escapes every value it interpolates — an f-string or a format whose
dynamic parts all pass through escape or conditional_escape — except where HTML escaping is not the
protection that matters. A value opening an href, src, action or style, or landing inside a
<script> element or an event-handler attribute, is reported even when escaped, because escaping leaves
javascript:alert(1) untouched:
Markup(f'<p>{escape(comment)}</p>') # not reported
Markup(f'<a href="{escape(target)}">go</a>') # reported: target can be a javascript: URI
The test is on the expression, and a local name is never resolved: Markup(msg) is reported even when
msg was built from escaped parts, because proving that needs the local data flow and the cost of being
wrong is a missed XSS. A module-level constant the file assigns exactly once is resolved, and only that.
This mirrors python.django_xss_protection_disabled, which documents the shapes in full.
render_template With Unescaped File Extension
To render a template without automatically escaping content, you can use custom file extensions or adjust Jinja environment settings to disable auto-escaping. Below is an example using a custom file extension:
from flask import Flask, render_template
app = Flask(__name__)
# Mock potentially dangerous user input
user_input = "<script>alert('XSS');</script>"
@app.route('/')
def home():
return render_template("unsafe.customext", user_input=user_input)
if __name__ == '__main__':
app.run(debug=True)