プログラミング・スキルアップ

CodeQL 2.26.0は仕事で使えるか|料金と実測レビュー2026

読了時間: 約27分

この記事の要点

  • CodeQL 2.26.0(2026年7月10日公開)で js/system-prompt-injection クエリが追加され、ユーザー入力がシステムプロンプトに流れ込む経路を静的に検出できるようになった
  • 実際に脆弱なExpressアプリを書いて走らせたところ、DB作成14.4秒・解析21.3秒で4ステップのデータフローごと検出。許可リスト方式に直すと検出はゼロになった
  • 料金はパブリックリポジトリなら無料、プライベートはGitHub Advanced Securityが必要。ローカルCLIは無料だがクエリパック取得にghcr.ioへの通信が要る

プロンプトインジェクションの対策といえば、これまでは実行時の話だった。入力をフィルタする、出力を検証する、ガードレールのモデルをもう一枚挟む。どれも「動かしてから守る」アプローチだ。

CodeQL 2.26.0が追加したのはその手前の防御線になる。コードを書いた段階で、信頼できない値がシステムプロンプトへ到達する経路をデータフロー解析で見つけ出す。SQLインジェクションを静的解析で潰してきたのと同じ手口を、LLMアプリに持ち込んだ格好だ。

発表は2026年7月10日。日本語で js/system-prompt-injection を扱った記事はまだ見当たらない。そこで551MBのCLIを落としてきて、26行の脆弱なコードと修正版の両方を食わせた。実測値をそのまま並べる。

CodeQL 2.26.0で何が変わったか

2.26.0の目玉は、JavaScript/TypeScript向けのシステムプロンプトインジェクション検出クエリだ。GitHub公式のchangelogでは、信頼できないユーザー提供の値がAIモデルのシステムプロンプトに流れ込み、モデルの挙動を攻撃者が操作できる状態を検出する、と説明されている。

同じリリースにKotlin 2.4.0対応と、実験的なSSRFクエリも入っている。IPv4のプライベート範囲だけ弾いてIPv6の遷移アドレス形式で迂回できてしまうガードを見つけるものだ。地味だが効果が大きいのはこちらかもしれない。

追加された3つのクエリ

クエリID 対象 位置づけ
js/system-prompt-injection JavaScript / TypeScript 正式版。デフォルトのsecurityスイートに含まれる
javascript/ssrf-ipv6-transition-incomplete-guard JavaScript / TypeScript 実験的(experimental)
Kotlin 2.4.0 抽出対応 Kotlin 言語サポート更新

クエリのメタデータを読む

クエリ本体はCodeQLリポジトリの javascript/ql/src/Security/CWE-1427/SystemPromptInjection.ql にある。ヘッダのメタデータを見ると、このクエリの性格がわかる。

/**
 * @name System prompt injection
 * @description Untrusted input flowing into a system prompt, developer prompt,
 *              or tool description of an AI model may allow an attacker to
 *              manipulate the model's behavior.
 * @kind path-problem
 * @problem.severity error
 * @security-severity 7.8
 * @precision high
 * @id js/system-prompt-injection
 * @tags security
 *       external/cwe/cwe-1427
 */

注目したいのは @precision high の指定だ。CodeQLの慣習では、precisionがhigh以上のクエリだけがデフォルトのコードスキャンで走る。つまり、GitHub上でcode scanningを有効にしているリポジトリは、何も設定しなくてもこの検出が効き始めている。

CWE-1427は「生成AIへの不適切な入力の中和」を指す分類。severity 7.8はCVSSでいうHighの下限あたりで、SQLインジェクション系の伝統的な脆弱性と同じ重みで扱われる。

path-problemであることの意味

@kind path-problem は「どこが危ないか」だけでなく「どこから来てどこへ行くか」まで報告する形式を指す。単なる文字列マッチではなく、変数の受け渡しを追跡した経路が結果に付いてくる。後述の検証でも、リクエストパラメータからシステムメッセージまでの4ステップが出力された。

