# NEXT_PUBLIC_ と service_role — AIが書いた「APIキー」がなぜ他人に見えるのか

> AIが生成したコードでAPIキーやパスワードが他人に読める状態になる仕組みを、非エンジニア向けに解説します。NEXT_PUBLIC_ の意味、Supabaseのanonキーとservice_roleキーの決定的な違い、GitHubに一度でも上げた鍵をなぜ作り直す必要があるのか。確認手順とAIへの修正指示つき。

- 公開日: 2026-08-06
- 著者: 友田 陽大
- タグ: バイブコーディング, セキュリティ, Next.js, Supabase, 個人開発
- URL: https://tomodahinata.com/blog/ai-app-api-key-exposure-explained
- カテゴリ: AIで作ったアプリの運営
- 総合ガイド: https://tomodahinata.com/blog/ai-app-pre-launch-guide-for-non-engineers

## 要点

- NEXT_PUBLIC_ が付いた環境変数は、ビルド時にブラウザ向けファイルへそのまま埋め込まれる。これはNext.jsの仕様であってバグではない — 埋め込まれる前提の値のための仕組み
- Supabaseの anon キーは公開前提で安全、service_role キーは全制限を無視できるため絶対にブラウザに置けない。AIが service_role を使いがちなのは「RLSを書かなくてもエラーが出ない」から
- GitHubは公開・非公開を問わずコミット履歴が残る。ファイルを消しても過去のコミットから鍵は読めるので、消すだけでは対策にならず鍵の作り直しが必須
- GitGuardianの2026年調査では、AI支援コミットの秘密情報漏洩率は人間のみのコミットの約2倍（3.2% 対 1.5%）
- AIとのチャットに貼ったコード、質問用のスクリーンショットも見落としやすい漏洩経路。鍵は「見られた可能性がある」時点で無効

---

「APIキーは環境変数に入れたから安全」——この理解が、AI で作ったアプリで最も裏切られる場所です。

環境変数に入れること自体は正しい。ただ、環境変数には**ブラウザに出るものと出ないもの**があり、その境目が名前の付け方だけで決まる。そしてAIは、動かすためにその境目を越えてしまうことがあります。

この記事は、その仕組みを非エンジニア向けに説明します。

---

## 1. `NEXT_PUBLIC_` が何をしているのか

Next.js には、はっきりしたルールが1つあります。

> 環境変数の名前が **`NEXT_PUBLIC_` で始まると、その値はビルド時にブラウザ向けのファイルへ埋め込まれる**

これは公式ドキュメントに書かれた仕様で、バグではありません。ブラウザで動かす必要がある値——たとえば公開APIのURLや、アクセス解析の測定ID——のための正規の仕組みです。

重要なのは「埋め込まれる」という部分です。実行時にどこかから安全に読み込んでいるのではなく、**ファイルの中に文字列としてそのまま書き込まれます**。

```
【NEXT_PUBLIC_ が付いていない】
サーバーの中だけで読まれる → ブラウザには一切届かない

【NEXT_PUBLIC_ が付いている】
ビルド時にJavaScriptファイルへ文字列として埋め込まれる
  → ページを開いた人なら誰でも読める
```

### 実際に見る方法

自分のアプリで確認できます。

1. アプリをブラウザで開く
2. F12（Mac は ⌘+Option+I）で開発者ツールを開く
3. 「Sources」タブでJavaScriptファイルを開き、鍵の一部を検索する

あるいはもっと簡単に、**ページのHTMLを右クリック→「ページのソースを表示」**して検索してもいい。埋め込まれていれば、そのまま文字列で出てきます。

---

## 2. なぜAIが境界を越えるのか

AI が悪意を持っているわけではありません。仕組みの問題です。

アプリを作っていて「データが取れません」というエラーが出たとします。あなたはAIに「エラーが出た、直して」と頼みます。AIは動く状態にするための最短経路を探します。

そのとき、ブラウザ側のコードから鍵を使いたい場面で、`NEXT_PUBLIC_` を付ければ確実に動く。付けなければ `undefined`（値が無い）になって動かない。

**AIは「動作すること」を目標にしているので、動く方を選びます。** そして動きます。エラーは消えます。あなたは直ったと思う。

これが、動くことと安全であることが正反対を向いている典型例です。

