GitHubのCodeQL 2.26.0には、AIプロンプトインジェクションのパターンを検知する組み込みクエリが追加されました。この変更により、CIパイプラインですでに新たなリスクがフラグ立てされています。アップグレードだけでは不十分です。コードの進化に合わせてルールが効果を維持することを保証する、回帰テストスイートが必要です。

なぜ回帰フィクスチャが重要なのか

プロンプトインジェクションにより、攻撃者は言語モデルが後に従うような悪意のある指示をプロンプトに紛れ込ませることができます。新しいクエリを使用すると、静的解析によって信頼できないソースからモデル呼び出しのシンク(sink)までのデータを追跡できます。もしルールを単に有効にするだけで検証を行わなければ、後のリファクタリングによってデータフローパスが壊れ、アラートが静かに消えてしまう可能性があります。回帰フィクスチャは、ルールをトリガーすべき(あるいはトリガーすべきでない)正確なパスをキャプチャし、静的解析の結果をビルドが強制する「契約」へと変えます。

信頼できるフィクスチャの3つの要素

  1. 信頼できないソース – 信頼できるコードベース以外からデータを取り込む任意の関数(例:GitHubのIssueの本文、Webhookのペイロード)。
  2. プロンプトの構築 – モデルへのリクエストを組み立てるコード。通常はクライアントSDKの呼び出しです。
  3. モデルのシンク – プロンプトをモデルに送信するSDKメソッド。CodeQLのデータフローエンジンがシンクを認識するには、プロダクションスタックからの実際の呼び出しを見る必要があります。

これら3つすべてが揃ったときに初めて、クエリが実行されます。

テストファイルの構成

従来のレイアウトを採用することで、スイートの監査が容易になります。

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 がシンクであり、データがサニタイズ工程なしで渡されています。これはまさにクエリが検知するように設計されているものです。

2つ目のポジティブケースでは、データをヘルパー関数経由でルーティングし、解析が間接的なパスを追跡できることを証明する必要があります。

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() と呼ばれる関数が安全性を保証すると想定してしまうことです。静的解析ツールは関数名を証明として扱わないため、テストでは誤解を招くようなサニタイズのスタブを避けるべきです。

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は、プロンプトインジェクションのバグをリリース前にキャッチする能力を提供しますが、それは集中した回帰フィクスチャによってその能力を固定した場合に限られます。信頼できないソース、実際のSDKシンク、およびJSON契約における明確な期待値を定義することで、静的解析ルールを、リグレッションを防ぎ、急速に進化する攻撃対象領域に対して継続的な注意を強制するゲートへと変えることができます。