GitHub 的 CodeQL 2.26.0 版本新增了一个内置查询,用于识别 AI 提示词注入(prompt-injection)模式,这一变化已导致 CI 流水线开始标记新的风险。仅进行升级是不够的——团队需要一套回归测试套件,以确保随着代码的演进,该规则始终保持有效。
为什么回归测试夹具(regression fixture)至关重要
提示词注入允许攻击者将恶意指令混入提示词中,随后语言模型会执行这些指令。通过新的查询,静态分析可以追踪从不受信任源(untrusted source)到模型调用汇聚点(model-calling sink)的数据流。如果只是简单地开启该规则而从未进行验证,后续的代码重构可能会破坏数据流路径,导致警报悄无声息地消失。回归测试夹具可以捕获那些应当触发(或不应当触发)规则的确切路径,将静态分析结果转化为构建过程强制执行的契约。
可靠夹具的三个要素
- 不受信任的源 (Untrusted source) – 任何从受信任代码库外部引入数据的函数(例如:GitHub issue 正文、webhook 负载)。
- 提示词构建 (Prompt construction) – 组装模型请求的代码,通常是调用客户端 SDK。
- 模型汇聚点 (Model sink) – 将提示词发送给模型的 SDK 方法。CodeQL 的数据流引擎需要看到来自生产栈的真实调用才能识别该汇聚点。
只有当这三个要素同时存在时,查询才会触发。
组织测试文件
采用常规的布局可以使测试套件易于审计:
security-fixtures/prompt-injection/
├─ positive/
│ ├─ direct-flow.ts
│ └─ helper-flow.ts
├─ negative/
│ └─ trusted-instruction.ts
└─ expected-alerts.json
Positive(正向)文件包含应当被标记的代码;negative(负向)文件则包含必须保持静默的安全模式。
编写正向用例
最简单的例子展示了从不受信任的值到模型调用的直接流向:
import { model } from "./supported-client";
declare function loadIssueBody(id: number): Promise<string>;
export async function summarize(id: number) {
const untrusted = await loadIssueBody(id);
return model.generate({
system: "Summarize the issue",
user: untrusted,
});
}
在这里,loadIssueBody 是不受信任的源,model.generate 是汇聚点,且数据在传递过程中没有任何清洗(sanitisation)步骤——这正是该查询旨在捕获的情况。
第二个正向用例应当通过辅助函数路由数据,以证明分析能够追踪间接路径:
function wrapUserInput(input: string) {
return { system: "Summarize the issue", user: input };
}
export async function summarizeViaHelper(id: number) {
const raw = await loadIssueBody(id);
return model.generate(wrapUserInput(raw));
}
这两个文件都属于 positive/ 目录。
编写负向用例
负向夹具必须证明用户输入无法篡改模型的指令。一个常见的错误是假设名为 sanitize() 的函数可以保证安全性。静态分析器并不会将函数名视为证明,因此测试应避免使用任何具有误导性的清洗存根(sanitisation stub):
export async function safeSummarize(id: number) {
const trusted = "Summarize the issue";
const user = await loadIssueBody(id); // not used in the system prompt
return model.generate({
system: trusted,
user: "Static placeholder",
});
}
由于不受信任的数据从未到达 system 字段,该规则应当保持静默。
在 JSON 中声明预期结果
测试套件的契约存储在 expected-alerts.json 中。它列出了必须触发的警报以及明确禁止的路径:
{
"required": [
{
"ruleId": "USE_ACTUAL_RULE_ID",
"pathSuffix": "positive/direct-flow.ts"
},
{
"ruleId": "USE_ACTUAL_RULE_ID",
"pathSuffix": "positive/helper-flow.ts"
}
],
"forbiddenPathSuffixes": [
"negative/trusted-instruction.ts"
]
}
请将 USE_ACTUAL_RULE_ID 替换为 CodeQL 文档或 SARIF 输出中显示的标识符。不要猜测 ID;精确的字符串对于 CI 检查至关重要。
将夹具接入 CI
- 将流水线中使用的 CodeQL CLI 版本固定为 2.26.0(或更高版本)。
- 在运行夹具之前,从当前检出的代码构建一个临时数据库。
- 运行查询,捕获警报,并将其与
expected-alerts.json进行对比。 - 如果任何必要的警报消失,或者禁止的路径开始触发警报,则导致构建失败。
不要断言整个仓库的警报总数——无关的更改可能会增加警报数量,从而导致误报失败。
升级后需要注意的事项
当您将 CodeQL 升级到更高版本时:
- 必要的警报仍然出现 – 继续进行常规审查。
- 必要的警报消失了 – 阻断构建;调查是新版本更改了查询逻辑,还是代码更改破坏了数据流。
- 出现了新的正向位置 – 在确认其为真实的注入路径后,将其添加到
required列表中。 - 负向控制项开始触发警报 – 重新审视缓解策略;该规则可能变得更加严格了。
静态分析无法证明模型在运行时会如何反应。应使用对抗性测试来补充回归测试套件,通过实际向模型发送精心构造的提示词并验证其响应。
总结
CodeQL 2.26.0 让您能够在提示词注入漏洞发布前将其捕获,但前提是您必须通过专注的回归测试夹具来锁定这种能力。通过在 JSON 契约中定义不受信任的源、真实的 SDK 汇聚点以及明确的预期,您可以将静态分析规则转化为一道关卡,既能防止回归,又能迫使团队持续关注快速演变的攻击面。