GitGuardian の2026年の調査では、AIの支援を受けて書かれたコミットは、人間だけのコミットに比べて**約2倍の頻度（3.2% 対 1.5%）で秘密情報を漏らしていました**。個々のAIが不注意なのではなく、「速く動かす」という目標設定の当然の帰結です。

---

## 3. 2種類の鍵 — `anon` と `service_role`

Supabase を使っている場合、鍵は2種類あります。この違いが分かれば、判断の8割は済みます。

| | `anon` キー | `service_role` キー |
| --- | --- | --- |
| 想定 | **公開される前提** | 絶対に秘密 |
| RLS | 制限を受ける | **すべて無視する** |
| ブラウザに置く | ○ 正常 | ✕ 致命的 |
| 用途 | 通常のアプリからの利用 | サーバー側の管理処理 |

### `anon` キーが見えても問題ない理由

`anon` キーは「データベースに話しかける権利」でしかありません。**何を見られるかは RLS が決めます**。RLS が正しく設定されていれば、このキーを持っていても他人のデータは1行も取れません。

だから、ブラウザで `anon` キーが読めること自体は正常です。心配する必要はありません。

### `service_role` キーが致命的な理由

`service_role` キーは、**RLSを含むすべての制限を無視できる管理者用の鍵**です。

これが漏れると、RLS を完璧に設定していても意味がありません。全部素通りされます。全テーブルの読み取り・書き換え・削除が、誰にでもできるようになります。

### なぜAIは `service_role` を使いたがるのか

理由は明快です。**エラーが出ないから**です。

- `anon` キーを使う → RLSポリシーを正しく書くまでデータが取れず、エラーになる
- `service_role` キーを使う → ポリシーを一切書かなくても、最初から全部動く

RLSを設定していない状態でAIに「データを取得する処理を書いて」と頼むと、`anon` では動かないので、AIは動く方を選ぶことがあります。

そして、その選択は画面上は完全に正常に見えます。

---

## 4. GitHub に上げた鍵 — 消しても消えない

もう1つの主要な漏洩経路です。

`.env` というファイルに鍵を書き、それを GitHub にアップロードしてしまう。よくあります。

問題は、**GitHubは公開・非公開にかかわらずコミット履歴を保持する**ことです。あとからファイルを削除しても、過去のコミットを開けば鍵はそのまま読めます。

```
コミット1: .env を追加（鍵が入っている）  ← ここに永久に残る
コミット2: .env を削除                    ← 現在の状態からは消える
```

「消したから大丈夫」は成立しません。

### 対策は1つだけ

**鍵そのものを作り直す**ことです。各サービスの管理画面で古い鍵を無効化し、新しい鍵を発行して、環境変数を更新します。

無効化まで済ませて初めて「対応済み」です。新しい鍵を作っただけで古い鍵を無効化していなければ、古い鍵はまだ生きています。

### あわせて `.gitignore` を確認

`.gitignore` は「このファイルはGitHubに上げない」というリストです。ここに `.env*` が入っているか確認してください。入っていなければ追加します。

---

## 5. 見落としやすい漏洩経路

`NEXT_PUBLIC_` と GitHub 以外にも、実際によくある経路があります。

**AIとのチャットに貼ったコード。** 「このエラーを直して」と、鍵が入ったままのファイルを貼り付けたことはありませんか。多くのサービスでは学習に使われない設定がありますが、貼った時点で「自分の管理外に出た」ことは事実です。

**質問用のスクリーンショット。** ターミナルや管理画面のスクリーンショットに、鍵の一部が写り込むことがあります。SNSや技術系のQ&Aサイトに投稿していれば、それは公開です。

**エラーメッセージのログ。** デバッグのために「リクエストの中身を全部出力する」コードをAIが書くことがあります。そのログに認証トークンが含まれていると、ログの保管場所ごと漏洩の対象になります。

**フロントエンドのソースマップ。** 本番ビルドでソースマップを公開していると、圧縮されたコードが元の形に戻せます。埋め込まれた値は当然読めます。

原則はシンプルです。**鍵は「見られた可能性がある」時点で無効**。迷ったら作り直してください。作り直すコストは数分、漏れたときのコストは計り知れません。

---

## 6. 自分で確認する手順

### 手順1: `NEXT_PUBLIC_` の棚卸し

