AIで作ったアプリに OpenAI や Stripe のキーが入っていませんか? サーバー側の関数に移す方法
フロントエンドにあるキーは誰でもコピーできます。ブラウザに置いてよいキーとそうでないキーの見分け方、見つけ方、Supabase の Edge Function への移し方を解説します。
4分で読めます
AIツールに、チャットボットや決済、メール送信をアプリに追加するよう頼んだら、一度で動いた。そんなに早く動いた理由は、多くの場合、API キーがそのままフロントエンドのコードに入れられたからです。
フロントエンドにあるものは、すべての訪問者のブラウザに送られます。そこにキーがあれば、誰でも開発者ツールを開いてコピーし、あなたのアカウントと請求で使えてしまいます。公開サイトからまさにこうしたキーを探し回る自動スキャナーも存在します。
ブラウザに置いてよいキー
すべてのキーが秘密というわけではありません。多くのサービスは、意図的に2種類のキーを発行しています。
| ブラウザに置いてよい | ブラウザに置いてはいけない |
|---|---|
Stripe の公開可能キー(pk_live_…) |
Stripe のシークレットキーまたは制限付きキー(sk_live_…、rk_live_…) |
| Supabase の publishable キーまたは anon キー | Supabase の secret キーまたは service role キー(漏れたときの対処法) |
| アクセス解析やタグの ID | OpenAI、Anthropic などのAIのキー(sk-…) |
| 自社ドメインに制限した地図のキー | メール送信用のキー(Resend の re_…、SendGrid、Postmark) |
判断基準は、そのキーでお金を使える、非公開のデータを読める、あなたの名前で何かを送れるなら、サーバーに置くべきだということです。
キーが漏れたときの被害はサービスによって異なります。AIのキーなら数時間で高額な請求が発生しえます。Stripe のシークレットキーなら顧客情報の閲覧や返金処理ができます。メールのキーなら、あなたのドメインからフィッシングメールを送られ、その後何か月もメールの到達率が落ちることになります。
公開中のアプリからキーを探す
アプリを開いて F12 を押し、Sources(Chrome)または Debugger(Firefox)を開いて、全ファイル検索(Ctrl+Shift+F または Cmd+Option+F)で sk-、sk_live、rk_live、re_、sb_secret_、service_role を探します。
手元にコードがある場合は、ビルドしてその結果を検索します。これが訪問者に届くものそのものです。
npm run build
grep -rEn "sk-[A-Za-z0-9_-]{20,}|sk_live_|rk_live_|re_[A-Za-z0-9_]{20,}|sb_secret_|service_role" dist/よくある原因は環境変数ファイルです。Lovable や Bolt で作ったアプリの多くが使う Vite では、VITE_ で始まる変数はすべてブラウザ用のコードにコピーされます。Next.js では NEXT_PUBLIC_ で始まる変数が同じ扱いです。VITE_OPENAI_API_KEY に入れた秘密情報は、.env に書いてあっても公開されています。
修正の手順
1. まずキーを差し替える
コードを移しても、漏れたキーは元に戻りません。サービスの管理画面で新しいキーを作成し、古いキーを削除します。誰にも見られていないと思っても、最初にこれを行ってください。
あわせて、AIサービスには月の利用上限額を設定しておきましょう。万一の被害に上限を設けられます。
2. 新しいキーをサーバー側のシークレットとして保存する
Supabase の Edge Functions は、シークレットを環境から読み込みます。ダッシュボードの Edge Functions → Secrets でキーを追加するか、CLI を使います。
supabase secrets set OPENAI_API_KEY=sk-your-new-keyLovable にはシークレット専用の保存場所があり、連携機能でキーが必要になると、コードに書く代わりにその入力を求めてきます。Vercel や Netlify では、VITE_ や NEXT_PUBLIC_ の接頭辞を付けない環境変数を使い、サーバー側の関数からだけ読み込みます。
3. サーバー側の関数からサービスを呼び出す
ブラウザはあなたの関数を呼び出し、関数がシークレットキーを使ってサービスを呼び出します。AI機能のための最小限の Edge Function は次のとおりです。
import { createClient } from 'npm:@supabase/supabase-js@2';
Deno.serve(async (req) => {
// 1. Only signed-in users may call this function.
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')! // public key: it only identifies the project
);
const token = (req.headers.get('Authorization') ?? '').replace('Bearer ', '');
const { data: { user } } = await supabase.auth.getUser(token);
if (!user) return new Response('Unauthorized', { status: 401 });
// 2. Accept only what the feature needs.
const { question } = await req.json();
if (typeof question !== 'string' || question.length > 2000) {
return new Response('Bad request', { status: 400 });
}
// 3. The secret key never leaves the server.
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: Deno.env.get('OPENAI_MODEL'),
max_tokens: 500,
messages: [{ role: 'user', content: question }]
})
});
const data = await response.json();
return Response.json({ answer: data.choices?.[0]?.message?.content ?? '' });
});アプリ側の呼び出しは supabase.functions.invoke('ask', { body: { question } }) になり、Supabase のクライアントがログイン中のユーザーのトークンを一緒に送ります。
4. 決済:金額はサーバーが決める
Stripe の場合、キーを移すだけでは不十分です。請求額をブラウザから関数に渡していると、誰かが金額を書き換えられます。ブラウザから送るのは何を買うか(商品やプランの ID)だけにし、サーバーが価格を調べ、シークレットキーで Checkout Session を作成してリンクを返します。支払いの確認は、ユーザーが直接開ける「完了」ページではなく、Stripe の署名を検証した Webhook で行ってください。
リリースのたびに
- 上記のパターンでビルド結果を検索する。
- 公開用の接頭辞が付いた変数には、秘密情報を入れない。
- AIが「とりあえず」フロントエンドにキーを置こうとしたら、サーバー側の関数を作るよう頼む。
決済、AI機能、個人データを扱うアプリなら、無料の Security Check をご利用ください。公開されたキー、誰でも呼べる関数、署名のない Webhook を外部から読み取り専用で確認し、深刻な問題を書面でお伝えします。
APIキーAIで作ったアプリSupabaseBolt
あわせて読みたい
記事一覧
アプリのセキュリティ
Lovable で作ったアプリを公開する前に:リリース前のセキュリティチェックリスト
実際のユーザーとデータが集まる前に行う15の確認。データベースのルール、キー、ストレージ、ログイン、決済、関数、バックアップ。ほとんどは数分で終わります。
5分で読めます

アプリのセキュリティ
Supabase Storage の公開バケット:ユーザーのファイルが丸見えになっていませんか?
アバター画像は公開でも構いませんが、身分証、請求書、契約書は違います。Supabase Storage のバケットとルールの確認方法と、アップロードを壊さずに直す手順を解説します。
5分で読めます

アプリのセキュリティ
Supabase のシークレットキーがブラウザから見えていたら? 最初の1時間でやること
フロントエンドに service role キーやシークレットキーがあると、誰でもデータベースを自由に操作できます。無効化、差し替え、ログ確認、原因の修正という正しい順番を解説します。
6分で読めます