AI セキュリティスキャナーが呼び起こす技術的負債
AIセキュリティスキャナーが、長年眠っていたバグを重大な脆弱性に変えつつあります。溢れ出る脆弱性の山を、溺れずに仕分けるための実践的な教訓をまとめました。

2024年、PHPプロジェクトのバグトラッカーに一件のバグ報告が上がりました。内容はメモリ不足(out-of-memory)の状態で、プロセスが落ちるという、厄介ではあるものの、バグの世界ではよくある「まあ仕方ない」案件です。そのまま放置されていました。ところが、OpenAIがスポンサーとなったPHPの独立監査の中で、セキュリティ企業Califの研究者がAIセキュリティスキャナーと自身の深い専門知識を組み合わせ、約2週間かけてこのクラッシュを遠隔コード実行(RCE)へと発展させました。脆弱なPHPビルドを動かしているサーバーは、攻撃者のコードを実行させられたり、機密データを吐き出させられたりする危険があったのです。影響範囲には、WordPressやDrupalがPHP上で動き、PHPがウェブの驚くほど大きな割合を支えていることから、NASA、Wikipedia、ホワイトハウスのサイトまで含まれていました。
幸いにも、悪意ある攻撃者が到達する前にパッチが公開されたので、この話は無事に終わりました。ただ、関係した研究者たちは口をそろえて、これは終わりではなく始まりにすぎないと言っています。人間が書いてきた40年分のコードには膨大なバグが眠っていて、私たちはそれらを「クラッシュ、修正せず」に分類してきました。ところがAIツールの登場で、それらのクラッシュのどれが実は装填済みの武器なのかを突き止めるコストが劇的に下がったのです。ここ数週間、実際のシステムのセキュリティを担当している人たちと話をして、現場で何が変わるのかを聞いてきました。以下は調査報告ではなく、チームがすでに犯しているのを見てきた失敗と、うまくやった人たちがどう対処したかをまとめたものです。
失敗その1:クラッシュバグを「面倒だけど無害」として片付ける
今回の中心にあるPHPのバグは、隠れていたわけではありません。報告され、認識され、そして優先度を下げられていました。理由は「メモリ不足エラーを起こすだけ」だったからです。これは成熟したソフトウェア組織で最も多い優先度判断の失敗であり、AIを使ったエクスプロイト開発によって、そのツケが急に高くつくようになりました。今回の開示をめぐるインタビューで、この種の研究に取り組んできたコロンビア大学のSuman Jana教授は、この力学を率直に説明しています。どの大規模プロジェクトにも、誰も真剣に取り合わないクラッシュ報告の山が常にある。なぜなら、クラッシュをエクスプロイトに変えるには、退屈で極端に個別具体的な推論を長く連鎖させる必要があるからだ、と。人間はその作業を避けます。しかしモデルは退屈しないのです。
この区別がなぜ重要なのか、仕組みの面から説明します。制御されないデータでヒープを破壊するメモリバグは、通常はプロセスを落とすだけです。アロケータが不整合に気づいて中断するか、プロセスがゴミ値を参照してセグメンテーション違反で落ちるかのどちらかです。しかし同じバグでも、サイズと形を慎重に調整した入力を与えれば、書き込みプリミティブに化けてしまいます。
void handle_request(const char *input, size_t len) {
char *buf = malloc(64);
memcpy(buf, input, len); /* len never validated */
/* len = 65: heap metadata trashed -> likely abort, filed as 'crash' */
/* len = attacker-tuned: adjacent object's function pointer overwritten */
process(buf); /* calls through the corrupted pointer -> their code runs */
free(buf);
}
この二つの結果を分けるのはバグそのものではなく、入力です。どのビットを、どの順番で上書きすれば、選んだアドレスで実行を成立させられるかを突き止める作業は骨が折れますが、機械的です。そして「骨は折れるが機械的」な作業こそ、言語モデルが最も得意とする領域の中心なのです。この失敗への対処は、技術的な話の前に、考え方を変えることから始まります。メモリ安全性のないコードについて、「クラッシュするだけ」を重大度の判断材料にするのをやめましょう。 CやC++のクラッシュ(あるいはPHPのようなインタプリタのC言語コア部分のクラッシュ)は、まだ開発されていないエクスプロイトにほかなりません。新しいトリアージの問いは「クラッシュするか」ではなく「破壊が攻撃者に影響されうるか」です。信頼できない入力が、不正な書き込みのサイズ、オフセット、内容のいずれかを制御できるなら、動作するエクスプロイトがまだ誰にも示されていなくても、悪用可能だと想定して優先度を上げてください。デモ済みのエクスプロイトが数年ではなく数週間で作られる今だからこそ、なおさらです。
失敗その2:スキャナーの出力をバックログのように扱う
二つ目の失敗パターンは、チームが最新のAI支援スキャナーを、20年前のコードを含む資産全体に実際に走らせた瞬間に現れます。ツールは数百件の検出結果を返し、ダッシュボードは真っ赤になります。するとチームは、バックログが教え込んできた通りに動きます。重大度スコアで並べ替え、上から順に潰していくのです。半年後には40件を直したものの、件数はむしろ増えており、チームの士気は崩壊しています。うまく対処しているエンジニアたちは、ほぼ全員が同じことを言っていました。AIスキャナーの出力はバックログではなく、生の情報であり、チケットを一枚切る前にアナリストによるトリアージが必要だと。
この体制で効果的なトリアージを行うには、重大度スコアではなく、エクスプロイトの経路に関する問いを立てる必要があります。
- 脆弱なコードは信頼できない入力から到達可能か? ローカル設定ファイルしか処理しない関数のヒープオーバーフローと、リクエスト処理で起きる同じオーバーフローは、まったく別物です。
- 今まさにインターネットに公開されているか? 「公開されるべきか」ではなく、「実際に公開されているか」を確認してください。アーキテクチャ図ではなく、ロードバランサーの設定とDNSを見るべきです。
- 入力が破壊を制御できるか? 不正なアクセスのサイズ、オフセット、内容を入力で左右できるかどうか。これは前節で述べた、クラッシュとエクスプロイトを分ける線そのものです。
- エクスプロイトに成功すると何が手に入るか? 再起動される隔離されたワーカーでのクラッシュと、データベースの認証情報を持つプロセスでのクラッシュは、まったく別の問題です。
- 野生でのエクスプロイトや、公開された解説記事はあるか? これがあれば、スコアに関係なく何よりも優先してキューの先頭に移すべきです。
最後の点は強調しておく価値があります。CVSSのようなスコアは、人間が人間のペースでバグを見つける時代に設計されたものです。そのため、理論上の重大度を、到達可能性よりも体系的に過大評価してしまいます。スキャナーが理論上の重大度を工業的な規模で生み出せるようになると、チームは溺れてしまいます。最もうまく対処しているチームは、パイプラインを逆転させています。まず到達可能性と露出を見て、次に重大度を見る。構造上到達不可能なものは、修正するのではなく、文書化してクローズするのです。修正版を出荷すれば、それ自体に回帰リスクが伴い、そのリスクの予算は本当に重要な検出結果のために取っておく必要があるからです。また、防御側の観点では、AIによるコード理解チェックがここで真価を発揮します。バグを見つけたのと同じツールが、「ネットワーク境界からこの関数までのすべての呼び出し経路を示せ」という問いに数分で答えてくれることが多いのです。これは以前なら、一件あたり熟練エンジニアが丸一日かけていた作業でした。

