- SAST
- 应用安全
【免费下载链接】bandit
Bandit is a tool designed to find common security issues in Python code.
Bandit 的 B703 检测插件(django_mark_safe)专门针对 Django 模板安全中的高频误用场景:当代码通过django.utils.safestring模块的mark_safe、SafeText、SafeString等 API 将动态内容标记为"安全 HTML"时,若传入内容并非可信的字符串字面量,就可能引入跨站脚本(XSS)漏洞。本指南基于 Bandit 仓库中 django_xss.py 的源码实现与其配套文档 b703_django_mark_safe.rst,完整讲解该插件的检测范围、判定逻辑、输出格式与实际使用方法,帮助你在 CI 流程中落地 Django 项目的 XSS 静态扫描。
插件定位:B7xx XSS 检测家族的一员
Bandit 将检测插件按 ID 分组管理,在 doc/source/plugins/index.rst 中可以看到:
| ID 分组 | 描述 |
|---|---|
| B1xx | misc tests(杂项测试) |
| B2xx | application/framework misconfiguration(应用/框架错误配置) |
| B3xx | blacklists (calls)(调用黑名单) |
| B4xx | blacklists (imports)(导入黑名单) |
| B5xx | cryptography(密码学) |
| B6xx | injection(注入) |
| B7xx | XSS |
B703 属于 B7xx XSS 家族,与 B701(Jinja2 模板自动转义关闭)、B702(Mako 模板使用)、B704(MarkupSafe Markup 构造 XSS)等插件共同构成 Bandit 对模板/标记类 XSS 问题的检测面。该插件在仓库中的注册方式位于 setup.cfg 的 entry point 配置:
bandit.plugins = ... # bandit/plugins/django_xss.py django_mark_safe = bandit.plugins.django_xss:django_mark_safe ...即插件名为django_mark_safe,实现函数为bandit.plugins.django_xss.django_mark_safe。
检测原理:识别 safestring 的可疑调用
插件核心函数定义在 bandit/plugins/django_xss.py,其触发条件分两层:
- 模块导入前置条件:
context.is_module_imported_like("django.utils.safestring"),即当前文件中必须导入了django.utils.safestring模块(from django.utils import safestring等写法均可被识别); - 受影响的调用名单:
context.call_function_name命中以下任一函数名:
affected_functions = [ "mark_safe", "SafeText", "SafeUnicode", "SafeString", "SafeBytes", ]也就是说,只要检测到对上述任一 API 的调用(Call 节点),插件就会取第一个实参(context.node.args[0])进入风险分析流程。
为什么 mark_safe 危险
mark_safe的语义是"声明该字符串为安全内容,无需转义"。它原本用于把已知安全的静态 HTML 片段交给模板渲染;但如果把来自用户输入、数据库、外部接口的不可信字符串也标记为 safe,Django 模板引擎将跳过转义,把其中的<script>、onerror=等载荷原样渲染到页面,形成存储型或反射型 XSS。B703 的任务正是在静态层面判断:mark_safe收到的参数是否可证明是安全的。
风险判定逻辑:安全与不安全如何划分
插件不会对每个mark_safe调用无脑告警,而是先做一轮"确定性过滤":
xss = context.node.args[0] if not ( isinstance(xss, ast.Constant) and isinstance(xss.value, str) ): return check_risk(context.node)- 若第一个实参是字符串字面量(如
mark_safe('<b>Hello</b>')),插件认为内容可信,不报问题; - 若实参是变量、函数调用、格式化表达式等动态值,则进入
check_risk做深度分析。
check_risk 的三类实参分析
check_risk(bandit/plugins/django_xss.py)根据实参的 AST 节点类型分三种路径:
1. 变量(ast.Name)
向上回溯到最近的模块或函数定义层(_bandit_parent链),先判断该变量是否为函数参数:
is_param = False if isinstance(parent, ast.FunctionDef): for name in parent.args.args: if name.arg == xss_var.id: is_param = True break函数参数一律视为不安全——因为参数由调用方传入,无法在函数体内证明其来源。之后调用evaluate_var在函数体内做赋值追溯,看该变量此前是否被赋值为可证明安全的值。
2. 函数调用(ast.Call)
调用evaluate_call分析,目前主要支持对字符串常量调用format()的形式(如'<b>{}</b>'.format(x))。源码中明确以TODO标注了当前限制:带关键字参数的format调用(call.keywords)会被直接判定为"不评估",即走不安全路径:
if ( isinstance(call.func.value, ast.Constant) and call.func.attr == "format" ): evaluate = True if call.keywords: evaluate = False # TODO(??) get support for thisevaluate_call会逐个检查format的位置参数:字符串字面量安全;变量需递归调用evaluate_var验证;嵌套调用需递归evaluate_call;*[...]展开形式(ast.Starred)会把列表元素并入参数继续分析。只有全部参数都被证明安全时才判定整个调用安全。
3. 二元运算 / 格式化表达式(ast.BinOp)
针对'<b>%s</b>' % var这类%格式化,transform2call(bandit/plugins/django_xss.py)会把它等价转换为format()调用:
- 左侧必须是字符串字面量,运算符必须是取模(
%,AST 中为ast.Mod); - 右侧是元组时取其元素作为参数,否则单值作为参数;
- 转换后交给
evaluate_call走同一套安全判定。
变量追溯的核心:DeepAssignation
evaluate_var依赖DeepAssignation类(bandit/plugins/django_xss.py)做轻量级数据流分析,它在函数/模块体内按行号顺序遍历语句,寻找目标变量的赋值点,并支持以下控制流结构:
| AST 节点 | 处理方式 |
|---|---|
Assign | 直接赋值:目标为变量或元组解包(text, url = ...)时记录赋值来源 |
AugAssign | 增量赋值(+=)记录右侧来源 |
If / For / While | 同时检查body与orelse分支,任一分支不安全则不安全 |
Try / ExceptHandler | 依次检查body、handlers、orelse、finalbody四个部分 |
With | 检查with ... as var绑定,以及语句体内赋值 |
FunctionDef | 若目标变量是函数参数,则判定不受外部赋值影响(保持不安全) |
对每个赋值来源再递归评估:字符串字面量 → 安全;变量 → 递归追溯;调用 → 交给 evaluate_call;其他(如文件读取、网络返回值)→ 不安全。只有在所有可能分支都被证明安全时,变量才被认定为安全。
报告输出:字段与严重级别
当插件判定不安全时,会返回一个bandit.Issue:
return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, cwe=issue.Cwe.BASIC_XSS, text=description, )对应到终端输出的形态(源码 docstring 中的示例):
>> Issue: [B703:django_mark_safe] Potential XSS on mark_safe function. Severity: Medium Confidence: High CWE: CWE-80 (https://cwe.mitre.org/data/definitions/80.html) Location: examples/mark_safe_insecure.py:159:4 More Info: https://bandit.readthedocs.io/en/latest/plugins/b703_django_mark_safe.html 158 str_arg = 'could be insecure' 159 safestring.mark_safe(str_arg)各字段含义:
- Severity(严重性)= Medium:XSS 属于常见但非直接 RCE 的中等级别风险;
- Confidence(置信度)= HIGH:调用点明确命中受影响的 API 名单,判定可信度高;
- CWE = CWE-80:即 "Improper Neutralization of Script-Related HTML Tags in a Web Page (Basic XSS)",对应常量定义在 bandit/core/issue.py(
BASIC_XSS = 80); - text:固定描述
"Potential XSS on mark_safe function."。
实际运行:命令与输出示例
在仓库根目录执行 Bandit,即可对示例文件进行扫描。仓库提供了三份配套示例:
- examples/mark_safe.py:模块级变量赋值后调用
mark_safe; - examples/mark_safe_secure.py:各类"安全"写法的集合;
- examples/mark_safe_insecure.py:各类"不安全"写法的集合。
基本命令:
bandit -r examples/ -ll-r递归扫描目录,-ll将报告级别放宽到中等级别(MEDIUM),确保 B703 的 Medium 告警能被输出。仅针对单个文件:
bandit examples/mark_safe_insecure.py不安全示例速览
examples/mark_safe_insecure.py 覆盖了插件能识别的大量危险场景,包括:
def test_insecure(str_arg): safestring.mark_safe(str_arg) # 函数参数:一律不安全 def test_insecure_with_assign(str_arg=None): if not str_arg: str_arg = 'could be insecure' safestring.mark_safe(str_arg) # 有 if 分支赋值为字面量,但另一分支仍是参数 def format_arg_insecure(cls='" onload="alert(\'xss\')'): my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe('<b>{} {}</b>'.format(my_insecure_str, 'STR')) # format 含不安全实参 def with_insecure(path): with open(path) as f: safestring.mark_safe(f.read()) # 文件读取结果:不安全 def some_insecure_case(): if ...: my_secure_str = insecure_function('insecure', cls=...) elif ...: my_secure_str = 'Secure' else: my_secure_str = 'Secure' safestring.mark_safe(my_secure_str) # 分支之一不安全 → 整体不安全此外还包括SafeText/SafeUnicode/SafeString/SafeBytes的同型调用、try/except/else/finally各分支的不安全赋值、%格式化('<b>%s</b>' % var)、*list展开、关键字参数与**dict形式、for/while循环累加、模块级外部变量(shadow 场景)、元组解包后取用不可信元素等约 29 个告警点。
安全示例速览
examples/mark_safe_secure.py 展示了不会触发告警的写法,可视为修复参考:
safestring.mark_safe('<b>secure</b>') # 字符串字面量 my_secure_str = '<b>Hello World</b>' safestring.mark_safe(my_secure_str) # 变量被赋值为字面量 my_secure_str, _ = ('<b>Hello World</b>', '') safestring.mark_safe(my_secure_str) # 元组解包,字面量来源 also_secure_str = my_secure_str safestring.mark_safe(also_secure_str) # 链式追溯 def format_secure(): safestring.mark_safe('<b>{}</b>'.format('secure')) my_secure_str = 'secure' safestring.mark_safe('<b>{}</b>'.format(my_secure_str)) # format 参数可证明安全 def all_secure_case(): if ...: my_secure_str = 'Secure' elif ...: my_secure_str = 'Secure' else: my_secure_str = 'Secure' safestring.mark_safe(my_secure_str) # 全部分支安全注意安全示例中format的关键字参数与**dict两种写法被显式标注了# nosec(见format_secure内第 38、39 行,percent_secure内第 49 行)——这印证了插件当前对关键字参数形式format调用暂不支持评估(对应源码中的TODO),需要开发者用# nosec显式豁免。
测试验证与版本沿革
Bandit 功能测试 tests/functional/test_functional.py 对该插件有完整的回归保障:
test_mark_safe(第 152 行):扫描 examples/mark_safe.py,期望结果为 1 个 MEDIUM/HIGH 告警;test_django_xss_secure(第 536 行):扫描mark_safe_secure.py,期望0 告警(防止误报);test_django_xss_insecure(第 549 行):扫描mark_safe_insecure.py,期望29 个 MEDIUM/HIGH 告警(防止漏报)。
两个方向一致的测试同时约束了插件"既不漏报、也不过度误报"的行为边界。版本信息方面,插件源码 docstring 记载:B703 自Bandit 1.5.0引入;1.7.3版本起补充了 CWE 信息(即告警输出中的CWE: CWE-80行)。
最佳实践:如何规避与修复
结合插件判定逻辑,Django 项目中使用mark_safe时应遵循以下原则:
- 只对静态字面量使用 mark_safe:
mark_safe('<b>标题</b>')这类写法不会触发告警; - 动态内容走 Django 的自动转义:模板中直接用
{{ var }}即可,引擎默认转义,不要为了"省事"先mark_safe再插入模板; - 需要输出富文本时使用专门的净化库:对不可信 HTML 先做白名单清洗(如 bleach 类工具),清洗后的结果再标记安全;
- 优先使用
format_html:Django 官方推荐django.utils.html.format_html作为mark_safe的安全替代——它会先转义参数再拼接,插件文档的 seealso 部分也明确列入了该 API; - 明确给函数参数输入边界:
mark_safe的实参若是函数参数,会被 B703 直接判为不安全,应在调用前完成净化或转义; - 对无法静态证明安全的
format关键字参数形式,需要在代码中显式加# nosec注释并人工复核(对应插件源码中的 TODO 限制)。
小结
B703django_mark_safe是 Bandit 针对 Django safestring 系列 API 的专用 XSS 检测插件:它通过模块导入检测 + 受影响函数名单圈定调用点,再以DeepAssignation变量追溯、format/%格式化调用分析等手段区分"可证明安全"与"可能不安全"的动态值,最终以Medium 严重性 / High 置信度 / CWE-80的标准 Issue 格式输出告警。将其纳入 CI 流水线,配合mark_safe_secure.py/mark_safe_insecure.py两组对照样例,可以系统性地约束 Django 项目中的"手动标记安全内容"行为,从源头降低 XSS 风险。
- SAST
- 应用安全
【免费下载链接】bandit
Bandit is a tool designed to find common security issues in Python code.
相关推荐
如何防范Django XSS漏洞:Bandit安全检测插件的终极指南
如何防范Django XSS漏洞:Bandit安全检测插件的终极指南 Bandit是一款专为Python代码设计的安全检测工具,能够帮助开发者发现常见的安全问题
SAST应用安全UI-TARS 1.5部署实战:从单条命令跑通到多副本稳定运行的完整路径
UI TARS 1.5部署实战:从单条命令跑通到多副本稳定运行的完整路径 这篇文章以开源 GUI agent 模型 UI TARS 1.5 为例,走一遍从部署选
SAST应用安全aisuite 语音转写如何调用 transcriptions.create 并让 language、prompt 在 OpenAI、Deepgram 与 Google 间自动映射
aisuite 语音转写如何调用 transcriptions.create 并让 language、prompt 在 OpenAI、Deepgram 与 Go
SAST应用安全
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考