上一篇里,我主要整理了 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>")

如果只匹配 HTMLResponsestarlette.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 侧四类风险检测,验证当前可插拔框架的多语言扩展能力。

更多推荐