GitHub 的 CodeQL 2.26.0 版本新增了一个内置查询,用于识别 AI 提示词注入(prompt-injection)模式,这一变化已导致 CI 流水线开始标记新的风险。仅进行升级是不够的——团队需要一套回归测试套件,以确保随着代码的演进,该规则始终保持有效。

为什么回归测试夹具(regression fixture)至关重要

提示词注入允许攻击者将恶意指令混入提示词中,随后语言模型会执行这些指令。通过新的查询,静态分析可以追踪从不受信任源(untrusted source)到模型调用汇聚点(model-calling sink)的数据流。如果只是简单地开启该规则而从未进行验证,后续的代码重构可能会破坏数据流路径,导致警报悄无声息地消失。回归测试夹具可以捕获那些应当触发(或不应当触发)规则的确切路径,将静态分析结果转化为构建过程强制执行的契约。

可靠夹具的三个要素

  1. 不受信任的源 (Untrusted source) – 任何从受信任代码库外部引入数据的函数(例如:GitHub issue 正文、webhook 负载)。
  2. 提示词构建 (Prompt construction) – 组装模型请求的代码,通常是调用客户端 SDK。
  3. 模型汇聚点 (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

  1. 将流水线中使用的 CodeQL CLI 版本固定为 2.26.0(或更高版本)。
  2. 在运行夹具之前,从当前检出的代码构建一个临时数据库。
  3. 运行查询,捕获警报,并将其与 expected-alerts.json 进行对比。
  4. 如果任何必要的警报消失,或者禁止的路径开始触发警报,则导致构建失败。

不要断言整个仓库的警报总数——无关的更改可能会增加警报数量,从而导致误报失败。

升级后需要注意的事项

当您将 CodeQL 升级到更高版本时:

  • 必要的警报仍然出现 – 继续进行常规审查。
  • 必要的警报消失了 – 阻断构建;调查是新版本更改了查询逻辑,还是代码更改破坏了数据流。
  • 出现了新的正向位置 – 在确认其为真实的注入路径后,将其添加到 required 列表中。
  • 负向控制项开始触发警报 – 重新审视缓解策略;该规则可能变得更加严格了。

静态分析无法证明模型在运行时会如何反应。应使用对抗性测试来补充回归测试套件,通过实际向模型发送精心构造的提示词并验证其响应。

总结

CodeQL 2.26.0 让您能够在提示词注入漏洞发布前将其捕获,但前提是您必须通过专注的回归测试夹具来锁定这种能力。通过在 JSON 契约中定义不受信任的源、真实的 SDK 汇聚点以及明确的预期,您可以将静态分析规则转化为一道关卡,既能防止回归,又能迫使团队持续关注快速演变的攻击面。