事故図鑑
AIで書いたコードからAPIキーが漏れ、不正課金された——本当の原因は放置したCVSS 10.0だった
AIで書いたアプリを公開した直後にAPIキーが盗まれ、見知らぬ大量バッチ処理で不正課金される——個人開発で実際によくある事故パターン。犯人探しの遠回りと、真因=数ヶ月放置した公開済みCVSS 10.0のRCEに辿り着くまで、そして再発を機械で防ぐ方法を、攻撃手順を伏せて防御目線でまとめます。
セキュリティに詳しくない個人開発者がよく踏む事故を、固有名詞をすべて伏せた事例として教訓に変えた記事です。攻撃の再現手順は載せません。狙いは「同じ轍を踏まないための防御」です。
- 種別
- APIキー漏洩 / 不正課金
- 重大度
- Critical(公開済み CVSS 10.0 を悪用)
- 真因
- Webフレームワークの認証前RCEを数ヶ月パッチせず放置
- 漏洩範囲
- 環境変数にあった全シークレット(鍵束ごと)
- 実行範囲
- 非特権ユーザーのコンテナ内に留まる(ホストrootは無事)
- 恒久対処
- ①修正版へ更新 ②全鍵ローテーション ③CVE機械監視
「課金を止めた」≠「対応完了」
請求を止めるのは止血。漏洩経路を塞ぐのは別の手術です。両方やって初めて対応完了です。
何が起きたか(時系列)
Day 0 — 公開・運用
AIの助けを借りて書いたアプリを公開して運用していた。ある日 — 請求の暴騰
クラウドAIの請求が突然跳ね上がる。使うはずのないモデルで、身に覚えのない大量処理が走っていた。自分の仕事ではない。調査 — 第三者の盗用
「自分のアプリの暴走」では説明がつかない。第三者が盗んだキーで回していたと判明。追跡 — 真の調査開始
「ではどこから鍵が漏れた?」——ここからが本当の調査だった。
なぜ最初に気づけなかったか(調査の遠回り)
恥を承知で、間違えた順に書きます。これ自体が教訓だからです。
犯人を決めつけた
→ 「自分のせい」も「他人のせい」も、決めつける前にまずデータを見る。
grepがcleanで安心しかけた
症状を隠して「直した」気になった
grepがcleanでも安心しない
鍵がファイルに無くても、実行中のプロセスの環境変数が脆弱性で抜かれることがあります。漏洩はファイルだけでなくランタイム(RCE・HTTPヘッダ)でも起きます。詳しくは RCEとは。
本当の原因:放置した公開済みCVSS 10.0
過去のアクセスログに残っていた奇妙なシグネチャを脅威情報で照合したら、一発で正体が分かった。使っていたWebフレームワークの特定バージョン帯に、認証前RCE(CVSS 10.0)が公開済みで、しかもすでに実際に悪用されていた。こちらはそれを数ヶ月、パッチせずに動かし続けていた。
- これは受動的なバグではなく、攻撃者がサーバー上で任意コードを実行し、環境変数を持ち出した能動的な攻撃だった。
- 不幸中の幸いは、実行範囲が非特権ユーザーのコンテナ内に留まっていたこと。侵害調査では常駐バックドア・マイナー・C2はいずれも検出されず、確認できた実害は環境変数の窃取だった。
- ただし内部DBには到達し得たので、DBの中身も漏れた前提で動いた。
教訓:奇妙な挙動は、まず既知CVEを疑う。自前バグだと決めつけない。
数の数え方も間違えた——「実稼働版」で判定する
横展開で「他にも脆弱な依存が8個ある」と報告したが、これも誤りだった。package.json の下限バージョン(^ のキャレット範囲)で数えていたからだ。本当に危ないのは、固定ピンで取り残された2個だけだった。
自分の“実稼働版”を確認するには
ロックファイルや稼働中のコンテナで、実際に解決されているバージョンを見ます。
# npm 系:実際に入っている版を確認
npm ls next react react-dom
# 稼働中コンテナの中で確認したいとき
docker exec <container> npm ls <package>package.json の ^ 表記ではなく、ここに出る数字が真実です。
初動でやったこと(再現可能な手順)
悪用キーを即無効化
フレームワークを修正版へ更新
全シークレットをローテーション
侵害調査
アカウント側の防御
再発防止の本命:機械に見張らせる
今回の事故は、突き詰めれば「公開済みのCVSS 10.0を、人間が見落として放置した」だけのこと。人手の巡回では必ず漏れる。機械に見張らせれば構造的に防げます。
今日からできる依存スキャン
無料で始められます。CIに1ステップ足すだけ。
# OSV スキャナ(Google)でロックファイルを検査
osv-scanner --lockfile=pnpm-lock.yaml
# GitHub なら Dependabot を有効化(リポジトリ設定 → Security)ITD自身も、この記事で書いた教訓どおり、自分の依存をCVE監視の対象にしています(人に勧める対策を、自分たちでも実践しています)。
教訓まとめ
やりがちな失敗
- 「課金を止めた」で対応完了にする
- grepがcleanで安心する
- 症状をプロキシで隠して直した気になる
package.jsonの下限で脆弱性を数える- 悪用確認の1個だけローテーションする
正しい防御
- 止血と「漏洩経路を塞ぐ」を別作業として両方やる
- ランタイム漏洩(RCE・ヘッダ)も疑う
- 根本(フレームワーク)を更新する
- 「実稼働版」で判定する
- 漏れたら env 全体を交換する
ひとことで言うと——暴走請求は氷山の一角。真因は放置したCVSS 10.0のRCE。そして真実は、一発で当たったのではなく、何度も間違えて、ぶつかって、削れて出てきた。
次に読む
- 用語:RCE とは / CVE とは / CVSS とは
- 対策:Next.js を安全に運用する(CVE追従)
- 事故:Laravelの .env が全公開されていた話
よくある質問
QAPIキーが漏れたら、悪用が確認された1つだけ無効化すればいい?
いいえ。漏洩経路がランタイム(RCEやヘッダ漏洩)の場合、環境変数にあった全シークレットが一度に漏れている前提で、すべてローテーションするのが安全です。悪用が確認できたキーは氷山の一角にすぎません。
Qコードもgitもgrepして鍵が見つからなければ安全?
安全とは言い切れません。鍵がファイルに無くても、フレームワークの脆弱性で実行中のプロセスから環境変数が抜かれることがあります。漏洩はファイルだけでなくランタイムでも起きます。
Q再発を一番確実に防ぐ方法は?
依存関係のCVEを機械で監視することです。今回の真因は「公開済みのCVSS 10.0を人間が見落として数ヶ月放置した」点にあり、Dependabotやosv-scannerに見張らせれば構造的に防げます。