失敗その3:パッチ適用が検出のスピードに追いつけると考える
ここで、すべてのセキュリティチームの部屋にポスターとして貼っておくべき非対称性があります。PHPのバグのエクスプロイト経路を見つけるのに、AI支援を受けた一人の熟練研究者がかかったのは約2週間でした。一方、パッチの開発には4人のエンジニアが2か月を費やしています。発見は探索問題です。通るべき経路を一本見つければいい。修正は証明に近い問題です。すべての経路を塞ぎ、しかも攻撃されていない何十億ものインストールが、これまで通り動き続けなければなりません。防御側の仕事は攻撃側より厳密に難しく、AIは防御側よりもはるかに速く、そのギャップの攻撃側を縮めてしまいます。
この非対称性を無視したチームは、二つの悪い賭けのどちらかをします。開示のプレッシャーに合わせて急いでパッチを出し、回帰を出荷してしまうか、絶望して完璧な修正を待つあいだ何もしないか。PHPのパッチは、自社のスタックを公開しているウェブサイトのおよそ4分の3が使うインタプリタの内部に触れていたので、修正が失敗したときの影響範囲は文明規模です。その中で、この期間をうまく乗り切った実務者たちは、英雄的ではないものの、より効果的なことをしました。「今すぐ緩和する」と「正しく修正する」を分けたのです。 緩和策は安価で、速く、可逆的です。リバースプロキシで該当エンドポイントを制限する、エクスプロイトの入力形式をブロックするWAFルールを入れる、脆弱なコードパスをフラグの裏に隠す、本物のパッチが成熟するまでサービスに到達できる人を減らす。といった具合です。以前パッチか緩和か、という判断が道徳的な失敗ではなく本物のトレードオフであることを書きましたが、今回の件はその教訓を地球規模で示したものです。
誰も聞きたくない系の補足があります。今や重要な指標は「緩和までの平均時間(MTTM)」です。「ホワイトハットが見つけたバグ」と「もっと悪質な誰かが見つけるバグ」の間の猶予期間が縮んでいるからです。PHPのケースでは、研究者が責任ある行動を取り、財団が迅速に動いたため、この猶予が保たれました。同じ時期に、Brotli圧縮ライブラリは対照的な結末を迎えています。よく似たバグが8年間コードに残り、gitでは修正されたものの8か月間リリースされず、その間に少なくとも4人の研究者が独立に再発見しました。発見が安価で並行して起きるとき、「いずれ誰かが見つけるだろう」は慰めの言葉ではなく、スケジュールの保証になってしまうのです。
失敗その4:LLMが書いたという理由だけで修正を信じる
これらすべてに対する楽観的な反論はこうです。確かに今日のところは、AIが修正するより速くバグを見つけてしまう。しかしAIは修正もやってくれるはずだ、と。Anthropicは10月に、まさにこの方向のツールを公開しました。LLMがパッチを生成し、別のモデルが、そのパッチが意図した振る舞いを保つことを機械的に検証可能な証明として生成し、小さくて愚直で信頼できる証明チェッカーがその証明を検証する仕組みです。正しくなければならないのはチェッカーだけで、しかもチェッカーはほとんど何もしないので、正しくすることが可能です。Jana教授もこの方向性には本当に乗り気で、その理由はよくわかります。修正を発見と同程度に自動化できる、初めての現実的な道筋だからです。
しかし、「AI生成のセキュリティパッチ」というワークフローを導入したチームを二つ見てきましたが、どちらも同じ間違いをしていました。モデルのパッチを解決策として扱い、候補として扱わなかったのです。一方のケースでは、パッチは報告されたヒープオーバーフローに長さチェックを追加して塞ぎました。ところが、オーバーフローは6行離れた第二のアロケーション経路に移動しており、モデルはそれをモデル化していませんでした。そのパッチは、明らかに正しく見えたのでレビューを通過しました。まさにこれが失敗モードです。LLMのパッチは人間のパッチと違って流暢で、その流暢さが精査を鈍らせるのです。生成パッチから価値を得たチームは、次の三つのゲートを、この順番で追加していました。
- エクスプロイト入力を含む回帰テストスイート。 検出のきっかけとなった概念実証を、恒久的なテストにします。スキャナーがクラッシュする入力を出していれば、それが最初の新しいテストケースになり、追加コストはゼロです。
- バリアント探索。 マージ前に、誰か(あるいは何か)が「同じパターンはほかのどこに出現するか」を確認します。体系的なパターンのひとつのインスタンスだけを直すと、CVEの続編が生まれてしまいます。
- 差分ではなく不変条件の独立検証。 「リクエスト処理から、検証されていない長さで memcpy に到達する経路は存在しない」のように、今後成立していなければならないことを明文化し、そのことをテスト、静的解析、あるいは形式手法のツールで確認します。差分だけをレビューしても、それはモデルの宿題を読み直しているにすぎません。
形式検証は成熟するにつれて役立つでしょうが、信頼できる仕組みにするには役割分担に注意が必要です。創造的な部分は幻覚を起こしてもかまいません。最終的な審判が機械的で小さいからです。これは、使っているツールに関係なく盗むべき設計原則です。LLMが生成するもの(発見、パッチ、証明のいずれも)は、同じ失敗モードを共有しないものによって終端させるべきです。検証のステップが別のプロンプトであれば、それは何も検証していないのと同じです。