プロジェクト内を `NEXT_PUBLIC_` で検索し、出てきた値を1つずつ見ます。判断基準はこれだけです。

> **この値が、見ず知らずの他人の手元にあっても平気か？**

- 公開APIのURL → 平気
- アクセス解析の測定ID → 平気
- Supabase の `anon` キー → 平気（RLSが前提）
- Supabase の `service_role` キー → **アウト**
- データベースのパスワード → **アウト**
- OpenAI / Anthropic などのAPIキー → **アウト**
- Stripe の秘密鍵（`sk_` で始まるもの）→ **アウト**

Stripe の場合、`pk_` で始まる公開可能キーはブラウザに置いてよく、`sk_` で始まる秘密鍵は絶対に置いてはいけません。名前で見分けられます。

### 手順2: ブラウザで実際に探す

本番のアプリを開いて、ページのソースを表示し、鍵の先頭数文字で検索します。**出てきたら、その鍵は世界中から読める状態です。**

### 手順3: GitHub を確認

リポジトリのファイル一覧に `.env` が無いか見ます。あれば、そのファイルの「History（履歴）」も確認してください。

---

## 7. AIに貼る修正指示

```text
このプロジェクト全体を調べて、次の3つを確認してください。

(1) NEXT_PUBLIC_ が付いた環境変数に秘密情報が入っていないか
    （Supabase の service_role キー、データベースのパスワード、
      OpenAI/Anthropic などのAPIキー、Stripe の sk_ で始まる秘密鍵）
(2) コードの中に鍵やパスワードが直接書かれていないか
(3) .gitignore に .env* が含まれているか。含まれていなければ追加する

見つかった場合は、その値をサーバー専用の環境変数（NEXT_PUBLIC_ を
付けない）に移し、参照しているコードをサーバー側（Route Handler /
Server Action）へ移動してください。クライアントコンポーネントから
秘密情報を参照している箇所は、すべて一覧にしてください。

重要: 値そのものは絶対に出力しないでください。「どのファイルの何行目に
どの種類の鍵があるか」だけを報告してください。

最後に、見つかった鍵それぞれについて「どのサービスの管理画面で作り直せ
ばよいか」と「作り直したあとに更新が必要な環境変数」を教えてください。
```

最後の一文が重要です。**移動しただけでは対策になりません**。一度でも公開側に出た鍵は、作り直すまで有効なままです。

---

## 8. 正しい配置のかたち

整理すると、こうなります。

```
【ブラウザに出てよい】NEXT_PUBLIC_ を付ける
  - 公開APIのURL
  - Supabase の URL と anon キー（※RLSが正しく設定されている前提）
  - アクセス解析の測定ID
  - Stripe の pk_ で始まる公開可能キー

【サーバーの中だけ】NEXT_PUBLIC_ を付けない
  - Supabase の service_role キー
  - データベースの接続文字列・パスワード
  - OpenAI / Anthropic などのAPIキー
  - Stripe の sk_ で始まる秘密鍵、Webhook の署名シークレット
  - メール送信サービスのAPIキー
```

そして本番の値は、**Vercel などホスティング側の環境変数画面にだけ**入れます。`.env` ファイルは手元の開発用で、GitHubには上げません。

---

## まとめ

- `NEXT_PUBLIC_` が付いた値は、ビルド時にブラウザ向けファイルへ埋め込まれる。仕様であってバグではない
- Supabase の `anon` キーは公開前提で安全。`service_role` キーは全制限を無視するので絶対にブラウザに置けない
- AI が `service_role` を使いがちなのは「RLSを書かなくてもエラーが出ない」から。楽な道が最も危険
- GitHubのコミット履歴は消えない。ファイルを消すだけでは対策にならず、鍵の作り直しが必須
- AIチャットへの貼り付け、スクリーンショット、ログも漏洩経路。「見られたかも」の時点で作り直す

鍵の配置は、仕組みさえ分かれば判断できます。基準は1つ、**「他人の手元にあっても平気か」**。迷ったらサーバー側に置いてください。

RLS とあわせて確認したい方は [Supabaseの「RLS」って何？](/blog/supabase-rls-explained-for-non-engineers)、全体像は [AIで作ったアプリを公開する前に](/blog/ai-app-pre-launch-guide-for-non-engineers) をどうぞ。
