「動いてる。でも、このまま公開していいのか分からない」
Claude Code や Cursor、Lovable でアプリを作った方から、いちばんよく聞く言葉です。この不安は正しい直感です。ただし、その理由はたぶんあなたが思っているものとは違います。
問題は「AIが書いたコードだから信用できない」ことではありません。AIは頼まれた機能は実装するが、頼まれていない防御は実装しない——これだけです。そして初めてアプリを作る人は、何を頼めばいいのかを知らない。当たり前です。知っていたら最初から自分で書けています。
この記事は、その「頼み忘れる部分」を危険な順に並べたものです。専門用語は初出時にすべて日常語に言い換えます。各項目には、自分で確認する方法と、そのまま Claude Code や Cursor に貼れる修正指示を添えました。
先に結論から言うと、公開前に必ず潰すべき項目は 4つ です。それ以外は、あとからでも間に合います。
この記事の内容を20問の質問形式にした無料セルフ診断があります。登録不要・入力内容はブラウザの外に出ません。読む前に自分の状態を知りたい方はそちらから。
1. なぜ「動く」と「安全」がずれるのか — 構造の話
まず、これがあなただけの問題ではないことを、測定結果で確認しておきます。
Veracode が100を超える大規模言語モデルを対象に80のコーディング課題で検証したところ、45%のケースで OWASP Top 10 に該当する脆弱性が混入していました。特にクロスサイトスクリプティング(他人が仕込んだスクリプトが自分のサイトで動いてしまう攻撃)への防御は、86%のサンプルで失敗しています。
GitGuardian の2026年の調査では、AIの支援を受けて書かれたコミットは、人間だけのコミットに比べて約2倍の頻度でAPIキーやパスワードを漏らしていました。
これはモデルが賢くなれば消える種類の問題ではありません。理由はシンプルで、AIは「動作するか」を目標にしていて、「攻撃されるか」を目標にしていないからです。
料理に例えるなら、AIは「美味しい料理を作る」ことには非常に長けていますが、「食中毒を出さない」ことは、そう言われない限り気にしません。そして食中毒は、味見では分かりません。
「動く」の反対は「壊れている」ではない
初めて開発する方が誤解しやすいのはここです。
- 動く / 動かない は、自分で試せば分かります
- 安全 / 危険 は、試しても分かりません
たとえば「他人のデータが見えてしまう」状態のアプリは、あなたが自分のアカウントで使っている限り、完璧に正常に動きます。壊れて見えることは一度もありません。誰かがURLの数字を1つ書き換えるまでは。
だから公開前に、意図的に「悪いこと」を試す 必要があります。それがこの記事の中身です。
2. 危険度の順序 — 何から潰すか
やるべきことを全部並べると心が折れるので、順番を固定します。判断基準は「起きたときに取り返しがつくか」です。
| 順位 | 項目 | 起きること | 取り返し |
|---|---|---|---|
| 1 | データベースの権限(RLS)が無い | 全ユーザーの全データが誰でも読み書きできる | つかない(漏れたデータは戻らない) |
| 2 | 秘密の鍵がブラウザ側に出ている | 同上+データの削除・改ざんもできる | つかない |
| 3 | サーバー側の所有者チェックが無い | URLの数字を変えると他人のデータが見える | つかない |
| 4 | 決済の二重処理を防いでいない | 顧客が二重に課金される/商品が二重に付与される | 返金はできるが信用は戻らない |
| 5 | 法務(個人情報・特商法)の不備 | 法令違反、決済審査に通らない | 是正できる |
| 6 | 従量課金の上限が無い | 想定外の請求が届く | 交渉次第、痛い |
| 7 | バックアップ・復旧手段が無い | 壊れたとき戻せない | 事前準備だけが対策 |
| 8 | エラーに気づけない | 壊れたまま何ヶ月も放置される | 気づけば直せる |
1〜4 が「公開前に必ず」の4項目です。5〜8 は公開直後にやれば間に合います。
以降、この順に見ていきます。
3. 【最優先】データベースの権限 — RLS とは何か
何の話か
RLS(Row Level Security、行レベルセキュリティ)は「データベース側で、誰がどの行を見られるかを決める設定」です。
「行」というのは、表計算ソフトの1行だと思ってください。ユーザーの一覧表があったとして、Aさんは自分の行だけ、Bさんは自分の行だけ見られるようにする——それがRLSです。
なぜ致命的なのか
Supabase のようなサービスでは、ブラウザから直接データベースに問い合わせが飛びます。このとき使われる anon(匿名)キーは、設計上公開されているものです。ブラウザの開発者ツールを開けば誰でも読めますし、それは正常な状態です。
問題は、RLS が無効だと、その公開キーだけで全データが読み書きできてしまうことです。
つまりこうなります。
- 攻撃者があなたのアプリを開く
- 開発者ツールでデータベースのURLと
anonキーを読む(どちらも公開情報) - そのままデータベースに問い合わせる
- 全ユーザーの、全テーブルが返ってくる
ログイン画面があっても関係ありません。ログイン画面はブラウザ側の話で、データベースはそれを知らないからです。
これは実際に起きている
CVE-2025-48757 として登録されている脆弱性の説明は、こうです。
生成されたサイトのデータベースの行レベルセキュリティポリシーが不十分で、認証されていない遠隔の攻撃者が任意のデータベーステーブルを読み書きできる
深刻度は CVSS 9.3 CRITICAL。最も高い部類です。対象は AI アプリビルダーの Lovable でした。
ここで重要なのは、この CVE にベンダーから異議が出ていることと、その理由です。ベンダー側の主張は「アプリのデータ保護は利用者の責任である」というものでした。
つまり——あなたが作ったアプリのデータを守る責任は、あなたにあると公式に言われている、ということです。ツール側が守ってくれることを期待してはいけません。
確認のしかた
Supabase を使っている場合:
- Supabase の管理画面 → Table Editor を開く
- テーブル一覧に「RLS disabled」「Unrestricted」といった警告が出ていないか見る
- 出ていたら、それが全公開のテーブルです
RLS が有効でも安心できません。ポリシー(許可の条件)が USING (true) になっていると、「全員が全行を見られる」という意味なので、無効なのと同じです。
AIに貼る修正指示
Supabase のマイグレーション/SQL を全部読んで、次を表にして報告してください。
(1) RLS が有効になっていないテーブル
(2) RLS は有効だがポリシーが1つも無いテーブル
(3) ポリシーの条件が USING (true) など実質的に全員を許可しているテーブル
そのうえで、各テーブルについて「ログインした本人の行だけ」に絞る RLS
ポリシーの SQL を書いてください。性能のため auth.uid() は
(select auth.uid()) の形で書いてください。
最後の (select auth.uid()) は性能上の定石です。この形にしないと、行数が増えたときにデータベースが極端に遅くなることがあります。
より詳しくは Supabase の「RLS」って何? — AIが作ったアプリでデータが全公開になる仕組み で解説しています。書いたポリシーが本当に絞れているかは、無料のRLSチェッカーに貼り付けると判定できます。
4. 【最優先】秘密の鍵 — なぜ「APIキー」が他人に見えるのか
何の話か
アプリは外部サービス(データベース、決済、AI)を使うために「鍵」を持っています。この鍵の置き場所を間違えると、鍵ごと他人に渡ることになります。
いちばん多い間違いが、NEXT_PUBLIC_ を付けてはいけない値に付けてしまうことです。
Next.js では、環境変数(設定値)の名前が NEXT_PUBLIC_ で始まると、ビルド時にブラウザ向けのファイルへそのまま埋め込まれます。これは公式ドキュメントに明記された仕様であって、バグではありません。ブラウザで動かす必要がある値のための仕組みです。
問題は、AI が「動かすため」に、秘密の鍵にもこの接頭辞を付けてしまうことがある点です。付ければ確かに動きます。そして誰でも読めるようになります。
特に危険な鍵
Supabase には2種類の鍵があります。
anonキー — 公開前提。RLS の制限を受ける。ブラウザに置いてよいservice_roleキー — すべての制限を無視できる鍵。RLS を素通りする。絶対にブラウザに置いてはいけない
service_role キーが漏れると、RLS を正しく設定していても意味がありません。全部素通りされます。AI が生成したコードでこの鍵が使われがちなのは、「とりあえず動く」からです。RLS ポリシーを書かなくてもデータが取れるので、エラーが出ません。
確認のしかた
- プロジェクト内を
NEXT_PUBLIC_で検索する - 出てきた値のうち、「これが他人に見えても平気か?」を1つずつ考える
- 少しでも迷ったら、それは移動すべき値です
あわせて、GitHub にコードを上げている場合は .env ファイルがコミットされていないか確認してください。一度コミットした鍵は、あとからファイルを消しても履歴に残ります。消すだけでは対策になりません。鍵そのものを作り直す必要があります。
AIに貼る修正指示
このプロジェクト全体を調べて、NEXT_PUBLIC_ が付いた環境変数に秘密情報
(Supabase の service_role キー、データベースのパスワード、各種 API の
秘密鍵)が入っていないか確認してください。
見つかった場合は、その値をサーバー専用の環境変数に移し、参照している
コードをサーバー側(Route Handler / Server Action)に移動してください。
クライアントコンポーネントから秘密情報を参照している箇所があれば、
すべて一覧にして報告してください。
なお、値そのものは出力せず「どのファイルの何行目にどの種類の鍵があるか」
だけを報告してください。
詳細は
NEXT_PUBLIC_とservice_role— AIが書いた「APIキー」がなぜ他人に見えるのか にまとめました。
5. 【最優先】所有者チェック — 「ログインしてる風」と本物の権限
何の話か
これは説明しづらいのですが、いちばん見落とされる項目です。
あなたのアプリに /orders/123 のような画面があるとします。123 は注文番号です。ここで、URLの 123 を 124 に書き換えたら何が起きるか。
もし他人の注文が表示されたら、それが脆弱性です。OWASP(Webセキュリティの標準的な団体)がAPIリスクの第1位に挙げている、最も一般的な穴です。
なぜ起きるのか
画面上に他人へのリンクが出ていなければ安全、と考えてしまうからです。しかしURLを直接打てば、リクエストは飛びます。リンクの有無は関係ありません。
AI は「ログイン中のユーザーの注文一覧を表示する」と頼まれれば、一覧はきちんと絞り込みます。しかし「注文詳細を表示する」と頼まれると、番号で引いてくるだけのコードを書きがちです。一覧が正しく絞られているので、動作確認では絶対に気づきません。
確認のしかた
これはブラウザだけで確認できます。
- テスト用のアカウントを2つ作る(AさんとBさん)
- Aさんでログインし、何かデータを作る。URLの番号(またはID)を控える
- ログアウトして、Bさんでログインする
- 控えたURLをアドレスバーに直接打ち込む
Bさんに、Aさんのデータが見えたらアウトです。見えずにエラーや「権限がありません」になれば正常です。
これは同じアプリの中で誰でもできる確認なので、公開前に必ず1回やってください。
AIに貼る修正指示
このアプリの、IDを受け取ってデータを返す/更新するすべての処理
(Route Handler、Server Action、API)を洗い出して、サーバー側で
「そのデータがログイン中のユーザーのものか」を検証しているか
1つずつ確認してください。
検証していないものがあれば、所有者チェックを追加した実装に修正して
ください。フロントエンドで表示を隠しているだけの箇所も、防御に
なっていない箇所として報告してください。
RLS を正しく設定していれば、この防御は二重になります。逆に言えば、RLS とサーバー側チェックはどちらか一方ではなく両方あるのが正しい状態です。
AIが作ったログイン機能は本物か — 「ログインしてる風」と本物の認可の違い で、実例を交えて詳しく扱っています。
6. 【最優先】決済 — 二重課金はなぜ起きるか
決済機能が無いアプリなら、この章は飛ばして構いません。
何の話か
Stripe をはじめとする決済サービスは、支払いが完了すると、あなたのアプリに「支払われましたよ」という通知(Webhook と呼びます)を送ります。あなたのアプリはそれを受けて、商品を渡したり、残高を増やしたりします。
ここに落とし穴があります。この通知は「最低1回」届く仕組みで、同じ通知が複数回届くことがあります。ネットワークが不安定だったり、あなたのアプリの応答が遅れたりすると、決済サービスは「届かなかったかもしれない」と判断して再送します。これは仕様であり、Stripe の公式ドキュメントにも明記されています。
通知を受け取るたびに残高を足す実装だと、そのまま二重付与になります。
なぜAIは書き漏らすのか
「支払いが完了したら残高を増やす」と頼めば、AI はその通りに書きます。それは正しい実装です。ただし「同じ通知が2回来たらどうするか」は、頼まれていないので考慮されません。
そしてこの不具合は、テストでは絶対に出ません。手元では通知は1回しか来ないからです。本番で、しかも忙しい時間帯に初めて出ます。
対策 — 冪等性(べきとうせい)
対策には名前があります。冪等性——「何回やっても結果が同じになる性質」のことです。
具体的には、通知に含まれるイベントIDをデータベースに保存し、すでに処理済みのIDが来たら何もせず「受け取りました」と返すだけです。難しくはありません。忘れられるだけです。
私が担当した決済基盤では、この設計により本番での二重課金を0件に保っています。技術的に難しい話ではなく、「必ずやる」と決めているかどうかの差です。
AIに貼る修正指示
このアプリの決済 Webhook の処理を読んで、同じイベントが複数回届いた
ときに二重処理されないか確認してください。
イベントIDを一意キーとして保存し、処理済みなら早期リターンする
冪等性の実装を追加してください。記録と業務処理は同一トランザクションに
まとめ、失敗時に決済サービスが再送できるよう適切なステータスコードを
返す実装にしてください。
あわせて、(1) 金額がクライアントから送られた値を使っていないか、
(2) Webhook の署名検証が行われているか、も確認してください。
AIに決済を作らせたとき、二重課金が起きる理由と防ぎ方 で、金額の改ざんと署名検証まで含めて扱っています。
7. 静かに間違うコード — 動くのに正しくない
ここからは「公開前に必ず」ではありませんが、放置すると効いてくる項目です。
AI 生成コードで最も多い不具合は、実は脆弱性ではありません。エラー処理と業務ロジックの間違いです。そしてこれらが厄介なのは、エラーが出ないことです。
- 集計が微妙に合わない(境界の日付が含まれていない)
- 検索結果が一部欠ける(並び順が不定で、ページ送りで漏れる)
- 失敗したのに成功と表示される(例外を握りつぶしている)
どれも画面上は正常に動きます。数字が少し違うだけです。だから何ヶ月も気づかれません。
対策は「壊れてはいけない条件」を先に言語化することです。「合計は必ず明細の和と一致する」「同じ検索を2回したら同じ順序で返る」——こういう当たり前のことを書き出して、AIにテストとして書かせます。
「動いてるのに間違ってる」— AI生成コードで一番多い静かなバグ で扱っています。
8. お金と法律 — 見落としやすい2つ
従量課金
クラウドサービスやAIのAPIは、使った分だけ請求されます。想定外のアクセスや無限ループがあると、請求は簡単に跳ねます。個人開発で数十万円の請求が届く事故は珍しくありません。
公開直後にやるべきは3つです。各サービスの請求アラート設定、AI APIのキー単位の上限額設定、そしてアプリ側のレート制限(同じ人からの連続リクエストを制限する仕組み)。
法務
メールアドレスを1つでも預かるなら、プライバシーポリシーの掲載は個人情報保護法上の義務です。「個人だから」「無料だから」は免除の理由になりません。
有料で提供するなら、特定商取引法に基づく表記も必要です。事業者名・連絡先・価格・支払時期・返品条件などの表示が法律で決まっています。Stripe の審査でも、ここが整っていないと通らないことがあります。
9. 壊れたときに戻れるか — 運用の最低ライン
最後は、公開後に効いてくる3点です。
バックアップ。 AI に「このテーブルを整理して」と頼んだ結果、意図せずデータが消えることがあります。無料プランはバックアップの保持期間が短い、あるいは自動バックアップが無いこともあります。「気づいたときには復旧できる期間を過ぎていた」が起こります。
ロールバック。 デプロイして壊れたとき、前のバージョンに戻す手順を知っていますか。深夜に本番が落ちたまま何時間も復旧できない、という事態は、手順を一度試しておくだけで防げます。なお、データベースの構造変更はコードと違って自動では戻りません。
エラーの可視化。 個人開発で最も多いのは「壊れていることに気づかない」です。フォームが送信できない状態が何ヶ月も続く——実際に起こります。ブラウザ側だけで失敗しているエラーは、サーバーのログには残りません。無料枠のあるエラー監視サービスを1つ入れておくだけで、状況は大きく変わります。
引き継ぎまで見据えるなら エンジニアに相談・引き継ぐときの準備 もどうぞ。
10. 公開前チェックリスト(印刷用)
上から順に、1つずつ。
公開前に必ず
- 全テーブルで RLS が有効で、ポリシーが「本人の行だけ」に絞られている
- 書き込み(INSERT/UPDATE)にも制限がかかっている(
WITH CHECK) -
service_roleキーやDBパスワードがNEXT_PUBLIC_に入っていない -
.envが GitHub にコミットされていない - 一度でも人目に触れた鍵を作り直した
- 別アカウントでURLの番号を書き換えて、他人のデータが見えないことを確認した
- 管理者機能がサーバー側で権限確認されている
- (決済がある場合)Webhook が二重に届いても二重処理されない
- (決済がある場合)金額をサーバー側で決めている
- (決済がある場合)Webhook の署名を検証している
公開直後に
- プライバシーポリシーを掲載した
- (有料の場合)特定商取引法に基づく表記を掲載した
- 各サービスの請求アラート・上限を設定した
- バックアップの有無と保持期間を確認した
- 前のバージョンに戻す手順を1回試した
- エラー監視を入れた、または重要処理の失敗が自分に届くようにした
- 本番URLで、登録とパスワード再設定が実際に動くことを確認した
このチェックリストを20問の質問形式にして、危険な順に並べた結果とAIへの修正指示を出す無料セルフ診断を用意しています。登録不要・約5分です。
まとめ — 作ったこと自体は、何も間違っていない
最後に一つだけ。
生成AIでアプリを作ったこと自体は、まったく正しい選択です。私自身、日々 Claude Code で本番のプロダクトを書いています。速くて安いのは事実で、それを使わない理由はありません。
ただ、「速く作る」と「安全に出す」は別の作業です。前者はAIが劇的に上手くなりました。後者は、まだ人間側に残っています。
そして後者は、思っているより短時間で終わります。この記事の4項目——RLS、鍵の露出、所有者チェック、決済の冪等性——を潰すだけで、致命的な事故のほとんどは防げます。全部作り直す必要はありません。
分からない項目があったら、まずは各章の修正指示をAIに貼ってみてください。それでも判断がつかないところ(特に認可の設計、決済、個人情報の扱い)だけ、人に聞けば十分です。