失敗その5:誰かがコードを監査してくれていると思い込む
ここで扱ったすべての技術的な教訓の底には、経済的な教訓があります。PHPは公共インフラに近い規模のソフトウェアです。WordPressやDrupal、そして宇宙機関や政府の公式サイトまで含むウェブの長い裾野を支えており、そのセキュリティは、小規模で資金の限られたPHP Foundationと、ボランティアに頼っています。オープンソースの心地よい神話は、「目玉が十分にあれば、バグはすべて浅くなる」というものでした。しかし実際の30年間は、誰もがその目玉は誰か別の人のものだと思い込んでいた、というのが実態です。今やAIが、攻撃者にも防御者にも分け隔てなく目玉を供給しています。つまりこの神話は、ついに現実になりつつあるのです。ただし、敵対的な形で、しかも締め切り付きで。
PHP2Shellの開示がうまくいったのは、珍しい体制があったからです。OpenAIが外部監査をスポンサーし、資金の潤沢な企業が作業を行い、Wizが検出結果の確認と拡大を支援しました。しかし、それは善意であって仕組みではなく、依存関係の中にある、重要でありながら資金のないプロジェクト何千件にはスケールしません。組織の本番環境がオープンソースのインタプリタ、フレームワーク、ライブラリに依存しているなら(そしてほぼ確実に依存しているはずです)、重要な対策は地味なものです。財団に資金を出し、監査をスポンサーし、エンジニアの時間を提供する。そして自前のスキャナーを走らせて依存関係に問題を見つけたら、フォークにパッチを当てて終わりにせず、上流に報告する。ジョンズ・ホプキンス大学のMatthew Green氏が予測する世界では、普通の人々が動揺するほど大きなインシデントが起きるまで、構造的には何も変わりません。その間の彼のアドバイスは地味ですが正しいものです。アップデートの通知が出たら、来週ではなく今すぐ入れてください。前回のPHPの大規模な開示のサイクルでも、同じ教訓を痛い形で学びました。対応が追いつかなかったチームにとっては、いっそ接続を切る方が、小細工の修正より効いたのです。
目玉は現れました。ただ、どちらの側につくかには無頓着なだけです。
AIが発見した脆弱性に対する月曜朝のプレイブック
ここまでの内容を、もし私がレガシーコードを本番で抱えるエンジニアリング組織を率いていたら今四半期に実際にやることとして、短いリストにまとめます。この記事を読んでいるなら、あなたの本番環境にもレガシーコードがあるはずです。
- 積極的にパッチを適用し、かかる時間を計測する。 更新までのレイテンシーは、今や安全性を保つしきい値が縮みつつあるセキュリティ統制です。必要になる前に、自社の数字を把握しておいてください。
- 既存のクラッシュバグのバックログを、重大度スコアではなく悪用可能性の問いで再トリアージする。 信頼できない入力が破壊を制御できるか? メモリ安全性がなく到達可能なものは、優先度を上げます。
- 自分たちでAI支援スキャンを、最も古いコードから順に走らせる。 再発見の限界コストはほぼゼロになったので、攻撃対象領域で見つけられるものは必ず見つかると想定すべきです。先に見つけてください。
- インシデント対応のプロセスで、緩和と修正を分ける。 プロキシのルール、フィーチャーフラグ、アクセス制限を事前に用意しておきましょう。時間を稼ぐことは正当な戦略ですが、時間が必要ないふりをするのは正当ではありません。
- 生成されたパッチは、差分がどれだけきれいに見えるかではなく、エクスプロイト入力のテストとバリアント探索を通過したかで承認する。
- 依存しているアップストリームのプロジェクトに、お金か時間を投資する。 今回のバグは、スポンサー付きの監査によって見つかりました。これは偶然ではなく、仕組みが機能している証拠です。そしてそれは例外的なことであるのをやめ、当たり前にならなければなりません。
この時代が「技術的負債の返済」なのか、それとも機能に寄与しない修正作業のルームランナーに永遠に乗せられることなのか、という議論には一理あります。おそらく両方でしょう。どんなに些細なパッチにも、次の微妙なバグのリスクが伴いますし、完成したシステムなどありません。それでも私は、負債が監査人やホワイトハットによって取り立てられている世界に住みたい。何にも飲み込まれていないからといって、底なし沼を岩盤だと言い続ける世界よりはずっとましです。PHP2Shellは、パッチと安堵のため息で終わりました。次の一件は、もう誰かのクラッシュキューに「無害」として登録されています。自分のキューも、ぜひ確認してみてください。


