---
title: "Lovableアプリのセキュリティ：12の確認と追加の8つ"
description: "LovableとSupabaseのアプリを守る確認リスト。クライアントのアプリを直した経験から、各項目を証明するクエリと、他が見落とす8つの穴を紹介します。"
url: "https://madsalt.tech/ja/blog/lovable-app-security-checklist/"
author: "Denys Zaichenko"
language: "ja"
published: 2026-10-18
updated: 2026-10-25
---

# Lovableアプリのセキュリティ：12の確認と追加の8つ

TL;DR

Lovableアプリの安全性は、Supabaseデータベースの安全性そのもので、「RLSはオンになっている」だけではほとんど何も証明できません。2社の2人のユーザーでテストし、ロールをユーザーメタデータに置かず、シークレットキーをブラウザに出さないこと。さらにgrant、ビュー、ストレージ、関数、レート制限は、それぞれ別のやり方でポリシーをすり抜けるため、個別に確認します。以下に、私が実行しているリスト、LovableやClaudeに貼り付けられるプロンプト、そしてクライアントのLovableアプリで最初の12項目では見えないものが見つかったあとに加えた8つの確認を載せます。

Denysです。フルスタック・AIエンジニアです。今月、Lovableで作られたクライアントのアプリを直しました。いつものようにすべてSupabase上で動いていて、セキュリティの修正があまりに多く、アプリが修正でできているように感じるほどでした。これは、LovableやSupabaseのプロジェクトで顧客データを守れていると言う前に、いま毎回確認しているリストです。

## Lovable は安全か？これまでのセキュリティ問題

プラットフォームはアプリをビルドしてホストしますが、アプリが安全かどうかは、主にその裏にあるSupabaseデータベースで決まります。アプリはブラウザからそのデータベースを直接読み書きするからです。公開されている記録には、両方の種類の問題が残っています。

