// docs / baas security / supabase service role exposure
JavaScript に露出した Supabase サービスロールキー: 意味と発見方法
Supabase サービスロールキーはあなたのデータベースのマスターキーです。これを保持する者は誰でも行レベルセキュリティをバイパスし、すべてのテーブルのすべての列を読むことができ、好きなものを書き込みまたは削除できます。これはサーバーサイドコードだけに置かれるよう設計されており、ブラウザに置かれてはなりません。AI コーディングツールがこれを JavaScript バンドルに送り込むと、あなたのデータベースは実質的に公開されたことになります。本記事では、漏洩したキーを特定する JWT 形状、漏洩を生む 3 つの AI ツールパターン、検出後 1 時間以内にすべきこと、そしてユーザーよりも先に自動的にスキャンする方法を説明します。
サービスロールキーとは
Supabase はプロジェクトごとに 2 種類の異なるキーを発行します: anon キー (新しいプロジェクトでは公開可能キーとも呼ばれます) と service_role キーです。どちらもプロジェクトの JWT シークレットで署名された JSON Web Token です。違いは JWT ペイロードに焼き込まれた role クレームです — 公開キーは anon、マスターキーは service_role。PostgREST、Supabase Storage、Supabase Auth はすべて、service_role クレームを見るとすべてをバイパスするモードに切り替わります。
jwt.io で任意の Supabase キーをデコードし、ペイロードを見てください。サービスロール JWT の形状は紛れもありません:
サービスロール JWT のデコードされたペイロード (下にシンタックスハイライト付きブロックとして表示)。
{
"iss": "supabase",
"ref": "[project-ref]",
"role": "service_role",
"iat": 1700000000,
"exp": 2000000000
}新しい Supabase プロジェクトは、JWT ではなくプレフィックス sb_secret_ を持つシークレット形式のキーを発行します。挙動は同一です — 公開バンドルに sb_secret_ が含まれていれば、同じく破滅的です。
AI コーディングツールがサービスロールキーを漏洩する仕組み
何千ものバイブコードされたアプリで、同じ 3 つのパターンを見てきました。それぞれ、開発者が AI ツールに助けを求めることから始まり、サービスキーがバンドル内にインライン化されることで終わります。
パターン 1: NEXT_PUBLIC_ プレフィックス付きの単一 .env ファイル
開発者は AI ツールに「Supabase をセットアップして」と頼み、両方のキーを含む単一の .env を受け入れます。AI ツールは — ほとんどの環境変数が NEXT_PUBLIC_* 経由で公開されているコーパスで訓練されているため — 両方に NEXT_PUBLIC_ をプレフィックスします。Next.js はそのプレフィックスに一致するものをビルド時にクライアントバンドルにインライン化します。Vercel にデプロイすると、サービスキーは main.[hash].js 内にあります。
パターン 2: createClient 呼び出しで誤ったキー
開発者は AI が生成した config.ts ファイルに両方のキーを貼り付け、AI が誤ってブラウザ側の createClient() 呼び出しに process.env.SUPABASE_SERVICE_ROLE_KEY を埋め込みます。ビルドがその変数を取り込み、JWT がバンドルに着地します。
パターン 3: シードスクリプトにハードコードされたサービスロールキー
開発者は AI ツールにデータベースをシードするスクリプトを書くよう頼みます。AI は (環境変数から読むのではなく) サービスロールキーを直接ファイルにハードコードし、そのファイルをリポジトリにコミットし、公開 GitHub リポジトリやデプロイされたアプリの /scripts/seed.js ルートがそのキーを提供し始めます。
FixVibe バンドルスキャンが漏洩を検出する仕組み
FixVibe のバンドルシークレットチェックは、デプロイされたアプリが参照するすべての JavaScript ファイル — エントリチャンク、遅延ロードされるチャンク、Web Worker、Service Worker — をダウンロードし、JWT 形状 (eyJ[base64-header].eyJ[base64-payload].[signature]) に一致するものをデコードする検出器を通します。デコードされたペイロードに "role": "service_role" が含まれていれば、スキャンはファイルパスとキーが現れる正確な行とともに、それを重大な検出結果として報告します。同じチェックはプレフィックスにより新しい sb_secret_* パターンにも一致します。
スキャンは決して発見されたキーで認証しません。形状を特定して漏洩を報告します — キーを使って悪用可能性を証明することは、あなたのデータベースへの無断アクセスになります。証拠は JWT ペイロード自体にあります。
検出後の最初の 1 時間にすべきこと
漏洩したサービスロールキーはランタイムの緊急事態です。キーはスクレイピングされたと仮定してください — 攻撃者はリアルタイムで公開バンドルを監視しています。キーをローテーションし、最近のアクティビティを監査するまで、データベースは侵害されたものとして扱ってください。
- 直ちにキーをローテーション。 Supabase ダッシュボードで Project Settings → API → Service role key → Reset を実行します。旧キーは数秒以内に無効化されます。キーを使用するサービス側コードはすべて、ローテーション着地前に更新および再デプロイする必要があります。
- 最近のデータベースアクティビティを監査。 ダッシュボードで Database → Logs を開きます。直近 7 日でフィルタします。PII を含むテーブルに対する異常な
SELECT *クエリ、大きなUPDATEやDELETEステートメント、既知のインフラ外の IP からのリクエストを探します。Supabase はすべてのリクエストでx-real-ipヘッダをログに記録します。 - ストレージオブジェクトをチェック。 Storage → Logs にアクセスし、最近のファイルダウンロードを確認します。漏洩したサービスロールキーは、プライベートバケットへもすべてバイパスするアクセスを与えます。
- ソース管理からキーを削除。 ローテーション後も、git 履歴に JWT を残しておくと、公開リポジトリで発見可能なままです。
git filter-repoまたは BFG Repo-Cleaner で履歴からスクラブし、強制プッシュします (事前に共同作業者に警告してください)。 - 修正後に再スキャン。 再デプロイしたアプリに対して新しい FixVibe スキャンを実行します。バンドルシークレットの検出結果はクリアされるはずです。どのチャンクにも
service_roleJWT もsb_secret_*文字列も残っていないことを確認します。
そもそも漏洩を防ぐ
構造的な解決策は命名規律とツールレベルのガードレールです:
- サービスキーに
NEXT_PUBLIC_*、VITE_*、その他のバンドルインライン化プレフィックスを決して付けない。 命名規約が境界です — すべてのフレームワークがそれを尊重します。 - 開発者マシンの
.envからサービスキーを完全に排除する。 デプロイ時にシークレットマネージャ (Doppler、Infisical、Vercel 暗号化環境変数) から読み込み、ローカルにコミットしないでください。 - <strong>Mark every Supabase client construction with explicit context.</strong> Files named <code>supabase/browser.ts</code> use the anon key; files named <code>supabase/server.ts</code> use the service-role key with <code>import 'server-only'</code> at the top. The <code>server-only</code> import causes a build error if a client component tries to consume the module.
- <strong>Add a pre-commit hook that greps for JWT-shaped strings.</strong> <code>git diff --staged | grep -E 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'</code> catches both anon and service tokens before they leave your machine.
- ビルド出力をスキャンする CI ゲートを追加。
next buildの後、.next/static/chunks/の出力でservice_role文字列を grep します。何かに一致したらビルドを失敗させます。
# Pre-commit hook: refuse any staged JWT-shaped string.
git diff --staged \
| grep -E 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' \
&& echo "JWT detected in staged changes — refusing commit" \
&& exit 1
# CI gate: fail the build if "service_role" shipped to the static bundle.
grep -RE 'service_role|sb_secret_' .next/static/chunks/ \
&& echo "Service-role credential leaked into bundle" \
&& exit 1よくある質問
攻撃者は実際にどれくらい速く漏洩した Supabase サービスロールキーを見つけますか?
公開バンドルスキャナは新しいデプロイを数分以内にトロールします。研究者は、初回デプロイから 1 時間以内に新しい Supabase プロジェクトに対する有効なエクスプロイトを記録しています。サービスロール露出は 60 日ではなく 60 分の窓として扱ってください。
キーをローテーションすれば十分ですか、それともデータ流出を想定しなければなりませんか?
ローテーションは漏洩したキーを無効化しますが、すでに引き出されたデータを元に戻すことはできません。テーブルに PII、決済データ、または規制対象データが含まれている場合、GDPR (72 時間)、CCPA、HIPAA に基づく通知義務があるかもしれません。ログを監査し、監査で疑わしいアクセスが示された場合は法的助言を求めてください。
サービスロールキーが漏洩した場合、RLS が私を守りますか?
いいえ。行レベルセキュリティは service_role クレームによって完全にバイパスされます。それは設計上のことです — このキーは、管理操作のためにバックエンドコードが RLS をスキップできるようにするために存在します。緩和策は、キーが攻撃者が読める場所に到達しないようにすることです。
これは新しい Supabase 公開可能 / シークレットキーモデル (<code>sb_publishable_</code> / <code>sb_secret_</code>) にも適用されますか?
はい — 同じリスククラスです。sb_secret_* キーは、新しいプロジェクト向けにサービスロール JWT を置き換える新しいシークレットキー形式です。バンドルに sb_secret_* が含まれていれば、漏洩したサービスロール JWT と同じくらい破滅的です。FixVibe のバンドルシークレット検出器は両方の形状に一致します。
anon / 公開可能キーはどうですか — バンドルに入っていても安全ですか?
はい、設計上そうです。anon キーはブラウザに置かれることを意図しており、すべての Supabase Web クライアントが使用するものです。その安全性は、すべての public テーブルで RLS が正しく構成されているかどうかに完全に依存します。何をチェックすべきかについてはSupabase RLS スキャナ記事を参照してください。
次のステップ
本番 URL に対して FixVibe スキャンを実行してください — バンドルシークレットチェックは無料、サインアップ不要、1 分以内に service_role 露出を報告します。RLS 層が役目を果たしているかを検証するにはSupabase RLS スキャナ記事を、ファイルアクセスをロックダウンするにはSupabase ストレージバケットセキュリティチェックリストを組み合わせてください。AI ツールがなぜこの漏洩クラスを確実に生成するのかの背景については、AI コーディングツールがセキュリティギャップを残す理由をお読みください。
