CodeQL 与 Semgrep
这一章面向规则设计。ToLOScanner 的技术主张不是发明新的污点传播算法,而是把 LLM 输出识别为新的 untrusted source family,再复用成熟的 sink 模型和数据流引擎。
读完本章你应当能:给定一个公开 CVE 案例,设计出能命中它的 Semgrep 规则草稿和 CodeQL 查询草稿。
这一章给你什么
| 你将能做到 | 用到的内容 |
|---|---|
| 解释 Semgrep 和 CodeQL 各自的工作机制(语法模式 vs 语义数据流) | §“两个工具的工作机制” |
| 知道一条 ToLO 规则要写哪些组件(source / sink / sanitizer / 传播 step) | §“规则模块组成” |
写出 Semgrep pattern-sources / pattern-sinks / pattern-sanitizers 三段 | Query Design |
写出 CodeQL TaintTracking::Configuration 的 isSource / isSink / isBarrier | Query Design |
| 给出一条规则的三档分级(High / Medium / Review) | Query Design |
你需要先知道什么
- ToLO 的 source / sink / sanitizer 集合 — 见 Sources and Sinks。
- 数据流分析 / 污点传播的基本概念 — 见 先修知识 §5+§8 与 Static Analysis。
- 看过至少一段 LangChain 代码,知道
AIMessage.content/tool_calls大概是什么。
不需要先会写 Semgrep YAML 或 CodeQL QL。本章演示足够的最小语法,边读边学。
两个工具的工作机制
| 工具 | 工作机制 | 一句话 |
|---|---|---|
| Semgrep | 把代码解析成 AST,用 YAML 规则做语法模式匹配 | ”可读的代码模式搜索” |
| CodeQL | 把代码编译成关系数据库,用 QL 写跨函数数据流查询 | ”把代码当数据库查询” |
Semgrep 工作机制(简化)
源代码 ──→ AST ──→ 规则模式匹配 (metavariable) ──→ 告警规则示例:
rules: - id: dangerous-eval pattern: eval($X) message: "eval() called with $X" languages: [python] severity: WARNING$X 是 metavariable(语法占位符),匹配任意表达式。优势:几小时能写几十条规则,跨语言。限制:不能跨函数追踪;不能精确判断 $X 是不是污染数据。
CodeQL 工作机制(简化)
源代码 ──→ CodeQL 数据库(类似关系表) ──→ QL 查询(类似 SQL) ──→ 告警规则示例:
import pythonimport semmle.python.dataflow.new.TaintTracking
class LLMOutputToEval extends TaintTracking::Configuration { LLMOutputToEval() { this = "LLMOutputToEval" }
override predicate isSource(DataFlow::Node n) { // AIMessage.content exists(Attribute a | a.getAttr() = "content" and a.getObject().getType().getName() = "AIMessage" and n.asExpr() = a) }
override predicate isSink(DataFlow::Node n) { exists(Call c | c.getFunc().(Name).getId() in ["eval", "exec"] and n.asExpr() = c.getArg(0)) }}
from LLMOutputToEval cfg, DataFlow::PathNode src, DataFlow::PathNode sinkwhere cfg.hasFlowPath(src, sink)select sink, src, sink, "LLM 输出经数据流到达 eval()"优势:能跨多文件、跨函数、跨对象追踪 AIMessage.content → 经过任意多次赋值 / parser / dict access → 最终到 eval 的完整路径。限制:学习曲线陡;构建 CodeQL 数据库要先成功编译整个项目;对 Python 这种动态语言尤其慢。
ToLO 上的分工
ToLO 需要二者配合:
- Semgrep 做教学和初筛 —— 快速大规模扫,定位”附近可能有问题”的代码点。
- CodeQL 做完整 source-to-sink 路径 —— 给出精确证据。
为什么需要两类工具
Semgrep 适合快速写出可读、可教学的局部模式。例如查找 AIMessage.content 附近是否出现 eval、subprocess(..., shell=True) 或 cursor.execute(f"...")。
- 优势:上手快、规则可读、CI 集成简单、跨语言。
- 适合:大规模初筛、教学规则、PR review hook。
CodeQL 适合表达跨函数、跨对象、跨模块的数据流。ToLO 的真实路径常常经过 parser、dataclass / Pydantic 字段、agent action、tool registry 和 helper wrapper —— 单个语法模式不够。
- 优势:跨函数完整 taint tracking、可解释路径、误报低。
- 适合:核心规则、CVE 复盘、高保真扫描。
一个对比例子
考虑 ToLO 的最小例子:LLM 输出经过 json.loads 后进入 open。
Semgrep 视角
rules: - id: tolo-path-near-llm-1 pattern-either: - pattern: | $MSG = $LLM.invoke(...) ... $OBJ = json.loads($MSG.content) ... open($OBJ[...]) message: "Possible ToLO-Path: LLM output → json.loads → open" languages: [python]这条规则只能匹配那种”几行内连续出现”的模式。如果 $MSG.content 经过更多变量赋值 / 跨函数,Semgrep 就漏掉。
CodeQL 视角
class ToLOPath extends TaintTracking::Configuration { override predicate isSource(DataFlow::Node n) { n instanceof LLMOutputSource // 抽象出 AIMessage.content 等 }
override predicate isSink(DataFlow::Node n) { n instanceof PathSink // 复用 CodeQL 标准 path-injection }
override predicate isAdditionalTaintStep(DataFlow::Node src, DataFlow::Node tgt) { // json.loads(x) → x 传播到返回值 exists(Call c | c.getFunc().(Attribute).getName() = "loads" and src.asExpr() = c.getArg(0) and tgt.asExpr() = c) }}CodeQL 跨函数追踪整条链路。即使 msg.content 在 file A,json.loads 在 file B,open 在 file C,只要数据流连通,都能命中。
ToLOScanner 的核心不是”找危险 API”
ToLOScanner 的核心是”找 LLM 输出到危险 API 的路径”。
这是设计原则。所有规则都应当能回答这个问题:
1. 这一行的值来自 LLM 输出吗?(source 端)2. 这个值流到哪里?(传播)3. 它最终被消费在敏感操作里吗?(sink 端)4. 中间有没有类型匹配的 sanitizer?(barrier)只回答 1 → 是 model safety / prompt injection 研究。 只回答 3 → 是传统 CWE 检测。 四个都回答清楚 → 才是 ToLO。
规则模块组成
一条完整 ToLO 规则需要以下组件:
| 组件 | Semgrep 对应 | CodeQL 对应 |
|---|---|---|
| Source 定义 | pattern-sources | isSource |
| Sink 定义 | pattern-sinks | isSink(复用标准库) |
| Sanitizer 定义 | pattern-sanitizers | isBarrier |
| 传播 step | (难,通常不显式建) | isAdditionalTaintStep |
| 跨函数 | 难 | 默认支持 |
| 路径展示 | 有限 | DataFlow::PathNode |
CodeQL 模块拆分推荐
ToLOSource/├── LLMClientSource.qll ← OpenAI / Anthropic SDK 直接返回字段├── LangChainSource.qll ← AIMessage / AgentAction / etc.├── LlamaIndexSource.qll ← Response / NodeWithScore / etc.├── ParsedOutputStep.qll ← json/yaml/parser 传播├── StructuredOutputStep.qll ← Pydantic / structured output 字段传播└── RAGSource.qll ← Document.page_content 等
ToLOSink/└── (复用 CodeQL 标准库 + 框架特定 wrapper)
ToLOSanitizer/├── SchemaBarrier.qll ← Literal / Enum 字段├── AllowlistBarrier.qll ← if x in ALLOWED├── ParameterizedBarrier.qll ← cursor.execute(..., params)├── SafeCodecBarrier.qll ← yaml.safe_load 等└── CapabilityBarrier.qll ← (检测较难,主要 placeholder)这样拆分后,新增框架通常只需要扩展 source 模块,不会扰动 sink 与 sanitizer 判定。
规则设计顺序
不要一开始就写完整规则。按这个顺序:
- 先做高置信 source:直接 LLM API 返回字段、LangChain / LlamaIndex 等旗舰框架 message 字段、结构化输出字段。
- 再接入复用 sink:代码执行、shell、SQL、路径、SSRF、模板、反序列化。直接 import CodeQL 标准库。
- 最后建 sanitizer:schema、allowlist、parameterized call、safe codec 和 capability gate。
这三步顺序很重要:
- 若先写 sink,再随意扩大 source → 会把普通业务字符串误报成 ToLO。
- 若只写 source,不建 sanitizer → 会无法区分已修复路径和真实失守路径。
详细顺序见 Query Design §查询设计顺序。
一个学习例子
Semgrep 可以先找:
pattern: subprocess.run(..., shell=True)但这只能说明有 shell sink,不能说明是 ToLO。CodeQL 需要进一步回答:
这个 shell 命令字符串是否来自 LLM output?中间是否经过 allowlist 或参数化?所以 ToLOScanner 的核心不是”找危险 API”,而是”找 LLM 输出到危险 API 的路径”。
常见误解
误解 1:“Semgrep 找到 eval 就是 ToLO。”
不一定。eval(some_local_var) 可能 var 来自任意位置。要证明 LLM 输出流入 eval。
误解 2:“CodeQL 会自动懂 LLM 框架。”
不会。必须显式建模 AIMessage、tool_calls、parser 字段等 source。CodeQL 默认只识别 RemoteFlowSource(HTTP 等)。
误解 3:“规则越宽越好。”
不一定。过宽 source 会把普通内部字符串误报成 ToLO。比如把”任何 .content 属性访问”都当 source,会包含很多无关字段(HTTP response、cache、数据库 row 等)。
误解 4:“Semgrep 比 CodeQL 简单,所以更适合初学。”
取决于场景。简单语法模式 Semgrep 更易写;但跨函数追踪 CodeQL 更可靠。ToLO 的真实路径常常跨函数,所以 CodeQL 的”高启动成本”会被回报。
误解 5:“写完规则就能跑去公开仓库扫漏洞。”
不是直接漏洞。规则结果是”可能路径”,需要人工 triage 才能上升到漏洞报告。详见 Query Design §告警分级。
读完检查
判断下面两条规则哪条更接近 ToLOScanner:
- A. 找所有
eval(...)。 - B. 找
AIMessage.content经过传播后进入eval(...),且中间没有 sandbox / capability。
B 更接近。A 是传统危险 API 搜索,不能说明 source 是 LLM 输出。
另一个问题:如果一条 Semgrep 规则找到 cursor.execute(sql),下一步应该问什么?
sql是否来自 LLM 输出或 RAG 文档?- 中间是否经过表/列白名单或参数化?
- 数据库账号是否只读?
只有这些问题回答清楚,才能进入 ToLO 判断。
下一步阅读
- Query Design Notes:完整规则设计笔记,带 Semgrep / CodeQL 草稿对比。
- Sources and Sinks:规则需要的 source / sink / sanitizer 集合。
- Predicate Rules:传播规则、
DataFlowvsTaintTracking取舍。