対策・学習19分で読める

AIで書いたコードからAPIキーが漏れ、不正課金された事例:原因は未修正のCVSS 10.0の脆弱性

AIで書いたアプリを公開した直後にAPIキーが盗まれ、不正課金された事例です。本当の原因は数ヶ月放置したCVSS 10.0のRCEでした。原因を取り違えた経緯と再発を機械で防ぐ方法を、防御目線でまとめます。

本文を読む
公開日
更新日
目次

セキュリティに詳しくない個人開発者がよく起こす事故を、固有名詞をすべて伏せた事例として教訓にまとめた記事です。攻撃の再現手順は載せません。狙いは、同じ事故を繰り返さないための防御です。

事故サマリー — INCIDENT FILE
種別
APIキー漏洩 / 不正課金
重大度
Critical(公開済み CVSS 10.0 を悪用)
原因
Webフレームワークの認証前RCEを数ヶ月パッチせず放置
漏洩範囲
環境変数にあった全シークレット
実行範囲
非特権ユーザーのコンテナ内に留まる(ホストrootは無事)
恒久対処
①修正版へ更新 ②全鍵ローテーション ③CVE機械監視
10.0
CVSS / 最悪クラス
数ヶ月
放置していた期間
env全体
漏れた前提の範囲
機械監視
再発防止で最も効く対策

「課金を止めた」≠「対応完了」

請求を止めるのは応急処置で、漏洩経路を塞ぐのは別の作業です。両方やって初めて対応完了です。

何が起きたか(時系列)

  1. Day 0 — 公開・運用

    AIの助けを借りて書いたアプリを公開して運用していた。
  2. ある日 — 請求の暴騰

    クラウドAIの請求が突然跳ね上がる。使うはずのないモデルで、身に覚えのない大量処理が走っていた。自分の仕事ではない。
  3. 調査 — 第三者の盗用

    「自分のアプリの暴走」では説明がつかない。第三者が盗んだキーで回していたと判明。
  4. 追跡 — 漏洩経路の調査開始

    「ではどこから鍵が漏れた?」——ここからが本当の調査だった。

なぜ最初に気づけなかったか

恥を承知で、間違えた順に書きます。これ自体が教訓だからです。

1

犯人を決めつけた

最初は「自分のコードのフォールバックが暴走した」と早合点。だがデータ(実際の処理内容)を見れば、それは自分の仕事ではあり得なかった。
→ 「自分のせい」も「他人のせい」も、決めつける前にまずデータを見る。
2

grepがcleanで安心しかけた

全コード・git履歴・連携リポジトリを検索 → 平文の鍵はどこにも無い。ここで「追跡不能」と諦めかけた。ファイルばかり見て、ランタイムの漏洩を見落としていた。
3

症状を隠して「直した」気になった

プロキシ側で症状を隠して満足しかけた。だが症状を隠してもRCE自体は残ったまま。原因を直すまで、悪用できる状態が続く。

grepがcleanでも安心しない

鍵がファイルに無くても、実行中のプロセスの環境変数が脆弱性で抜かれることがあります。漏洩はファイルだけでなくランタイム(RCE・HTTPヘッダ)でも起きます。詳しくは RCEとは。

本当の原因:放置した公開済みCVSS 10.0

過去のアクセスログに残っていた奇妙なシグネチャを脅威情報で照合したら、すぐに原因が分かった。使っていたWebフレームワークの特定バージョン帯に、認証前RCE(CVSS 10.0)が公開済みで、しかもすでに実際に悪用されていた。こちらはそれを数ヶ月、パッチせずに動かし続けていた。

  • これは受動的なバグではなく、攻撃者がサーバー上で任意コードを実行し、環境変数を持ち出した能動的な攻撃だった。
  • 不幸中の幸いは、実行範囲が非特権ユーザーのコンテナ内に留まっていたこと。侵害調査では常駐バックドア・マイナー・C2はいずれも検出されず、確認できた実害は環境変数の窃取だった。
  • ただし内部DBには到達し得たので、DBの中身も漏れた前提で動いた。
