项目实训(八):补齐 Python XSS,让四类核心风险形成闭环
上一篇里,我主要整理了 Python detector 的进一步增强:危险函数检测补了函数调用传播、返回值追踪、导入别名和作用域处理;硬编码密钥检测也继续做了一些误报控制。
这一篇没有急着切 JavaScript,而是先把 Python 侧还缺的一类核心风险补上:XSS。
这样一来,Python 方向基本就覆盖了任务书里的四类安全问题:
SQL 注入
XSS
危险函数调用
硬编码密钥
所以这篇的重点不是“又加了一个 detector”,而是让 Python 侧四类风险先形成一个比较完整的闭环。
一、为什么这次先做 XSS
XSS 和 SQL 注入、危险函数有点像,它不是简单看某个函数名就能判断的。
比如:
q = request.args.get("q")
return f"<h1>{q}</h1>"
这里真正的问题不是单独的 request.args.get("q"),也不是单独的 return,而是用户输入一路进入了 HTML 输出:
用户输入
-> HTML 字符串构造
-> 页面响应输出
所以这次 XSS 检测没有做成“看到 HTMLResponse 就报”,而是继续沿用当前项目里的数据流分析思路:
source -> HTML build -> sink
只有这三部分能连起来,才作为 XSS 风险候选。
二、本轮新增了什么
这次新增了一条完整的 Python XSS 检测链路:
xss_rules
-> xss flow analyzer
-> xss engine
-> xss evaluator
-> PythonXssDetector
-> registry 注册
也就是说,XSS 和前面的 SQL 注入、危险函数一样,都接入了统一的 detector / engine / evaluator 结构。
主要新增文件包括:
backend/core/rules/python/xss_rules.py
backend/core/flow/python/xss/analyzer.py
backend/core/engines/python/xss.py
backend/core/decision/python/xss_evaluator.py
backend/core/detectors/python/xss.py
backend/tests/test_python_xss.py
同时也更新了漏洞样例文件和综合测试文件,让测试样例里能同时覆盖 SQL 注入、硬编码密钥、危险函数和 XSS。
三、XSS 的核心判断逻辑
这次 XSS 检测主要分三步。
第一步,看有没有用户输入 source。
这部分没有重新写一套,而是复用了之前整理过的公共 source 规则。比如:
request.args.get("q")
request.form.get("name")
request.GET.get("id")
request.POST["name"]
request.query_params.get("q")
这样做的好处是,SQL 注入、危险函数、XSS 都能共用同一套输入源规则。后面如果继续补充新的 source,几条检测链路都能受益。
第二步,看用户输入有没有参与 HTML 构造。
目前支持几种常见写法:
html = f"<h1>{name}</h1>"
html = "<div>" + q + "</div>"
html = "<h1>{}</h1>".format(q)
html = "<h1>%s</h1>" % q
也就是说,分析器会先判断这个表达式是不是“像 HTML”,再结合 taint 判断里面有没有不可信输入。
第三步,看这个 HTML 有没有进入输出点 sink。
目前支持的 sink 主要有:
render_template_string(...)
HTMLResponse(...)
HttpResponse(...)
Response(...)
make_response(...)
return HTML
比如下面这种就会报:
from flask import request, render_template_string
q = request.args.get("q")
render_template_string(f"<h1>{q}</h1>")
它对应的链路就是:
request.args.get("q")
-> f"<h1>{q}</h1>"
-> render_template_string(...)
四、几个误报边界
XSS 很容易一不小心报多,所以这次也专门处理了几个边界。
首先,普通响应不会直接报。
比如:
q = request.args.get("q")
make_response(q)
这里虽然 q 来自用户输入,但 make_response(q) 不一定是在返回 HTML,也可能只是普通文本。所以这种暂时不报。
但如果参数本身已经是动态 HTML,就会报:
q = request.args.get("q")
make_response(f"<h1>{q}</h1>")
或者显式声明返回 HTML,也会报:
from flask import Response
q = request.args.get("q")
Response(q, content_type="text/html")
第二,经过安全转义的不直接报。
例如:
from html import escape
from flask import request, render_template_string
q = request.args.get("q")
safe = escape(q)
render_template_string(f"<h1>{safe}</h1>")
这里 q 进入 HTML 前经过了 escape(),所以当前不会作为 XSS 报告。
目前识别的安全转义函数包括:
escape
html.escape
flask.escape
markupsafe.escape
django.utils.html.escape
第三,render_template(...) 文件模板暂时不直接报。
例如:
render_template("index.html", name=q)
这里真正是否存在 XSS,要看模板文件里怎么使用 name,还要看模板引擎有没有自动转义。当前单文件分析拿不到模板上下文,所以先不把它直接扩大成 XSS。
五、补了一些真实项目里的写法
真实项目里经常会用模块别名。
例如:
import starlette.responses as responses
q = request.query_params.get("q")
responses.HTMLResponse(f"<h1>{q}</h1>")
如果只匹配 HTMLResponse 或 starlette.responses.HTMLResponse,这种写法就容易漏掉。
所以这次对少数明确的 HTML sink 做了后缀匹配,让 responses.HTMLResponse(...) 这类写法也能识别。
另外也支持了一些关键字参数形式:
render_template_string(source=f"<h1>{q}</h1>")
这些都是比较小的优化,但对真实代码会更友好一些。
六、函数调用和返回值传播
XSS 检测也支持一部分函数调用传播。
例如:
from flask import request
def render_name(name):
return f"<h1>{name}</h1>"
name = request.args.get("name")
render_name(name)
这里分析器会把链路连起来:
request.args.get("name")
-> name
-> render_name(name)
-> 参数 name
-> return f"<h1>{name}</h1>"
这样就不会只停留在当前一行,而是能看出用户输入经过函数参数进入了 HTML 返回值。
这部分思路和前面危险函数 detector 是一致的:尽量理解变量是怎么传过去的,而不是只看表面调用。
七、Web 入口函数与上下文不足
这一轮还处理了一个比较现实的问题:很多 Flask / FastAPI / Django 的 route handler,并不会在当前文件里显式调用。
比如:
@app.route("/search")
def search():
q = request.args.get("q")
return f"<h1>{q}</h1>"
从单文件静态分析角度看,search() 没有被调用。
但从 Web 框架角度看,它很可能会被框架在请求到来时调用。
如果完全不扫这种函数,会漏掉很多 Web 风险;但如果把所有未调用函数都当成确定漏洞,又会明显误报。
所以当前采用了一个折中策略:
确认调用路径可达:正常报 high
疑似 Web 入口:报 low + needs_more_context=True
普通未调用函数:报 low + needs_more_context=True
这里的 low 更像“低置信上下文提示”,不是说 XSS 本身危害低,而是当前单文件上下文还不能确认它一定可达。
普通未调用函数和 Web 入口函数最终可能都是:
low + needs_more_context=True
但内部证据不一样。
普通未调用函数更像:
unconfirmed.parameter
疑似 Web 入口更像:
probable_entrypoint / http.parameter
这样后续生成中文解释时,就可以把它说成“潜在风险提示”,而不是确定漏洞。
八、返回给后续解释功能的数据
XSS 最后还是会转换成统一的 RiskCandidate,方便后续解释层继续使用。
一个结果大概会包含这些信息:
{
"risk_type_candidate": "XSS",
"severity": "high",
"summary": "不可信输入进入 HTML 输出点 HTMLResponse(...),存在跨站脚本(XSS)风险。",
"start_line": 5,
"end_line": 5,
"needs_more_context": false,
"evidence_chain": {
"source_line": 4,
"sink_line": 5,
"source_variable": "q",
"source_kind": "http.query",
"source_selector": "q"
}
}
同时还会带上 flow_trace,记录 source、HTML 构造、sink 这些步骤。
这样后续生成解释时,不只是知道“这里有 XSS”,还可以知道它为什么被判成 XSS。
九、测试覆盖
同时,这轮也新增和更新了多组测试,主要覆盖:
render_template_string f-string XSS
HTMLResponse 字符串拼接 XSS
responses.HTMLResponse 模块别名
render_template_string(source=...)
make_response 动态 HTML
make_response 普通文本不误报
Response(text/html) 报 XSS
escape 转义后不报
jsonify 不报
render_template 文件模板不报
route handler 低置信提示
普通未调用函数低置信提示
selection 模式行号偏移
函数调用返回 HTML
样例文件 fixture_xss
综合样例 single_file_all_risks
最后重新跑了一遍后端测试,全部通过,这说明当前 Python 侧四类风险已经都进入了统一测试链路。
十、总结与下一步
这一轮主要完成了 Python XSS 检测初版,也让 Python 方向基本覆盖了任务书中的四类核心风险:SQL 注入、XSS、危险函数调用和硬编码密钥。
XSS detector 没有简单按函数名报错,而是沿用了当前项目里的分析思路,围绕:
source -> HTML build -> sink
建立轻量数据流判断。
只有不可信输入参与 HTML 构造,并最终进入 HTML 输出点时,才作为 XSS 风险候选。
同时,这轮也处理了几个容易误报或漏报的边界:普通响应不直接扩大成 XSS,经过 escape() 的输入不直接报,render_template(...) 文件模板暂时不直接报,未调用函数和疑似 Web 入口则通过 needs_more_context=True 降级为上下文提示。
到这里,Python 侧四类风险已经进入统一的分析链路。下一步准备继续推进 JavaScript 侧四类风险检测,验证当前可插拔框架的多语言扩展能力。
更多推荐
所有评论(0)