本文へ移動

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-key

Lovable にはシークレット専用の保存場所があり、連携機能でキーが必要になると、コードに書く代わりにその入力を求めてきます。Vercel や Netlify では、VITE_ や NEXT_PUBLIC_ の接頭辞を付けない環境変数を使い、サーバー側の関数からだけ読み込みます。

3. サーバー側の関数からサービスを呼び出す

ブラウザはあなたの関数を呼び出し、関数がシークレットキーを使ってサービスを呼び出します。AI機能のための最小限の Edge Function は次のとおりです。

supabase/functions/ask/index.ts
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

この記事は次の言語でも読めます: EnglishPortuguêsEspañol中文

記事一覧