見える症状:身に覚えのない不正課金
利用者に見えるのはここまで
↑ 盗まれた鍵が時間差で転売・悪用される
環境変数ごとシークレットを窃取(env 全体が漏れる)
↑ 攻撃者が実行中サーバーで任意コード実行
原因:放置した公開済み CVSS 10.0(pre-auth RCE)
身に覚えのない請求は、表に見えた症状にすぎない。その下に「公開済みのCVSS 10.0を放置した」という原因がある。

教訓:奇妙な挙動は、まず既知CVEを疑う。自前バグだと決めつけない。

漏れた場所を絞り込む:そのキーはどこに置いていたか

grepで見つからないときは、探し方を変えます。悪用されたキーが「どこに書かれていたか」を並べると、漏れた場所の候補が絞れます。たとえば本番サーバーの環境変数にしか入れていないキーが使われたなら、Gitやブラウザではなく、本番サーバーかホスティング事業者の側を疑います。

漏れる場所よくある原因自分で確認する方法塞ぎ方
Gitの履歴・公開リポジトリ.env のコミット、コードへの直書きgitleaks git で全履歴を検査する。GitHubのシークレットスキャンの通知を見る先に失効し、コミット前の検査で止める
ブラウザに配るJavaScriptNEXT_PUBLIC_ 付きの変数にキーを入れる、ブラウザから直接APIを呼ぶビルド結果のフォルダを、キーの先頭の文字列で検索するAPI呼び出しをサーバー側の処理に移す
Webの公開フォルダ.env やバックアップを公開フォルダに置く自分のサイトの /.env を開き、404か403になるか確かめる秘密のファイルを公開フォルダの外へ移す
ログ・エラー画面デバッグ表示のまま公開、リクエストのヘッダーをそのまま記録本番のデバッグ設定と、ログに Authorization などが出ていないかを見るデバッグ表示を切り、ログで秘密を伏せる
実行中のサーバー公開済みの脆弱性(RCEなど)の放置実際に動いている版を、セキュリティ勧告の修正版と比べる修正版へ更新し、環境変数の秘密をすべて入れ替える
ホスティング事業者事業者側の侵害事業者のセキュリティ速報とメール通知を確認する後から値を表示できない設定で保存し、入れ替える
開発用のPC悪意のあるパッケージや拡張機能入れたパッケージや拡張機能の版が、侵害の公表と重なるか確かめる該当の版を消し、PCにあった秘密を入れ替える

この事例は「実行中のサーバー」の行でした。下の4行は、コードやGitを検索しても跡が見つからない経路です。OWASPのSecrets Management Cheat Sheetも、環境変数は同じ環境のプロセスから読めることが多く、ログやダンプに含まれることがあると注意しています。だから、1本のキーが悪用されたら、同じ場所にあった秘密はすべて漏れた前提で扱います。

キーを分けておくと、調べるのが速くなる

サービスごと・環境(本番と開発)ごとに別のキーを使っていれば、悪用されたキーを見ただけで、上の表のどの行かがほぼ決まります。1本のキーをあちこちで使い回していると、すべての場所を疑うことになります。キーの分け方は 盗まれたAPIキーでOpenAIアカウントがBANされた場合 にまとめています。

脆弱性は、実際に動いている版で数える

ほかの依存も調べて「他にも脆弱な依存が8個ある」と報告したが、これも誤りだった。package.json の下限バージョン(^ のキャレット範囲)で数えていたからだ。本当に危ないのは、固定ピンで取り残された2個だけだった。

自分の“実稼働版”を確認するには

ロックファイルや稼働中のコンテナで、実際に解決されているバージョンを見ます。

# npm 系:実際に入っている版を確認
npm ls next react react-dom
 
# 稼働中コンテナの中で確認したいとき
docker exec <container> npm ls <package>

package.json の ^ 表記ではなく、ここに出る数字が、実際に動いている版です。

自分の環境で確認する方法

請求の異常に気づいたときも、まだ何も起きていない今も、次の順で確かめられます。どれも自分のアカウントとサーバーを見るだけの作業です。

1

どのキーがいつ使われたかを見る

AI APIの管理画面で、利用量と費用をプロジェクト(AnthropicではWorkspace)ごと、日ごとに分けて見ます。普段使わないモデル、普段動かない時間帯の利用があれば、そのキーが漏れています。上の表で、そのキーを置いていた場所の行から調べます。
2

