Skip to content

静态分析总览

这一章把 ToLO 的信任边界模型落到静态分析术语:哪些值算 LLM source(不可信数据起点)、哪些调用算危险 sink(敏感操作入口)、哪些 sanitizer(能消除某类风险的处理),以及哪些 barrier/guard(静态分析里阻断污点路径的条件或调用)才能切断路径。

目标不是教完整 CodeQL 语法,而是把 ToLO 的概念约束写成可执行规则规格。只要 source、sink、sanitizer 三个集合定义不稳,后续任何查询都会在 false positive(误报:安全路径被报出)和 false negative(漏报:危险路径没被报出)之间摇摆。

这一章给你什么

你将能做到用到的内容
解释”静态分析”和”数据流分析”在做什么§“先修概念:静态分析与数据流”
解释 taint path、barrier/guard、interprocedural flow、false positive / false negative§“术语速查” + §“跨过程数据流”
写出 isLLMSource / isDangerousSink / isSanitizer 三个谓词的伪代码Sources and Sinks
把一段教学代码人工标注成 source / transform / sink / guard 四列§“初学者审计顺序” + 本页练习
区分 DataFlow::ConfigurationTaintTracking::Configuration 的取舍Predicate Rules
设计两层 query(core / survey)平衡 precision 与 recallPredicate Rules
用小型 fixture 概念性测试规则是否过宽或过窄§“如何概念性测试规则”

你需要先知道什么

读这一章前应当熟:

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

术语速查

这些词后文会反复出现,先统一含义:

术语本章含义ToLO 里的问题
source污点起点,也就是规则认为不可信的数据来源S_LLM^{direct,framework,parsed,structured,rag} 的任一子集
sink敏感操作入口,一旦接收不可信值就可能影响被保护对象ToLO-{Deser,Exec,Shell,SQL,Path,SSRF,Template} 七类
sanitizer能把某类风险真正消掉的处理C_SAFE^{schema,allowlist,parameterized,safe-codec,capability}
barrier / guard数据流规则里的阻断点;可以是校验分支、封装 API 或权限门控只有类型匹配的 sanitizer 才能作为 barrier
dataflow值从一个程序点传到另一个程序点的关系msg.content -> args["path"] -> Path(path)
taint path从 source 到 sink 的完整污染路径报告需要展示这条路径,而不是只列危险 API
interprocedural flow跨函数、方法或模块边界的数据流LLM 输出传入 tool 函数, sink 在另一个函数体内
false positive规则报告了,但路径实际上被有效防御切断把所有 json.loads 后的字段都报到 SQL,却忽略了参数化
false negative规则没报告,但未防御路径真实存在忘了建模 AgentAction.tool_input 或 tool registry
type-matched sanitizersanitizer 与 sink 语义匹配SQL 参数化只对 SQL 值位置有效,不能清洗文件路径

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

什么是静态分析

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

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

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

一个最小例子

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 个变量。关键能力是”跨变量赋值传播”

什么是跨过程数据流

跨过程数据流(interprocedural flow)指数据流穿过函数、方法或模块边界。ToLO 很多路径不是一眼能在同一个函数里看完的:

def parse_action(message):
return json.loads(message.content)
def read_file(path):
return Path(path).read_text()
def handler(question):
msg = llm.invoke(question)
action = parse_action(msg)
return read_file(action["path"])

人工看这段代码时,taint path 是:

msg.content -> parse_action return -> action["path"] -> read_file(path) -> Path.read_text()

如果规则只做单函数匹配,它会看到 json.loadsPath.read_text,却看不到二者之间的关系,从而产生 false negative。

什么是污点分析(taint tracking)

污点分析(taint analysis / taint tracking)是数据流分析的一种用法,用来回答安全问题:某个被污染的输入,是否流到了某个敏感操作? 这条从 source 到 sink 的链路就是 taint path。

可以理解为给数据”贴标签”:

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。

设计取舍

ToLO 静态分析规则应偏向可解释。每条告警最好能展示一条完整路径:

source location → transform → sink location → missing guard

只报告”这里有 eval”不够 —— 它无法证明 LLM 输出参与其中;只报告”这里有 LLM output”也不够 —— 它未必触达敏感操作。

第一版规则可以刻意保守:优先覆盖高置信 source 与高危 sink,避免把普通自然语言展示路径报成漏洞。随后再扩展到 parser 派生字段、结构化输出字段和 RAG metadata。

详见 Predicate Rules §设计取舍

如何概念性测试规则

在写 CodeQL 或 Semgrep 之前,先用小型 fixture 测规则规格。每个 fixture 只验证一个判断:

Fixture期望检查点
S_LLM^{direct} -> json.loads -> Path.read_text应报告 ToLO-Pathparser 不是路径 sanitizer
S_LLM^{framework} -> cursor.execute(query, (value,))不报告 SQL 值注入参数化与 SQL 值位置类型匹配
S_LLM^{structured}.str_field -> f"SELECT {field}" -> execute应报告 ToLO-SQLPydantic str 字段仍 tainted
S_LLM^{rag} -> prompt -> model output -> tool_input -> subprocess应报告或标为需人工确认RAG 与 tool registry 都被建模
S_LLM^{direct} -> html.escape -> os.system仍应报告 ToLO-Shellsanitizer 与 sink 类型错配
普通配置文件字符串 -> eval不作为 ToLO 报告可能是传统 CWE,但 source 不是 S_LLM

一个 fixture 的预期结果应写清楚:source 行、sink 行、是否有 barrier、为什么该 barrier 有效或无效。这样能尽早发现规则过宽导致的 false positive,或 source 建模不足导致的 false negative。

常见误区

误区为什么不对
把所有 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 各适合什么场景。

下一步阅读