本节摘要:CodeQL 是 GitHub 的语义分析引擎,社区热度约 10.2k stars(2026-10 社区口径)。它的心智模型是一句话:把代码当数据库查询。本节先走通链路——database create 建库、.ql 查询、analyze 出报告;再讲 QL 语言的声明式思想:你描述「要什么」,引擎负责「怎么找」,递归与传递闭包是语言内建能力;然后拆解一条污点追踪查询的三要素 source、sink、path,看数据流查询如何跨函数跨文件落地,并做一次「换 sink」的改写练习;接着解释变体发现——一条查询为什么能横扫上千个仓库;最后划清授权边界:查询库 MIT 开源,开源与研究用途免费,商业 CI 需要授权。示例对象仍是自建 Flask 项目。
CodeQL 的第一课不是学语法,而是换心智模型:分析不是「跑脚本遍历代码」,而是「对代码数据库执行查询」。建库时,抽取器把源码解析成一张张关系表——函数表、调用表、表达式表、赋值表;查询时,QL 引擎在这些表上做逻辑推导。
# cql_flow.sh —— CodeQL 链路:建库、查询、出报告(CLI 自 GitHub 官方发布页获取) codeql database create flask-db --language=python --source-root=./myflask codeql database analyze flask-db CmdInjection.ql --format=sarif-latest --output=out.sarif
两条命令的关键差异要记住:Python、JavaScript 这类解释型语言建库不需要编译;C、C++、Java 需要 --command 捕获真实构建过程(比如 --command="make"),因为抽取器要看到完整的类型与宏展开信息。这也是它比 semgrep「重」的根源——语义信息的代价是构建环境。
# db_pipeline.txt —— 建库到报告的数据流示意 源码 ──database create──▶ 代码数据库(关系表集合) │ .ql 查询(逻辑推导) │ ▼ SARIF/表格形式的路径级发现
建库完成后可以验证规模:codeql database info flask-db(子命令以官方文档为准)能列出抽取到的文件数、函数数等规模信息——建库是「一次投入、多次查询」的资产,日常迭代只改查询不改库。
QL 是声明式语言:查询描述「什么条件成立」,不描述「按什么顺序去找」。对比一段心算:
QL 的核心构件是谓词(predicate):一个返回真假的逻辑判断,可以组合、可以递归。查询主体是 from-where-select 三段:声明变量、施加约束、输出结果,形似 SQL 而推理能力强得多。
| 过程式脚本的负担 | QL 里的对应物 |
|---|---|
| 手写遍历与工作队列 | 引擎自动搜索 |
| 手工去重与记忆化 | 逻辑变量的唯一性约束 |
| 自己实现传递闭包 | 谓词递归一行搞定 |
| 终止条件靠人保证 | 引擎保证半可判定安全 |
第一次写 QL 的常见不适应是「想写循环」——忍住,把循环改写成谓词的递归定义,通常三五行就够。第二个不适应是「调试」:QL 没有 print 断点,调试方法是把中间谓词写成小查询逐个 select,看它命中的是哪些元素——这反而逼着你把大问题拆成可命名的小断言。
污点追踪是 CodeQL 的招牌能力。任何一条污点查询都由三要素构成:污染源 source(外部输入从哪进来)、汇聚点 sink(危险操作在哪)、以及连接两者的数据流路径 path。以 Python 项目为例,查询骨架如下(写法示意,API 细节以官方查询库文档为准):
# CmdInjection.ql —— CodeQL 污点查询示意:请求参数流向命令执行 import python module CmdInjectionConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { exists(Call call | call.getFunc().(Attribute).getName() = ["get", "post"] and call.getFunc().(Attribute).getAttr().getName() = "args" and call = source.asCall() ) } predicate isSink(DataFlow::Node sink) { exists(Call call | call.getFunc().(Name).getId() = "system" and sink.asExpr() = call.getArg(0) ) } } module Flow = DataFlow::Global<CmdInjectionConfig>; from Flow::PathNode source, Flow::PathNode sink where Flow::flowPath(source, sink) select sink.getNode(), source, sink, "用户输入流向命令执行"
结构读法:isSource 定义「什么算外部输入」,isSink 定义「什么算危险出口」,flowPath 负责中间的一切——跨函数、跨文件、经过赋值与容器操作。select 输出的不只是两端,还有完整路径节点序列,这就是 path 要素:证据自带的调用链。
对照表帮记忆三要素与 6.1 节的映射:
| 要素 | CodeQL 里的形态 | semgrep taint 对应物 | 提问 |
|---|---|---|---|
| source | isSource 谓词 | pattern-sources | 输入从哪进来 |
| sink | isSink 谓词 | pattern-sinks | 危险在哪发生 |
| path | flowPath 推导 | 引擎内建 | 中间经过了什么 |
| 屏障 | 障碍谓词(可选) | pattern-sanitizers | 什么算净化 |
改写练习:把 isSink 换成 open 调用即得路径穿越查询;再加一个屏障谓词,把已知安全的封装函数排除——这与 6.1 节 sanitizer 的思路一一对应,只是粒度从「一行净化调用」升级到「任意自定义屏障逻辑」。改写后先造一个必命中的测试函数放进项目(阳性对照),查询抓到它再扫全库——这是第 10.2 节会反复出现的评测基本功。
变体发现(variant analysis)是 CodeQL 区别于单仓库扫描工具的战略能力,思路分三步:
# variant_flow.txt —— 变体发现的复用路径 新漏洞修复补丁 ──抽象──▶ 污点查询(缺陷模式) │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ 仓库 A 仓库 B 仓库 C ... │ │ │ └───────┬───────┴───────┬───────┘ ▼ ▼ 命中:同源变体 未命中:排除
对个人研究者的日常用法更朴素:把 6.2 节练手的查询留存成库,每接手一个新项目先跑一遍——查询就是可复用的「缺陷模式记忆」。第 10 章会看到,这种「缺陷模式沉淀」与 AI 挖洞方法论是互补关系:模型负责提出新的模式假设,CodeQL 负责在全仓库范围内验证。
引用与使用 CodeQL 前先把授权说清楚(以 GitHub 官方条款为准):
| 事项 | 口径 |
|---|---|
| 查询库与标准库 | github/codeql 仓库中开源,MIT 许可证 |
| 开源项目与研究用途 | 免费使用 |
| 企业内部代码的商业 CI | 需要 GitHub 相应授权(Advanced Security 体系) |
| 学习资源 | 官方文档提供教学查询与 CTF 练习资源,codeql 仓库内含大量示例 |
对本书读者最实际的建议:研究、学习、开源贡献三类场景都在免费范围内;把能力建设花在查询编写上,而不是引擎本身。社区也流传大量第三方查询集,采用前先审一遍内容——查询本身也是代码,也该被 review;这一点在把查询接入第 10 章的 AI 流程时尤其重要,因为一条写错的查询会把幻觉合法化成「机器结论」。
一轻一重两台引擎都转起来了:semgrep 秒级快筛、CodeQL 深度污点。但它们各是各的模型——一个活在规则文件里,一个活在数据库里。下一节先把两者放进同一张对比表,再回答一个容易混淆的问题:这和《语义代码检索》讲的「检索式理解」到底是不是一回事。