ブラウザに配るファイルにキーが入っていないか調べる

Next.jsでは、NEXT_PUBLIC_ で始まる環境変数の値は、ビルド時にブラウザへ送るJavaScriptへ埋め込まれます。ビルド後に grep -rl "sk-" .next/static のように、出力フォルダを使っているキーの先頭の文字列で検索します。見つかったら、そのキーは誰でも読める状態でした。
3

公開フォルダから秘密のファイルが見えないか確かめる

自分のサイトに対して curl -sI https://example.com/.env(example.comは自分のドメインに置き換える)を実行し、1行目のステータスが200でないこと(404や403であること)を確かめます。詳しくは 公開ディレクトリの秘密ファイルの棚卸し。
4

本番で動いている版を勧告と比べる

上の囲みのとおり、本番のコンテナの中で npm ls を実行して版を確かめます。ロックファイルは osv-scanner scan -L pnpm-lock.yaml で、既知の脆弱性がある版を含んでいないか調べられます。
5

ログに秘密が残っていないか調べる

アプリのログを保存しているフォルダに gitleaks dir --redact を実行すると、ログに書き出されたキーを探せます。--redact を付けると、見つかった値そのものは画面に出ません。
6

上限と通知の設定を確かめる

OpenAIは管理画面の制限のページで、金額を超えたときの通知と、月ごとの上限(hard spend limit)を設定できます。AnthropicはWorkspaceごとに上限を設定できます。通知だけで止まらない設定になっていないかを見ます。

初動でやったこと(再現可能な手順)

1

悪用キーを即無効化

まず被害を止める。ただしこれは対応の一部にすぎない。
2

フレームワークを修正版へ更新

RCEを実際に修正。更新後、漏洩のシグネチャが消えたことをログで確認。
3

全シークレットをローテーション

漏れた前提で。優先順は「どこからでも悪用できるもの」から:オブジェクトストレージ鍵 → セッション署名鍵 → 各種APIキー → DB認証情報。
4

侵害調査

常駐物・不正な定期実行・外部通信の有無を確認(今回はいずれも無し)。
5

アカウント側の防御

パスワード変更とMFA有効化、見知らぬAPIキーが増えていないか確認。

再発防止:CVEを自動で監視する

今回の事故は、突き詰めれば「公開済みのCVSS 10.0を、人間が見落として放置した」だけのこと。人手の巡回では必ず漏れる。機械に見張らせれば構造的に防げます。

今日からできる依存スキャン

無料で始められます。CIに1ステップ足すだけ。

# OSV スキャナ(Google)でロックファイルを検査
osv-scanner scan -L pnpm-lock.yaml
 
# GitHub なら Dependabot を有効化(リポジトリ設定 → Security)

当サイト自身も、この記事で書いた教訓どおり、自分の依存をCVE監視の対象にしています(人に勧める対策を、自分たちでも実践しています)。

よくある間違いと直し方

  • 新しいキーを、まだ塞いでいないサーバーに入れる:同じ経路から新しいキーも盗まれます。悪用されたキーはすぐ止め、脆弱性を修正してから、残りの秘密の入れ替えを仕上げます。
  • 予算アラートを上限だと思う:Google Cloudは、通知だけの予算では利用や支出が自動で止まらないと明記しています。止めたいなら、OpenAIの月ごとの上限やAnthropicのWorkspaceの上限など、利用そのものを止める設定を別に入れます。
  • AI APIのキーをブラウザやスマホアプリに入れる:利用者の手元に届いた値は、誰でも取り出せます。AI APIの呼び出しはサーバー側で行い、ブラウザからは自分のサーバーを呼ぶ形にします。
  • ファイルやGitの履歴から消して終わりにする:すでに写しを持つ人は使えます。失効が先で、履歴の整理は後です(→ GitHubで漏れた認証情報が今も有効な理由)。
  • 開発用と本番で同じキーを使う:どちらから漏れたのか分からず、両方を止めることになります。環境ごとに別のキーを発行します。

公開事例:事業者の側から環境変数が漏れたケース