| 時期                  | 何が起きたか                                                                                                                                                                                                                   | 出典                                                                                                                                               |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2025年3月             | CVE-2025-48757：Lovableのショーケースアプリ1,645件をスキャンしたところ、170件で計303のエンドポイントが、RLSがないか緩すぎたために、公開キーを持つ誰にでもデータを返していた                                                    | [Matt Palmerによる開示](https://mattpalmer.io/posts/2025/05/statement-on-CVE-2025-48757/)、2025年5月                                               |
| 2026年2月             | Lovableでホストされた1つのアプリが、18,697件のユーザーレコードを公開していた。関数内のアクセスチェックが匿名の訪問者を通し、ログイン済みのユーザーを弾いていた。Lovableは、コードの一部はLovableが生成したものではないと述べた | [The Register](https://www.theregister.com/software/2026/02/27/ai-built-app-on-lovable-exposed-18k-users-researcher-claims/5038511)、2026年2月27日 |
| 2026年2月3日〜4月20日 | プラットフォームのリグレッションにより、ほかのLovableユーザーが公開プロジェクトのチャット履歴とソースコードを読めた                                                                                                            | [Lovableのインシデント投稿](https://lovable.dev/blog/our-response-to-the-april-2026-incident)、2026年4月22日                                       |

最初の2つはアプリレベルの穴で、以下の確認で見つかる種類のものです。3つ目はLovable自身の問題で、Lovableが修正しました。あなたの役目は、キーをチャットやコードに置かないこと（確認3）と、そこにあったキーをローテーションすることです。Lovableは公開前にセキュリティスキャンを実行しますが、その結果に対応するのはあなたで、CVEが公開された時点のスキャンは、ポリシーが正しいかではなく、ポリシーが存在するかを確認するものでした。つまり「Lovableは安全か」は、あなたのデータベースが以下の確認を通るかどうかに行き着きます。

## Lovableアプリで「RLSはオン」だけでは足りないのはなぜ？

Lovableアプリは、ブラウザからデータベースに直接アクセスします。間に自前のバックエンドはありません。ページが公開キーでSupabaseのData APIを呼び出し、何を返すかはPostgresが決めます（そのAPIがどのスキーマを提供するかは別の設定で、[Supabase Exposed schemas](https://madsalt.tech/ja/notes/supabase-exposed-schemas/)で扱っています）。つまり、顧客データを守るルールはすべてデータベースの中にある必要があり、そのデータベースには入口がいくつもあります。

row level securityはその1つです。grantはロールがそもそもテーブルに触れるかを決め、ポリシーはどの行かを決めます。そして3つのものはポリシーを完全に飛び越えます。シークレットキー、`security_invoker`なしで作ったビュー、`SECURITY DEFINER`関数です。これは仮の話ではありません。Lovable自身の[セキュリティガイド](https://docs.lovable.dev/tips-tricks/security-best-practices)は、Lovableが設定するRLSポリシーを自分で確認するよう求めていますし、上の[CVE-2025-48757](https://www.upguard.com/blog/everything-everywhere-systemic-data-exposure-in-supabase-apps)は、誰も確認しなかったときに起きることです。

![リクエストがデータに届く4つの経路。RLSポリシーが確認するのは最初の1つだけ。通常のリクエスト：ブラウザ → Grant → RLSポリシー → 自分の行、Edge Functionのシークレットキー：ブラウザ → Edge Function → シークレットキー → 全行、security_invokerなしのビュー：ブラウザ → ビュー → 所有者の権限 → 全行、SECURITY DEFINER関数：ブラウザ → RPC → 所有者の権限 → 全行。](https://madsalt.tech/charts/ja/lovable-app-security-checklist-paths.svg)

*図1. ポリシーが確認するのは最初の経路だけです。残る3つはRLSを飛び越える権限で動きます。出典：RLSとAPIの保護に関するSupabaseドキュメント、2026年10月。*

## プロンプト：LovableかClaudeに貼り付ける

そのまま貼り付けて構いません。そのあと、結果は自分で確認してください。AIは終わっていないのに「完了」と言いがちで、エラーを消すプロンプトは、アクセスを広げることでそれを実現してしまうことがあります。

```text
このプロジェクトのセキュリティを監査し、見つかった問題を修正して、何を変えたか教えてください。

1. 2社の2人のユーザーでテストする。ユーザーAがBのレコードを、IDを指定して、API、検索、エクスポートで読む、編集する、削除することを試す。すべて失敗しなければならない。
2. すべてのテーブルでRLSを有効にし、ポリシーは行の所有者または会社を確認する。「ログイン済み」だけでは不十分。
3. service role keyはフロントエンドに絶対に置かない。これを使うEdge Functionは、ユーザーの確認と会社での絞り込みを自分で行う。キーはRLSを飛び越える。
4. ロール、プラン、会社IDをユーザーメタデータから取らない。ユーザーが編集できるため。サーバーだけが書き込めるテーブルに置く。
5. リクエストが2つのID（プロジェクト + タスク）を送ってきたら、そのタスクがそのプロジェクトに属するか確認する。
6. anonとauthenticatedからすべてのテーブル権限を取り消し、ポリシーに必要なものだけを付与し直す。RLSはTRUNCATEを止めない。テーブルを作るマイグレーションは必ずGRANTを含める。2026年10月30日から、Supabaseは新しいテーブルに自動ではgrantしなくなり、アプリは42501エラーになる。
7. すべてのビューをsecurity_invoker = trueにする。そうしないとRLSを飛び越える。
8. ストレージのバケット：ユーザーは自分のファイルだけを読み書きできる。非公開データに公開バケットを使わない。
9. SECURITY DEFINER関数：NULLは許可ではなく拒否を意味しなければならない。
10. ログイン、サインアップ、パスワードリセット、OTPにレート制限を設け、メモリではなくDBに保存する。
11. すべての入力をサーバー側で検証する。
12. Supabaseのsecurity advisorを実行し、すべての警告を修正する。
```

## 12の確認と、それぞれの証明方法

### 1. 2社の2人のユーザー

会社1にユーザーA、会社2にユーザーBを作り、それぞれにいくつかのレコードを持たせます。Aとしてサインインし、Bのレコードを、IDを指定して、API、検索、すべてのエクスポートで読み、編集し、削除してみます。どの試みも、何も返さないかエラーにならなければなりません。これは、以降のほとんどを捕まえる唯一のテストです。書いたものではなく、攻撃者が実際にやることを試すからです。

![2人のユーザー、2つの会社。会社1のユーザーAが会社2のレコードを狙う。どの経路も何も返さないこと。APIでIDを指定：ユーザーA → BのレコードID → 読む・編集・削除 → 0行またはエラー、検索経由：ユーザーA → Bの文言を検索 → 結果 → Bの行なし、エクスポート経由：ユーザーA → エクスポート → ファイル → Bの行なし。](https://madsalt.tech/charts/ja/lovable-app-security-checklist-test.svg)

*図2. 2社テスト。会社2のデータがどの経路からも返らなければ合格です。*

両方の会社に所属する3人目のユーザーも加えてください。「呼び出し元は行の会社のメンバーである」という形のポリシーでは、そのユーザーは会社1で作業しながら会社2の行を読めます。別のSupabaseアプリで、そのようなユーザーでプローブを実行したところ、クエリがリクエストの会社でも絞り込むようになるまで、もう一方の会社のドキュメントが返りました。これは[RLSが分離できなかった、2つの組織に属するユーザー](https://madsalt.tech/ja/notes/supabase-rls-user-in-two-organizations/)としてまとめています。

### 2. すべてのテーブルでRLSを有効にし、所有者または会社で絞る

`auth.uid() is not null`は「サインインしている任意のユーザー」を意味し、公開のサインアップページがあれば、それは誰でもという意味になります。各ポリシーは、行の所有者または会社を呼び出し元と比較します。RLSがまだオフのテーブルを一覧にします。

```sql
select c.relname
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r', 'p') and not c.relrowsecurity;
```

### 3. シークレットキーをフロントエンドに絶対に置かない

service role keyと、新しい`sb_secret_`キーは、すべてのポリシーを飛び越えます。ビルド済みのJavaScriptから両方を検索し、一度でも公開されたキーはローテーションしてください。これを使うEdge Functionは、ユーザーが誰かの確認と会社での絞り込みを自分で行う必要があります。このキーでは、他に誰もやってくれないからです。

### 4. ロール、プラン、会社IDをユーザーメタデータから取らない

`raw_user_meta_data`は、クライアントが`signUp`に送ったものそのままで、ユーザーは後から変更できます。別のアプリでは、新規ユーザーのトリガーが招待されたロールをここから読んでいたため、登録した誰もがオーナーロールを渡してOwnerになれました。ロール、プラン、会社IDは、サーバーだけが書き込むテーブルか`app_metadata`に置きます。詳しい経緯は[サインアップのメタデータから取られたロール](https://madsalt.tech/ja/notes/supabase-raw-user-meta-data-role/)にあります。

### 5. 1つのリクエストに2つのID：両者が一緒に属するか確認する

リクエストにプロジェクトIDとタスクIDが含まれるとき、プロジェクトを認可するだけでは足りません。プロジェクトAの権限を持つユーザーが、AとあわせてプロジェクトBのセクションIDを送ると、その行はBに入ります。RLSはこれを捕まえません。INSERTは攻撃者自身のプロジェクトIDを書き込み、それはまさにポリシーが確認するものだからです。書き込む前に、子が親に属しているか確認してください。

### 6. grant：取り消し、必要なものだけを付与し直す

grantとポリシーは別々の層です。ポリシーを追加してもgrantは取り消されません。だからSupabaseの[RLSガイド](https://supabase.com/docs/guides/database/postgres/row-level-security)は、明示的にrevokeして付与し直すよう勧めています。別のプロジェクトでは、`supabase/postgres` 17.6.1のイメージが、`public`のすべてのテーブルについて`anon`と`authenticated`にTRUNCATE、REFERENCES、TRIGGER、MAINTAINを付与しており、TRUNCATEにはどのポリシーも適用されません。Data APIにTRUNCATEのエンドポイントはないため、1回のリクエストで消せるわけではありませんが、どのクライアントロールにも必要ない権限であり、そのロールでSQLを実行する関数はそれを引き継ぎます。

```sql
revoke all on public.orders from anon, authenticated;
grant select, insert, update on public.orders to authenticated;

select grantee, table_name, privilege_type
from information_schema.role_table_grants
where table_schema = 'public' and grantee in ('anon', 'authenticated')
order by table_name;
```

1つの日付がこの確認を変えます。Supabaseは[新しいテーブルに自動でgrantしなくなります](https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically)。新規プロジェクトは2026年5月30日から、既存のすべてのプロジェクトは、Lovableが作成したものも含めて2026年10月30日からです。その後は、Lovableやマイグレーションが明示的な`GRANT`なしで作ったテーブルはアプリから見えなくなります。リクエストは42501で失敗し、そのテーブルを使う機能が壊れます。そのため、テーブルを作るすべてのマイグレーションは、`enable row level security`やポリシーと並べて、grantも含める必要があります。

変わらないことが2つあります。既存のテーブルは今あるgrantを保持するので、不要な権限を取り消すのは引き続きあなたの仕事です。また、この変更で取り消されるのはselect、insert、update、deleteだけです。Lovableのプロジェクトでは、新しいデフォルトでも、新しいテーブルには`anon`と`authenticated`向けのTRUNCATE、REFERENCES、TRIGGER、MAINTAINが付いてきました。先にすべてrevokeしてからgrantする順序が、引き続き安全です。

### 7. すべてのビューに`security_invoker = true`

ビューは所有者の権限で動き、Supabaseでは所有者が通常`postgres`なので、保護されたテーブルの上のビューは、ポリシーが止めているすべての行を渡してしまいます。Postgres 15以降では次のようにします。

```sql
alter view public.customer_summary set (security_invoker = true);
```

### 8. ストレージ：自分のファイルだけ、非公開データに公開バケットを使わない

公開バケットは、URLを知っている誰にでも任意のファイルを返します。非公開ファイルは、`storage.objects`に所有者または会社を確認するポリシーを付けた非公開バケットに置きます。多くの場合はパスの最初のフォルダから判定します。プロジェクトを移した後は、有効なストレージポリシーの数を数えてください。あるアプリでは、移行後にすべてのアップロードが失敗しました。ポリシーのマイグレーションが実行されておらず、テーブルはRLSがオンでポリシーが0件だったからです。

### 9. SECURITY DEFINER関数：NULLは拒否を意味する

PL/pgSQLでは、`if not (has_permission or assignee_id = auth.uid())`は、`assignee_id`がnullのとき例外を出しません。条件全体がnullになり、`if`はそれをスキップします。同じ式は、nullを拒否として扱うポリシーの中では安全です。あるアプリでは、これにより、すべての権限をオフにしたロールが、3683件すべてのタスクを操作できました。どのタスクにも担当者がいなかったからです。条件を`coalesce(…, false)`で包んでください。詳細は[PL/pgSQLのIF NOTでNULLがチェックを飛ばした](https://madsalt.tech/ja/notes/plpgsql-if-not-null-fails-open/)にあります。

### 10. データベースに保存するレート制限

ログイン、サインアップ、パスワードリセット、OTPを制限し、カウンターはPostgresに保存します。そのLovableアプリでは、制限がEdge Function内の変数にありました。ホスト版のプロジェクトを2026年10月17日にプローブしたところ、すべてのリクエストが新しいisolateで実行されるため、メモリ上のカウンターは常に1で、制限が作動することはありませんでした。ローカルの`supabase functions serve`はisolateを再利用するので、同じコードがローカルのテストをすべて通りました。

![カウンターの置き場所で効くか決まる。ホスト版Supabase Edge Functionのレート制限。関数のメモリ上のカウンター：リクエスト → 新しいisolate → カウンター = 1 → 制限されない、Postgres上のカウンター：リクエスト → claim_rate_limit → 共有の行 → 上限で429。](https://madsalt.tech/charts/ja/lovable-app-security-checklist-ratelimit.svg)

*図3. ホスト版Supabase Edge Functionでの私のプローブ、2026年10月17日。すべてのリクエストが新しいisolateで始まりました。*

保存先と同じくらい、キーも重要です。ホスト版では、`X-Forwarded-For`の最後のホップはSupabase自身のプロキシなので、それをキーにすると全員が同じバケットに入ってしまい、`x-client-ip`はクライアントが設定できます。私のプローブでは、`cf-connecting-ip`が実際のクライアントを示していました。修正は、service roleだけが実行できる`SECURITY DEFINER`関数`claim_rate_limit(bucket, key, max, window)`で、アドバイザリロックで直列化し、生のIPやユーザーIDではなくそのハッシュを保存するものでした。

### 11. すべての入力をサーバー側で検証する

フォームが何を確認していても、APIはクライアントが送るものを何でも受け付けます。長さ、形式、enum、所有関係は、データベース（制約、トリガー）かEdge Functionで確認し、Reactだけで済ませてはいけません。

### 12. Supabase Security Advisor：すべての警告

Advisorsを開き、Security Advisorを選んで、すべての指摘を修正します。RLSがオフのテーブル、RLSを飛び越えるビュー、ユーザーメタデータを読むポリシー、許容的な`using (true)`ポリシー、`search_path`が変更可能な関数を指摘してくれます。確認1、5、9、10は見えません。これらは設定ではなくロジックの問題だからです。

## 最初の12項目では見えないことを、Lovableアプリは何で教えてくれた？

これらはクライアントのアプリとそのレビューから出てきたものです。どれもスキャナーには現れません。

1. **クライアントが設定してはいけないフィールド。** クライアントが`status: 'approved'`を自分で送り、モデレーションを飛び越えました。`with check`が`using`より緩いUPDATEポリシーでも、ユーザーが行を別の所有者に移せます。ポリシーは値を計算できないので、`BEFORE INSERT OR UPDATE`トリガーが、`status`、ロール、価格、会社をサーバー自身のデータから再計算し、クライアントが送った値を無視します。
2. **メインのINSERTだけでなく、すべての書き込み経路。** メインのINSERTを直した後も、同じ抜け道が添付ファイル、コメント、論理削除したアイテムの復元、カウンターに残っていました。最初の1つを直す前に、保護された列に書き込むものをすべて洗い出してください。
3. **拒否リストではなく許可リスト。** 偽造されたレコードは5回のレビューを要しました。修正のたびに1つの組み合わせを塞いでも、似た組み合わせが開いたままだったからです。typeとstatusの組み合わせのうち一覧にあるものだけを許可するトリガーが、クラス全体を一度に塞ぎました。
4. **関数もAPI。** `public`のすべての関数は、EXECUTE権限を持つ任意のロールが`/rpc`経由で呼び出せ、RLSは関数には適用されません。すべての`SECURITY DEFINER`関数に`set search_path = ''`を付け、`public`、`anon`、`authenticated`からEXECUTEを取り消し、必要なロールにだけ付与します。
5. **EXECUTEの取り消しが公開ページを壊すことがある。** Postgresは、そのロールに適用されるすべてのポリシーが使うすべての関数のEXECUTEを、実行されない分岐の中であっても確認します。`anon`が`is_admin()`のEXECUTEを失った途端、すべての匿名の読み取りが42501で失敗しました。管理者専用のポリシーは`to authenticated`にし、公開の読み取りには管理者の分岐を含まない専用のポリシーを用意します。
6. **メール確認がオンのサインアップ。** 確認がオンだと、`signUp`はセッションを返さないので、その直後のプロフィール書き込みは`anon`として実行され、何も書き込まれません。しかもエラーも出ません。Lovableのプロジェクトは自動確認がオンのことが多く、そのせいでバグが隠れます。フィールドは`signUp({ options: { data } })`で渡し、新規ユーザーのトリガーに検証と書き込みをさせ、そのメタデータは信頼できないものとして扱います（確認4）。
7. **誤った理由で通るテスト。** ローカルのスタックとホスト版のプロジェクトでは、デフォルトの権限が違っていました。そのため「anonはINSERTできない」がローカルで通ったのは、RLSが働いたからではなく、grantがなかったからでした。否定のテストが意味を持つのは、肯定側のgrantが存在するときだけです。ローカルと本番で`pg_default_acl`を比較してください。
8. **マイグレーションファイルではなく、稼働中のデータベース。** マイグレーションやプロジェクトの移行の後は、実際に稼働しているものを比較します。`pg_policies`、各ガードの`pg_get_functiondef`、そして上記のgrantです。別のアプリでは、新しいマイグレーションが古い関数の本体をコピーし、後の修正を黙って元に戻していました。

## LovableアプリはGDPRに準拠している？

アプリがGDPRに準拠するかどうかをLovableが決めるわけではありません。決めるのはアプリが個人データをどう扱うかで、セキュリティレビューではそこに答えが出ません。Lovable Cloudのアプリで私が確認する6つの抜けと、それぞれの直し方です。

1. **処理者をすべて洗い出す。** Lovable Cloudはデータベース、認証、ストレージ、関数をSupabase上で動かし、Lovableはサイトをホストしてサインインメールも送れます。Lovable AI Gatewayはテキストをモデルの提供元に渡します。関数が呼び出すメール、AI、スクレイピングのサービスもすべて加えてください。個人データを受け取るサービスにはそれぞれデータ処理契約（DPA）が必要で、EU域外のサービスには移転の根拠も必要です。ホスティングのリージョンはコードに出てこないので、問い合わせてください。
2. **コンテンツを持つユーザーを削除してみる。** そのユーザーが書いたものからの外部キーが削除を止めることがあり、そうなると削除請求に応えられません。外部キーをcascadeかset nullにし、ユーザーのメールログとログイントークンも消す削除トリガーを追加し、そうしたユーザーを削除するデータベーステストを残してください。
3. **保存期間を決める。** メールログ、閲覧の記録、失敗したメールのキュー、論理削除した行は、個人データを期限なしで持ち続けがちです。論理削除した行を一定期間後に物理削除し、そのファイルも消す定期ジョブで、ほとんどが片付きます。
4. **メールの開封計測は同意制にする。** 受信者ごとに開封とクリックを記録するには、ePrivacy規則のもとで同意が必要です。同意フラグは既定でオフにし、タイムスタンプはクライアントではなくサーバーが設定します。
5. **関数のログにアドレスを残さない。** Edge Functionのログは自動でマスクされないので、受信者のアドレスを含む`console.log`はそのアドレスを残します。IDと件数を記録してください。
6. **メール確認をオンにする。** 自動確認がオンだと、誰でも自分のものではないアドレスで登録でき、実在する人のアドレスがアプリに入ります。登録側の対応は上のリストの6で扱っています。

自分のアプリを確認するには、関数内のAPIキーを探してサービスを洗い出し、コンテンツを持つテストユーザーを削除し、関数のログで`@`を検索し、各提供元にデータ処理契約を求めてください。これは法的助言ではありません。

## 再発をどう防ぐ？

上の確認はすべてクエリかテストなので、リポジトリに入れ、デプロイのたびに実行してください。Supabaseの[pgTAPテスト](https://supabase.com/docs/guides/database/extensions/pgtap)でgrant、ポリシー、関数をカバーできます。2社テストはその隣に置きます。アプリにロールと権限画面があるなら、ポリシーごとのテストでも足りません。私は、権限のチェックボックスを1つずつ本番のコピーに対して証明しています。その方法は[Supabase RLSのテスト方法](https://madsalt.tech/ja/blog/how-to-test-supabase-rls/)にあり、87のチェックボックスのうち30が何もしていませんでした。無料の[Supabase セキュリティチェック](https://madsalt.tech/ja/tools/supabase-security-check/)は、プロジェクトのロール、grant、ポリシー、認証設定に対して46件のOWASP Top 10:2025ルールを実行するので、最初のチェックに使ってください。2社テストと、UIに関することは手作業のままです。

このリストがカバーしていないもの：決済とWebhook、Git履歴の中のシークレット、ホスト側のセキュリティヘッダー、バックアップ。どれも重要ですが、別のリストになります。

引き継いだLovableやSupabaseのアプリが、これらのうちどれを通過するのか分からない場合、この監査が[プロジェクトの救出](https://madsalt.tech/ja/services/project-rescue/)の出発点です。[監査法人の工数管理の事例](https://madsalt.tech/ja/work/audit-firm-time-tracking/)で、本番アプリのロール確認がどのようなものかを見られます。

このリストにない穴を見つけましたか？

ぜひ聞かせてください。LovableとSupabaseのアプリを守る方法について、情報交換しましょう。

[お問い合わせ](https://madsalt.tech/ja/#contact)
