静态分析总览
这一章把 ToLO 的信任边界模型落到静态分析术语:哪些值算 LLM source、哪些调用算危险 sink、哪些 guard 才能切断路径。
目标不是教完整 CodeQL 语法,而是把 ToLO 的概念约束写成可执行规则规格。只要 source、sink、sanitizer 三个集合定义不稳,后续任何查询都会在误报和漏报之间摇摆。
这一章给你什么
| 你将能做到 | 用到的内容 |
|---|---|
| 解释”静态分析”和”数据流分析”在做什么 | §“先修概念:静态分析与数据流” |
写出 isLLMSource / isDangerousSink / isSanitizer 三个谓词的伪代码 | Sources and Sinks |
| 把一段教学代码人工标注成 source / transform / sink / guard 四列 | §“初学者审计顺序” + 本页练习 |
区分 DataFlow::Configuration 和 TaintTracking::Configuration 的取舍 | Predicate Rules |
| 设计两层 query(core / survey)平衡 precision 与 recall | Predicate Rules |
你需要先知道什么
读这一章前应当熟:
- source / sink / sanitizer 的基本含义(先修知识 §5)
S_LLM五子集 /S_DANGER七子类 /C_SAFE五类(Taxonomy)- 至少写过几行 Python,能看懂 AST 概念(先修知识 §8)
不需要先会 CodeQL 也不需要写过 Semgrep。这两个工具的语法在 CodeQL and Semgrep 章再讲;本章只关心规则规格。
先修概念:静态分析与数据流
什么是静态分析
静态分析就是不运行程序,直接读代码,判断可能的数据流。它不能像真实运行那样知道每个变量的具体值,但能回答:
“某个不可信值有没有可能流到某个危险调用?”
类比:警察查案不需要”目击犯罪现场”,他们看监控录像 + 物证 + 关系图,推断可能的犯罪路径。静态分析是给程序做同样的事:不需要执行,只看代码结构 + 函数调用图 + 变量赋值。
一个最小例子
answer = llm.invoke(prompt)sql = answer.contentdb.execute(sql)静态分析会:
- 把
answer.content标成 source(LLM 输出) - 把
db.execute标成 sink(SQL 执行) - 检查
answer.content到db.execute(sql)之间有没有有效 sanitizer - 没有 → 报告一条
ToLO-SQL警告
整个过程不运行代码。
什么是数据流分析
数据流分析(dataflow analysis)是静态分析的一种,专门追踪数据在变量、字段、函数之间如何传递。
content = ai_message.content # ← 起点data = json.loads(content) # ← 经过 json.loadspath = data["path"] # ← 从 dict 取字段file = Path(path) # ← 再包装一层file.read_text() # ← 进入 sink数据流分析能跟着这条链路追踪,即使中间经过 4 个变量。关键能力是”跨变量赋值传播”。
什么是污点分析(taint tracking)
污点分析(taint analysis / taint tracking)是数据流分析的一种用法,用来回答安全问题:某个被污染的输入,是否流到了某个敏感操作?
可以理解为给数据”贴标签”:
content = ai.content # ← 贴上 "tainted" 标签data = json.loads(content) # ← 标签传给 datapath = data["path"] # ← 标签传给 pathopen(path) # ← tainted 值进入 sink → 报告!只要标签从 source 一直传到 sink,且中途没被 sanitizer 移除,就报告。
CodeQL DataFlow vs TaintTracking
CodeQL 区分两种模式:
DataFlow::Configuration:值保持传播(value-preserving)。y = x算传播;y = json.loads(x)不算(因为y是新对象,值变了)。误报低,但漏报多。TaintTracking::Configuration:污点传播(taint-preserving)。y = x.replace("a","b")、y = json.loads(x)、y = some_dict[x]都算 —— 只要污染语义没被中和,污染就传。召回高,但误报也多。
ToLO 因为常有 parser、container、format 字符串这种”中间表示变了但内容仍 untrusted”的步骤,通常需要 TaintTracking。详见 Predicate Rules。
阅读顺序
- Sources and Sinks:定义
S_LLM五子集、七类 sink 与五类C_SAFE,可直接映射到 CodeQLDataFlow::Configuration或TaintTracking::Configuration的isSource/isSink/isBarrier。 - ToLO 谓词与传播规则:把判定条件写成数据流谓词,并说明 CodeQL
DataFlow/TaintTracking的取舍、容器传播、字符串变换、框架分派的特殊处理。
建议边读边拿公开案例做标注练习:先在案例中圈出 LLM-output source,再圈出 sink,最后检查修复是否是类型匹配的 sanitizer。能完成这三步,才进入查询实现。
产出目标
读完本章后,你应当能:
- 用统一 source 集合覆盖 direct、framework、parsed、structured、rag 五类 LLM 输出。
- 复用经典 sink(
code-injection/sql-injection/command-line-injection/path-injection/server-side-request-forgery/unsafe-deserialization/template-injection),不在 sink 端发明新类别。 - 只把类型匹配的 schema、allowlist、parameterized call、safe codec、capability gate 视为 sanitizer。
- 给一段陌生代码做四列标注:source / transform / sink / guard。
初学者先不要纠结什么
先不要纠结 CodeQL 语法、SSA、AST、IR 这些实现细节。先把每个案例人工拆成:
source: 这个值哪里来的?transform: 它经过哪些变量、字段、parser?sink: 它最后进入什么危险操作?sanitizer: 中间有没有真正匹配的防御?如果人工都拆不清楚,写查询只会把混乱自动化。
一段标注练习
看这段代码:
msg = llm.invoke(question)args = json.loads(msg.content)path = args["path"]return Path(path).read_text()人工标注结果:
| 列 | 标注 |
|---|---|
| Source | msg.content (S_LLM^framework if LangChain AIMessage,S_LLM^direct if raw SDK) |
| Transform | json.loads(msg.content) 解析(不是 sanitizer)→ args["path"] 字段访问(不是 sanitizer) |
| Sink | Path(path).read_text() (ToLO-Path,对应 CWE-22) |
| Sanitizer | 没有。json.loads 只是解析,不是路径 sanitizer |
因此这条路径应进入 ToLO-Path 分析。
同一段代码,加 sanitizer 后
from pathlib import PathROOT = Path("/srv/notes").resolve()
msg = llm.invoke(question)args = json.loads(msg.content)path = args["path"]
target = (ROOT / path).resolve() # ← 拼接 + resolveif not target.is_relative_to(ROOT): # ← 类型匹配 sanitizer raise PermissionError("path escapes root")return target.read_text()新的标注:
| 列 | 标注 |
|---|---|
| Source | 同上 |
| Transform | 同上 |
| Sink | 同上 |
| Sanitizer | is_relative_to(ROOT) after resolve() → C_SAFE^allowlist(类型匹配) |
这条路径不再报告 ToLO。
设计取舍
ToLOScanner 的规则应偏向可解释。每条告警最好能展示一条完整路径:
source location → transform → sink location → missing guard只报告”这里有 eval”不够 —— 它无法证明 LLM 输出参与其中;只报告”这里有 LLM output”也不够 —— 它未必触达敏感操作。
第一版规则可以刻意保守:优先覆盖高置信 source 与高危 sink,避免把普通自然语言展示路径报成漏洞。随后再扩展到 parser 派生字段、结构化输出字段和 RAG metadata。
常见误区
| 误区 | 为什么不对 |
|---|---|
| 把所有 Pydantic model 都当 sanitizer | 裸 str 字段仍可携带任意 payload;只有 Literal / Enum / 受约束类型才算 |
| 把所有 parser 都当 sanitizer | json.loads 只改变表示形式,不清洗字段内容 |
| 把所有 LLM 输出展示都当 sink | 显示到页面、日志或用户聊天框未必构成 ToLO,除非进入敏感操作 |
| 忽略 tool registry | 很多危险调用藏在工具函数内部,source 先进入 tool_input,再由 registry 分派 |
把 subprocess.run([...], shell=False) 当万能 | list 形式参数化是 sanitizer,但前提是 list 第一项(可执行文件)固定;如果第一项也是 LLM 输出,仍然危险 |
把”agent 框架带 sandbox” 当 C_SAFE^capability | 要看 sandbox 真的封住了什么(syscall / 网络 / fs / quota),不能默认 |
阅读检查
继续读规则页前,确认你能解释:
- 为什么
json.loads不一定是 sanitizer。 - 为什么 source 建模比 sink 建模更关键。
- 为什么报告需要展示完整 path,而不是只说”这里有 eval”。
DataFlow::Configuration和TaintTracking::Configuration各适合什么场景。
下一步阅读
- Sources and Sinks:五子集 / 七类 / 五类的具体定义与代码模式。
- ToLO 谓词与传播规则:传播规则、容器传播、字符串变换、框架分派、CodeQL 取舍。
- 完成本章后读 CodeQL and Semgrep,理解两个工具在规则实现上的分工。