Skip to content

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::ConfigurationisSource / isSink / isBarrierQuery Design
给出一条规则的三档分级(High / Medium / Review)Query Design

你需要先知道什么

  • ToLO 的 source / sink / sanitizer 集合 — 见 Sources and Sinks
  • 数据流分析 / 污点传播的基本概念 — 见 先修知识 §5+§8Static 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 python
import 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 sink
where 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 附近是否出现 evalsubprocess(..., 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-sourcesisSource
Sink 定义pattern-sinksisSink(复用标准库)
Sanitizer 定义pattern-sanitizersisBarrier
传播 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 判定。

规则设计顺序

不要一开始就写完整规则。按这个顺序:

  1. 先做高置信 source:直接 LLM API 返回字段、LangChain / LlamaIndex 等旗舰框架 message 字段、结构化输出字段。
  2. 再接入复用 sink:代码执行、shell、SQL、路径、SSRF、模板、反序列化。直接 import CodeQL 标准库。
  3. 最后建 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 框架。”

不会。必须显式建模 AIMessagetool_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 判断

下一步阅读