flawopen.com/xss/Python
Python 中的 XSS 跨站脚本攻击(Flask、Jinja2、Django)
把用户输入包进 Markup、mark_safe,或放进传给 render_template_string 的 f-string,会让访客的文字在其他用户的浏览器里以 JavaScript 身份运行,并带着他们的会话(CWE-79)。本文说明 Flask、Jinja2 和 Django 在哪些地方替你转义、有四个地方不会,以及如何安全地允许富文本。
想象一家带有安全表单的印刷店:填入空白栏的文字会作为普通文本印出。但如果店员在开印前把顾客的便条粘进打印机的指令页,便条就可能变成打印命令。传给 render_template_string 的 Python f-string 正是如此:它把输入插入 Jinja2 源码,早于 autoescape 将其作为变量处理。Markup 和 mark_safe 像是盖上“HTML 已检查”的章,但它们并不会检查内容。让输入保留为模板变量并交给 autoescape 处理;在 Django 中构造小片段则使用 format_html。
核心概念与专有名词
Jinja2 autoescape- Flask 会为
.html、.htm、.xml、.xhtml模板以及render_template_string开启 Jinja2 自动转义,因此{{ q }}会变成<img ...>。独立创建的jinja2.Environment()默认关闭自动转义,除非传入autoescape=select_autoescape()。 Markup / |safe / mark_safe- 这三者都会把字符串标记为已经安全的 HTML,模板引擎便不再转义它。它们只是开发者的承诺,不是净化器:
Markup(user_input)或mark_safe(user_input)会把用户输入的任何内容原样发布。 render_template_string(f"...")- f-string 会在 Jinja2 解析之前就把值放进模板源码,所以自动转义永远不会把它当作变量看待。同一行代码还构成服务端模板注入:像
{{ 7*7 }}这样的值会被 Jinja2 求值。 django.utils.html.format_html- 像
str.format一样构造 HTML,但会先用conditional_escape转义每个参数,再把结果标记为安全。这是 Django 推荐的在 Python 代码中构造小段 HTML 的方式,用来替代mark_safe(f"...")。 nh3.clean()- 富文本需要的是净化器,而不是转义。
nh3.clean()(Rust 的 ammonia)维护标签和属性的白名单,删除<script>、事件处理器和javascript:URL。Bleach 已停止维护,nh3 是仍在维护的继任者。
框架映射
- 本指南所述弱点的 CWE 参考: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
攻击执行流程分解
输入随请求到达
搜索框、个人资料表单或链接参数把 q、bio 或 next 发到服务器。攻击者可以给受害者发送精心构造的链接,或者该值被保存下来并展示给每个查看该资料的人。
值绕过了转义
f-string 把它放进模板源码,Markup() 或 mark_safe() 把它标记为可信,或者它落入 href,而在那里只转义 < 和 > 是不够的。无害的测试值 <img src=x onerror=alert(1)> 原封不动地通过。
浏览器把它解析为标记
响应中包含一个真实的 <img src=x onerror=alert(1)> 元素。如果图片资源加载失败,就会触发 onerror,链接中的 javascript: URL 在点击时执行。
脚本以网站的身份运行
在所示的 Flask/Jinja 和 Django 路径中,输出的标记会在应用源下运行。将不可信值放在自动转义的模板数据位置;只有经过符合上下文的净化后才允许 HTML。
源代码对比:漏洞与安全实现
# views.py:下面每个处理函数都让请求文本以 HTML 形式进入页面
from django.utils.safestring import mark_safe
from flask import Flask, render_template_string, request
from markupsafe import Markup
app = Flask(__name__)
# f-string 构造的是模板源码,所以 Jinja2 永远不会转义 q
@app.get("/search")
def search():
q = request.args.get("q", "")
return render_template_string(f"<h1>Results for {q}</h1>")
# Markup() 告诉 Jinja2 这段 bio 是可信的 HTML
@app.get("/profile")
def profile():
bio = request.args.get("bio", "")
return render_template_string("<div class='bio'>{{ bio }}</div>", bio=Markup(bio))
# 对 f-string 使用 mark_safe 会把 name 当作 HTML 发布
def greeting(name):
return mark_safe(f"<p>Hello, {name}</p>")
# 自动转义拦不住 href 中的 javascript: URL
@app.get("/continue")
def continue_link():
url = request.args.get("next", "/")
return render_template_string('<a href="{{ url }}">Continue</a>', url=url)
# views.py:值始终是模板变量;富文本经过净化
import nh3
from django.utils.html import format_html
from flask import Flask, render_template_string, request
app = Flask(__name__)
# q 是模板变量,所以自动转义会对它编码
@app.get("/search")
def search():
q = request.args.get("q", "")
return render_template_string("<h1>Results for {{ q }}</h1>", q=q)
# 在信任这段 HTML 之前,nh3 先按标签白名单过滤
@app.get("/profile")
def profile():
bio = request.args.get("bio", "")
return render_template_string("<div class='bio'>{{ bio|safe }}</div>", bio=nh3.clean(bio))
# format_html 先转义每个参数,再把结果标记为安全
def greeting(name):
return format_html("<p>Hello, {}</p>", name)
# 只接受本站路径作为链接目标
@app.get("/continue")
def continue_link():
url = request.args.get("next", "/")
if not url.startswith("/") or url.startswith("//"):
url = "/"
return render_template_string('<a href="{{ url }}">Continue</a>', url=url)
工程与系统安全加固清单
- ✓遇到 Bandit B703/B308(
mark_safe)和 B704(对非字面量使用Markup),以及第一个参数是 f-string、%或.format()结果的render_template_string时让 CI 失败。 - ✓以变量的形式把值传给模板,交给自动转义处理;独立使用 Jinja2 时,创建
Environment(autoescape=select_autoescape())。 - ✓构造小段 HTML 时用
format_html()替代mark_safe(),所有用户提供的 HTML 都经过nh3.clean()。 - ✓所有用户可控的 URL 在进入
href或src之前都要校验:只接受以单个/开头的本站路径,或明确的 http/https 白名单。 - ✓发送基于 nonce 的
script-src、且不含'unsafe-inline'的 Content-Security-Policy,并把会话 cookie 设为HttpOnly和SameSite=Lax。
参考来源
- CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
- Flask — Security Considerations: Cross-Site Scripting
- Django — Security: Cross site scripting (XSS) protection
- Django — format_html
- nh3 — Python bindings to the ammonia HTML sanitizer
- OWASP Cross Site Scripting Prevention Cheat Sheet
- nh3 — Python bindings to the ammonia HTML sanitizer