CodeQLを手元で動かして検出させた記録

結論から書くと、動く。実際に走らせた結果は脆弱版が1件検出、許可リスト方式に直した版が0件だった。以下は手を動かした順の記録で、時間はすべてこの環境での実測値だ。

CLIの入手とバージョン確認

CodeQL CLIはGitHubのリリースページからzipで落とす。展開してパスを通すだけで、インストーラの類は不要だ。

curl -sL -o codeql.zip \
  https://github.com/github/codeql-cli-binaries/releases/download/v2.26.0/codeql-linux64.zip
unzip -q codeql.zip
./codeql/codeql version

出力はこうなる。

CodeQL command-line toolchain release 2.26.0.
Copyright (C) 2019-2026 GitHub, Inc.
Unpacked in: /tmp/codeql

zipは551MBあった。回線によっては待たされる。ここで一つ落とし穴があるのだが、それは後半のつまずきポイントでまとめる。

検出させるための脆弱なコード

クエリの狙いをそのままなぞる形で、リクエストパラメータをシステムメッセージに連結するExpressアプリを用意した。26行のミニマルな構成だ。

const express = require("express");
const OpenAI = require("openai");

const app = express();
const client = new OpenAI();

app.get("/summarize", async (req, res) => {
  const role = req.query.role;

  const completion = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: "You are a helpful assistant. " + role },
      { role: "user", content: "Summarize today's sales." }
    ]
  });

  res.send(completion.choices[0].message.content);
});

app.listen(3000);

req.query.role をそのままシステム側の指示に足している。実運用でこれをやると、クエリ文字列に「これまでの指示を無視しろ」と書くだけで振る舞いを乗っ取られる。

データベース作成にかかった時間

CodeQLはソースを直接読まない。まずコードを関係データベースに変換し、そこへクエリを投げる。この変換がデータベース作成だ。

codeql database create /tmp/pi-db \
  --language=javascript \
  --source-root=/tmp/pi-demo

26行のプロジェクトで計測したところ、所要は約14秒だった(実測14.4秒 / user 22.8秒 / sys 5.3秒)。生成されたDBはrelations 3.07 MiB、string pool 4.78 MiBある。ソース本体は1KBにも満たないのに、この容量になる。Node.jsの外部型定義まで一緒に取り込むためだ。

Finished writing database (relations: 3.07 MiB; string pool: 4.78 MiB).
TRAP import complete (6.6s).
Successfully created database at /tmp/pi-db.

real	0m14.427s
user	0m22.804s
sys	0m5.318s

ここは見落としがちだが、CIに組み込むときに効いてくるのはこのDB作成時間のほうだ。クエリ本数を絞っても、DB作成は毎回フルで走る。

クエリ実行と検出結果

クエリを1本だけ指定して解析をかけ、結果をSARIFで受け取る。

codeql database analyze /tmp/pi-db \
  qlpacks/codeql/javascript-queries/2.4.0/Security/CWE-1427/SystemPromptInjection.ql \
  --format=sarif-latest \
  --output=/tmp/pi-result.sarif

実際に計測した値は評価12.6秒、全体で約21秒、検出は1件。

results: 1
ruleId: js/system-prompt-injection
message: This system prompt depends on a [user-provided value](1).
  loc: app.js {'startLine': 13, 'startColumn': 34, 'endColumn': 72}
  codeFlows: 1
    step: app.js 8  req.query.role
    step: app.js 8  role
    step: app.js 13 role
    step: app.js 13 "You ar ...  + role
driver: CodeQL 2.26.0

4ステップのデータフローが出た。8行目の req.query.role がsource、13行目の文字列連結がsink。途中の変数代入まで律儀に並べてくれる。レビューで指摘するとき、この経路がそのまま説明資料になる。

修正版では検出が消えるか

誤検知の裏返しとして、直したときに消えるかどうかも確かめた。ユーザー入力を許可リストの添字にだけ使い、システムプロンプトへ渡すのは定数側にする。差分は3行だ。