ファイルを検索しても見つからない漏洩は、自分のサーバー以外でも起きます。Vercelは2026年4月19日、社内システムへの不正アクセスを公表しました。同社の発表によれば、社員が使っていた外部のAIツールの侵害を起点に、一部の顧客の環境変数のうち「sensitive」(保存後に値を表示できない設定)にしていないものが取得されました。

Vercelは利用者に、sensitiveでない環境変数の見直しと入れ替え、アクティビティログと最近のデプロイの確認を勧めています。また、プロジェクトやアカウントを削除するだけではリスクはなくならないとしています。

このケースの利用者は、自分のコードやGitをいくら検索しても漏れた跡を見つけられません。取るべき行動は、この記事の事例と同じく「その場所に置いていた秘密をすべて入れ替える」ことです。手順は Vercelのセキュリティインシデント(2026年4月) にまとめています。

教訓まとめ

やりがちな失敗

  • 「課金を止めた」で対応完了にする
  • grepがcleanで安心する
  • 症状をプロキシで隠して直した気になる
  • package.json の下限で脆弱性を数える
  • 悪用確認の1個だけローテーションする

正しい防御

  • 請求を止めることと、漏洩経路を塞ぐことを、別の作業として両方やる
  • ランタイム漏洩(RCE・ヘッダ)も疑う
  • 原因になったフレームワークを更新する
  • 「実稼働版」で判定する
  • 漏れたら env 全体を交換する

まとめると、急増した請求は症状で、原因は放置したCVSS 10.0のRCEでした。正しい原因には一度でたどり着いたのではなく、何度も間違え、検証し直してたどり着きました。

次に読む

出典

  • OWASP「Secrets Management Cheat Sheet」(環境変数のリスク、漏洩時の対応): cheatsheetseries.owasp.org
  • Next.js Docs「How to use environment variables in Next.js」(NEXT_PUBLIC_ の埋め込み): nextjs.org
  • OSV-Scanner「Usage」: google.github.io
  • OpenAI「Production best practices」(キーの管理、通知と上限): developers.openai.com
  • Claude Help Center「Creating and managing Workspaces」(Workspaceの上限とキー): support.claude.com
  • Google Cloud「Create, edit, or delete budgets and budget alerts」: docs.cloud.google.com
  • Vercel「Vercel April 2026 security incident」: vercel.com

よくある質問

QAPIキーが漏れたら、悪用が確認された1つだけ無効化すればいい?
A

いいえ。漏洩経路がランタイム(RCEやヘッダ漏洩)の場合、環境変数にあった全シークレットが一度に漏れている前提で、すべてローテーションするのが安全です。悪用が確認できたキーは、漏れたもののごく一部にすぎません。

Qコードもgitもgrepして鍵が見つからなければ安全?
A

安全とは言い切れません。鍵がファイルに無くても、フレームワークの脆弱性で実行中のプロセスから環境変数が抜かれることがあります。漏洩はファイルだけでなくランタイムでも起きます。

Q再発を一番確実に防ぐ方法は?
A

依存関係のCVEを機械で監視することです。今回の原因は「公開済みのCVSS 10.0を人間が見落として数ヶ月放置した」点にあり、Dependabotやosv-scannerに見張らせれば構造的に防げます。

QAI APIの請求が急に増えました。最初に何をすればいい?
A

まず、使われたキーを管理画面で無効にして被害を止めます。次に、そのキーをどこに置いていたかから漏れた場所を絞り込み、脆弱性や設定ミスを直してから、同じ場所にあった秘密をすべて入れ替えます。キーを止めただけでは、漏れた経路は残ったままです。

Qどのキーが漏れたのかは、どうやって調べる?
A

AI APIの管理画面で、利用量と費用をプロジェクト(AnthropicではWorkspace)ごとに分けて見ます。普段使わないモデルや時間帯の利用があったところが、漏れたキーです。サービスや環境ごとにキーを分けておくと、そのキーを置いていた場所から漏れた経路まで絞り込めます。

Q予算アラートを設定していれば、不正利用の請求は止まる?
A

止まるとは限りません。たとえばGoogle Cloudは、通知だけの予算(alerts-only budget)では利用や支出が自動で止まらないと明記しています。止めたい場合は、OpenAIの月ごとの上限(hard spend limit)やAnthropicのWorkspaceごとの上限のように、利用そのものを止める設定を別に行います。