Skip to content

静态分析总览

这一章把 ToLO 的信任边界模型落到静态分析术语:哪些值算 LLM source、哪些调用算危险 sink、哪些 guard 才能切断路径。

目标不是教完整 CodeQL 语法,而是把 ToLO 的概念约束写成可执行规则规格。只要 source、sink、sanitizer 三个集合定义不稳,后续任何查询都会在误报和漏报之间摇摆。

这一章给你什么

你将能做到用到的内容
解释”静态分析”和”数据流分析”在做什么§“先修概念:静态分析与数据流”
写出 isLLMSource / isDangerousSink / isSanitizer 三个谓词的伪代码Sources and Sinks
把一段教学代码人工标注成 source / transform / sink / guard 四列§“初学者审计顺序” + 本页练习
区分 DataFlow::ConfigurationTaintTracking::Configuration 的取舍Predicate Rules
设计两层 query(core / survey)平衡 precision 与 recallPredicate Rules

你需要先知道什么

读这一章前应当熟:

不需要先会 CodeQL 也不需要写过 Semgrep。这两个工具的语法在 CodeQL and Semgrep 章再讲;本章只关心规则规格

先修概念:静态分析与数据流

什么是静态分析

静态分析就是不运行程序,直接读代码,判断可能的数据流。它不能像真实运行那样知道每个变量的具体值,但能回答:

“某个不可信值有没有可能流到某个危险调用?”

类比:警察查案不需要”目击犯罪现场”,他们看监控录像 + 物证 + 关系图,推断可能的犯罪路径。静态分析是给程序做同样的事:不需要执行,只看代码结构 + 函数调用图 + 变量赋值。

一个最小例子

answer = llm.invoke(prompt)
sql = answer.content
db.execute(sql)

静态分析会:

  1. answer.content 标成 source(LLM 输出)
  2. db.execute 标成 sink(SQL 执行)
  3. 检查 answer.contentdb.execute(sql) 之间有没有有效 sanitizer
  4. 没有 → 报告一条 ToLO-SQL 警告

整个过程不运行代码

什么是数据流分析

数据流分析(dataflow analysis)是静态分析的一种,专门追踪数据在变量、字段、函数之间如何传递

content = ai_message.content # ← 起点
data = json.loads(content) # ← 经过 json.loads
path = 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) # ← 标签传给 data
path = data["path"] # ← 标签传给 path
open(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

阅读顺序

  1. Sources and Sinks:定义 S_LLM 五子集、七类 sink 与五类 C_SAFE,可直接映射到 CodeQL DataFlow::ConfigurationTaintTracking::ConfigurationisSource / isSink / isBarrier
  2. 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()

人工标注结果:

标注
Sourcemsg.content (S_LLM^framework if LangChain AIMessage,S_LLM^direct if raw SDK)
Transformjson.loads(msg.content) 解析(不是 sanitizer)→ args["path"] 字段访问(不是 sanitizer)
SinkPath(path).read_text() (ToLO-Path,对应 CWE-22)
Sanitizer没有json.loads 只是解析,不是路径 sanitizer

因此这条路径应进入 ToLO-Path 分析。

同一段代码,加 sanitizer 后

from pathlib import Path
ROOT = Path("/srv/notes").resolve()
msg = llm.invoke(question)
args = json.loads(msg.content)
path = args["path"]
target = (ROOT / path).resolve() # ← 拼接 + resolve
if not target.is_relative_to(ROOT): # ← 类型匹配 sanitizer
raise PermissionError("path escapes root")
return target.read_text()

新的标注:

标注
Source同上
Transform同上
Sink同上
Sanitizeris_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。

详见 Predicate Rules §设计取舍

常见误区

误区为什么不对
把所有 Pydantic model 都当 sanitizerstr 字段仍可携带任意 payload;只有 Literal / Enum / 受约束类型才算
把所有 parser 都当 sanitizerjson.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::ConfigurationTaintTracking::Configuration 各适合什么场景。

下一步阅读