const ALLOWED_ROLES = {
  sales: "Focus on sales figures.",
  support: "Focus on support tickets."
};

app.get("/summarize", async (req, res) => {
  const instruction = ALLOWED_ROLES[req.query.role] ?? "";

  const completion = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: "You are a helpful assistant. " + instruction },
      { role: "user", content: "Summarize today's sales." }
    ]
  });

  res.send(completion.choices[0].message.content);
});

同じ手順でDBを作り直して解析すると、結果はこうなる。

[1/1 eval 9.7s] Evaluation done
FIXED results: 0

きれいに消えた。攻撃者が制御できるのは「どの定数を引くか」だけになり、システムプロンプトの中身そのものには手が届かない。文字列連結のコードは1文字も変えていないのに、検出は1件から0件に切り替わった。ツールが追っているのは形ではなく値の出所だ、という証拠になる。

実測サマリー(26行のExpressアプリ)

  • DB作成: 14.4秒 / relations 3.07 MiB
  • 解析(クエリ1本): 21.3秒、うち評価12.6秒
  • 脆弱版: 1件検出、4ステップのパス付き
  • 修正版(許可リスト): 0件

どのSDKのどこがsinkになるのか

検出範囲はモデル定義ファイルで決まる。CodeQLはSDKごとの「危険な引数」をYAMLで持っており、ここに載っていない呼び出しは検出されない。使っているライブラリが対象かどうかは、この一覧で判断できる。

対応SDKと定義行数

バンドル同梱の javascript-all 2.8.0 には23個のモデル定義がある。そのうちシステムプロンプト系のsinkを持つのは5つだった。

モデル定義 sink定義行数 主な対象
openai.model.yml 12 Responses API、Assistants、Realtime、@openai/agents
anthropic.model.yml 6 Messages APIのsystem、legacy completion
google-genai.model.yml 5 systemInstruction、cachedContent
langchain.model.yml 5 SystemMessage、プロンプトテンプレート
openrouter.model.yml 3 互換エンドポイント経由のsystem

OpenAI SDKのsink定義を覗く

YAMLの中身はアクセスパスの列挙になっている。「このクライアントの、このメソッドの、この引数のこのプロパティ」を1行ずつ指定する形だ。

- ["openai.Client", "Member[responses].Member[create].Argument[0].Member[instructions]", "system-prompt-injection"]
- ["openai.Client", "Member[beta].Member[realtime].Member[sessions].Member[create].Argument[0].Member[instructions]", "system-prompt-injection"]
- ["openai.Client", "Member[beta].Member[assistants].Member[create].Argument[0].Member[instructions]", "system-prompt-injection"]
- ["openai.Client", "Member[beta].Member[threads].Member[runs].Member[create].Argument[1].Member[instructions,additional_instructions]", "system-prompt-injection"]
- ["@openai/agents", "Member[Agent].Argument[0].Member[instructions,handoffDescription]", "system-prompt-injection"]
- ["@openai/agents", "Member[Agent].Instance.Member[asTool].Argument[0].Member[toolDescription]", "system-prompt-injection"]

最後の2行が興味深い。エージェントSDKの handoffDescriptiontoolDescription まで対象に入っている。ツールの説明文は人間向けのメモに見えるが、モデルはそれを読んで呼び出しを判断する。実質的にプロンプトの一部だ、という設計判断が透けて見える。

エージェント構成での攻撃面についてはAIエージェントの仕組みと活用事例で全体像を押さえておくと、なぜツール定義まで守る必要があるのかが腑に落ちる。

検出されないケース

自作のHTTPラッパー

公式SDKを使わず fetch で直接APIを叩いている場合、モデル定義に一致しないため素通りする。

Python / Go のコード

2.26.0時点でこのクエリはJavaScript/TypeScript限定。同じ構造でもPython側には対応クエリがない。

DB経由で入る値

一度データベースに書き込んでから読み出す設計だと、sourceとして認識されず経路が途切れる。

