セキュリティに詳しくない個人開発者がよく起こす事故を、固有名詞をすべて伏せた事例として教訓にまとめた記事です。攻撃の再現手順は載せません。狙いは、同じ事故を繰り返さないための防御です。
- 種別
- 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を疑う。自前バグだと決めつけない。
漏れた場所を絞り込む:そのキーはどこに置いていたか
grepで見つからないときは、探し方を変えます。悪用されたキーが「どこに書かれていたか」を並べると、漏れた場所の候補が絞れます。たとえば本番サーバーの環境変数にしか入れていないキーが使われたなら、Gitやブラウザではなく、本番サーバーかホスティング事業者の側を疑います。
| 漏れる場所 | よくある原因 | 自分で確認する方法 | 塞ぎ方 |
|---|---|---|---|
| Gitの履歴・公開リポジトリ | .env のコミット、コードへの直書き | gitleaks git で全履歴を検査する。GitHubのシークレットスキャンの通知を見る | 先に失効し、コミット前の検査で止める |
| ブラウザに配るJavaScript | NEXT_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 の ^ 表記ではなく、ここに出る数字が、実際に動いている版です。
自分の環境で確認する方法
請求の異常に気づいたときも、まだ何も起きていない今も、次の順で確かめられます。どれも自分のアカウントとサーバーを見るだけの作業です。
どのキーがいつ使われたかを見る
ブラウザに配るファイルにキーが入っていないか調べる
NEXT_PUBLIC_ で始まる環境変数の値は、ビルド時にブラウザへ送るJavaScriptへ埋め込まれます。ビルド後に grep -rl "sk-" .next/static のように、出力フォルダを使っているキーの先頭の文字列で検索します。見つかったら、そのキーは誰でも読める状態でした。公開フォルダから秘密のファイルが見えないか確かめる
curl -sI https://example.com/.env(example.comは自分のドメインに置き換える)を実行し、1行目のステータスが200でないこと(404や403であること)を確かめます。詳しくは 公開ディレクトリの秘密ファイルの棚卸し。本番で動いている版を勧告と比べる
npm ls を実行して版を確かめます。ロックファイルは osv-scanner scan -L pnpm-lock.yaml で、既知の脆弱性がある版を含んでいないか調べられます。ログに秘密が残っていないか調べる
gitleaks dir --redact を実行すると、ログに書き出されたキーを探せます。--redact を付けると、見つかった値そのものは画面に出ません。上限と通知の設定を確かめる
初動でやったこと(再現可能な手順)
悪用キーを即無効化
フレームワークを修正版へ更新
全シークレットをローテーション
侵害調査
アカウント側の防御
再発防止: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でした。正しい原因には一度でたどり着いたのではなく、何度も間違え、検証し直してたどり着きました。
次に読む
- 漏らした後の対応:GitHubに公開された認証情報54万件が今も有効——削除ではなく失効が必要な理由
- 用語:RCE とは / CVE とは / CVSS とは
- 対策:Next.js を安全に運用する(CVE追従)
- 事故:Laravelの .env が全公開されていた話
- 基本:.env と APIキーの基本(漏れたときの手順)
- 予防:gitleaksでコミット前に秘密を止める
- 事故:盗まれたAPIキーでOpenAIアカウントがBANされた場合
出典
- 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