RAGで取り込む外部文書

検索結果や取得したWebページの内容がプロンプトに入る間接的な経路は、静的解析の守備範囲外になる。

4つ目が実務では一番怖い。間接プロンプトインジェクションと呼ばれる形で、外部から取り込んだ文書に仕込まれた指示をモデルが実行してしまう。ここは設計とプロンプト側の防御で対処するしかない。プロンプトエンジニアリングの基本テクニックで扱う指示の階層づけが、そのまま守りの一手になる。

CodeQLの料金|無料で済む範囲と課金の境界

料金の線引きはシンプルで、リポジトリが公開かどうかで決まる。パブリックなら無料、プライベートならGitHub Advanced Securityの契約が要る。

3つの利用形態と費用

利用形態 費用 向いている場面
パブリックリポジトリのcode scanning 無料 OSS、個人の公開プロジェクト
プライベートリポジトリ GitHub Advanced Security(Code Security)の契約が必要 社内プロダクト、受託案件
ローカルCLI ライセンス費用なし(用途制限あり) 導入前の評価、手元での事前チェック

ローカルCLIは無料で使えるが、GitHubのライセンス条項では自動化された解析への利用に条件が付く。今回の検証のように手元で1回叩いて確かめる、pushする前に自分のコードを見る。この範囲なら問題にならない。

プライベートで使うときの実質コスト

Advanced Securityはコミッター課金だ。1か月にpushした人数だけ数える。レビュー専任は対象外。5人全員が毎月コミットするチームなら、素直に5人分になる。

もったいないと感じるのが、機能がまとめ売りになっている点。プロンプトインジェクション検出だけ欲しくても、シークレットスキャンや依存関係レビューとセットの契約になる。逆に言えば、既にAdvanced Securityを入れている組織はこのクエリを追加費用ゼロで手に入れている。

導入前に確認しておきたいこと

既にcode scanningを有効にしているリポジトリでは、CodeQLの新バージョンは自動でデプロイされる。つまり2.26.0への更新作業は不要で、ある日から js/system-prompt-injection のアラートが出始める。既存のLLMアプリを抱えているなら、いきなりアラートが増えても慌てないよう先に中身を把握しておきたい。

AI関連のセキュリティ人材の需要そのものが伸びている。この分野をキャリアとして見るならAIセキュリティエンジニアの市場動向に年収レンジと求められるスキルをまとめてある。

導入手順|ローカルとGitHub Actions

最短で試すならローカルCLI、継続的に回すならGitHub Actions。用途が違うので両方の入り口を書いておく。

GitHub Actionsで回す

リポジトリのSecurity設定からcode scanningを有効にすると、デフォルト設定なら追加のファイルは要らない。挙動を細かく制御したい場合だけワークフローを書く。

name: CodeQL
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 3 * * 1"

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      contents: read
    strategy:
      matrix:
        language: [javascript-typescript]
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          queries: security-extended
      - uses: github/codeql-action/analyze@v3

queries: security-extended を指定すると、デフォルトより広い範囲のクエリが走る。ただし js/system-prompt-injection はprecision highなので、この行がなくても検出される。

ローカルで単発チェックする

CLI単体版にはクエリパックが入っていない。バンドル版を落とすのが確実だ。

# クエリパック同梱のバンドル版
curl -sL -o codeql-bundle.tar.gz \
  https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.26.0/codeql-bundle-linux64.tar.gz
tar xzf codeql-bundle.tar.gz
export PATH="$PWD/codeql:$PATH"

# DB作成 → 解析
codeql database create ./db --language=javascript --source-root=.
codeql database analyze ./db \
  codeql/javascript-queries:Security/CWE-1427/SystemPromptInjection.ql \
  --format=sarif-latest --output=result.sarif

バンドル版は822MBある。CLI単体の551MBと合わせて1.3GB以上を落とすことになるので、CI環境でこれをやるならキャッシュを効かせたい。

SARIFから結果を読む

SARIFはJSONなので、CIの中で件数だけ拾ってゲートにするのが手っ取り早い。

import json, sys

with open("result.sarif") as f:
    data = json.load(f)

results = data["runs"][0].get("results", [])
for r in results:
    loc = r["locations"][0]["physicalLocation"]
    print(r["ruleId"], loc["artifactLocation"]["uri"], loc["region"]["startLine"])

sys.exit(1 if results else 0)

GitHub上で使うなら codeql-action/analyze がSARIFのアップロードまで面倒を見てくれるので、この自作処理は不要になる。手元やGitHub以外のCIで回すときの手段だと思っておけばいい。

つまずいたポイント3つ

CodeQLは公式ドキュメントどおりに進めて素直に通ったわけではない。実際に踏んだ3件を、遭遇した順に挙げる。

1. CLI単体版にはクエリが入っていない

一番はまったのがこれだ。codeql-linux64.zip はツールチェーン本体だけで、解析クエリは別配布になっている。バージョン確認もDB作成も通るので、解析の直前まで気づけない。

正攻法は codeql pack download でクエリパックを取ってくる方法だが、これがコンテナレジストリへの通信を伴う。egressを絞った環境だとここで止まる。

$ codeql pack download codeql/javascript-queries
Package specifications to check for download: codeql/javascript-queries
A fatal error occurred: Error getting package
https://ghcr.io/v2/codeql/javascript-queries/blobs/sha256:29ae91...
('codeql/javascript-queries'): HTTP/1.1 403 Forbidden
Response body: request rejected: host not permitted

解決策は前述のバンドル版だ。codeql-bundle-linux64.tar.gz にはツールチェーンとクエリパックが同梱されており、GitHubのリリース配布なのでghcr.ioを経由しない。社内プロキシの申請が面倒な環境では、こちらを標準にしたほうが早い。

2. ヒープ不足の警告が出る

解析中に次の行が流れる。

Not caching stages during query-loading, since max heap size is only 1800 MB.

26行のプロジェクトでは実害がなく、21.3秒で完走した。ただし本番規模のリポジトリでこれが出るなら、中間結果のキャッシュが効かないぶん解析時間が伸びる。--ram オプションか JAVA_TOOL_OPTIONS でヒープを積むことになる。

3. クエリのパス指定が長い

バンドル内のクエリはバージョン番号入りのディレクトリに置かれる。手元の環境では qlpacks/codeql/javascript-queries/2.4.0/Security/CWE-1427/ だった。CodeQL本体が2.26.0でも、JavaScriptのクエリパックは2.4.0という別採番になっている。

バージョンを直書きするとパック更新のたびに壊れるので、パック名でクエリを指定する書き方に寄せておくと安定する。

codeql database analyze ./db \
  codeql/javascript-queries:Security/CWE-1427/SystemPromptInjection.ql \
  --format=sarif-latest --output=result.sarif

ディスクを1.3GB以上使う

CLI単体551MB、バンドル822MB。両方落とすと1.3GBを超える。加えて解析対象ごとにDBが積み上がるので、CI用の使い捨てコンテナだとディスク不足で落ちることがある。バンドル版だけに絞れば822MBで済む。

静的解析で防げない領域と併用すべき対策

このクエリが守るのは「コードに書かれた経路」だけだ。実行時に外から入ってくる内容は守備範囲の外にある。ここを理解せずに導入すると、対策済みだと誤認する危険がある。

守れる範囲と守れない範囲

攻撃経路 静的解析で検出 必要な対策
リクエストパラメータ→システムプロンプト 可能 許可リスト化、設計の見直し
ツール説明文への注入 可能 定数化、テンプレート分離
RAGで取得した文書内の指示 不可 出力検証、権限分離、実行時ガードレール
MCPサーバー経由で入る外部データ 不可 サーバーの信頼性評価、権限最小化
ユーザーがアップロードしたファイル 経路次第 内容の隔離、システム側と混ぜない

MCP環境での注意

外部サーバーからツールとその説明文を受け取る構成では、説明文自体が信頼できない入力になる。コードには何も書かれていないので、静的解析には映らない。

2026年7月28日の仕様改訂でMCPは認可周りが大きく変わる。MCP 2026-07-28 RCの変更点と移行ガイドに整理してあるので、サーバーを運用しているなら先に目を通しておきたい。自前でサーバーを書くならMCPサーバーの自作手順で権限の切り方から確認できる。

他のAIセキュリティツールとの住み分け

CodeQL

コードを書いた時点の経路を潰す。CIで機械的に回る。誤検知が少ない代わりに範囲は狭い。

LLMベースのレビュー

設計意図まで踏み込めるが、指摘の再現性が低い。CodeQLが拾えない文脈依存の問題向け。

実行時ガードレール

RAGや外部データ由来の注入に対する最後の砦。レイテンシとコストが乗る。

3つは競合しない。静的解析で潰せるものを先に潰しておけば、実行時ガードレールに任せる範囲が減ってコストも下がる。AIによる脆弱性検出という方向性ではOpenAI Codex Securityの自動検出・修正もあるので、アプローチの違いを比べておくと選びやすい。

開発フロー全体をどう組むかという視点では、GitHub Copilotエージェントモードの料金と合わせて、どこまで自動化にお金を払うかを一度は計算しておきたい。

よくある質問

Q. CodeQL 2.26.0に手動で更新する必要はありますか?

GitHub上でcode scanningを使っているなら不要です。新しいバージョンは自動でデプロイされます。ローカルCLIを使っている場合だけ、自分でzipを入れ替えます。

Q. Python製のLLMアプリでも検出できますか?

2.26.0時点では対応していません。このクエリはJavaScript/TypeScript専用です。Python側で同等の検出をしたい場合は、カスタムクエリを書くか、実行時の対策で補うことになります。

Q. 誤検知は多いですか?

クエリのメタデータにprecision highが指定されており、GitHubの基準では誤検知が少ない部類に分類されます。実際に検証した範囲でも、許可リスト方式に直した版は検出0件でした。ただし対象は26行の小さなコードなので、大規模なコードベースでの傾向は別途の確認が要ります。

Q. 無料で使えますか?

公開リポジトリなら無料。非公開はAdvanced Securityの契約が要ります。

Q. 解析にどれくらい時間がかかりますか?

26行のExpressアプリで検証したところ、CodeQLのデータベース作成は約14秒、クエリ1本の解析は約21秒でした。本番規模のリポジトリではDB作成の比重が大きくなるため、CIに組み込むならこちらの時間を基準に見積もります。

Q. 検出されたらどう直せばいいですか?

ユーザー入力をシステムプロンプトから切り離すのが基本です。選択肢が限られるなら許可リストの添字に使い、自由入力が必要ならユーザーメッセージ側に置きます。エスケープ処理での対処は推奨されません。

まとめ

LLMアプリを書いているなら、CodeQL 2.26.0のこのクエリは入れる価値がある。CodeQLを既に回しているリポジトリなら、なおさら判断は早い。理由は単純で、コストがほぼゼロだからだ。パブリックリポジトリなら無料、Advanced Securityを契約済みの組織なら追加費用なし。何もしなくても勝手に有効になる。

自分ならまずローカルのバンドル版を使う。理由は一つ、CLI単体版はクエリパックの取得でつまずくからだ。

期待しすぎないほうがいい部分もはっきりしている。守れるのはコードに書かれた経路だけで、RAGやMCP経由で入ってくる外部データの注入は素通りする。そこは実行時の設計で守るしかない。

それでも、これまで実行時対策一辺倒だったプロンプトインジェクション対策に、静的解析という選択肢が正式に加わった意味は小さくない。SQLインジェクションが「書いた時点で潰す」ものになったのと同じ道を、LLMアプリも歩き始めている。

手元のリポジトリで codeql database create を1回叩けばいい。26行で約14秒だった。中規模のプロジェクトでも、昼休みの間には結果が出る。