最初に結論を述べます。Supabaseのデータベース関数(RPC)が SECURITY DEFINER で定義され、かつ search_path を固定していないと、認証済みの一般ユーザーが「定義者(多くの場合 postgres)の権限」で任意のオブジェクトをすり替えられ、RLSを迂回して権限昇格できます。 これはSupabase固有のバグではありません。PostgreSQLの SECURITY DEFINER が昔から持つ古典的な落とし穴が、テーブルや関数を自動でREST/RPCとして公開するSupabaseという環境で、攻撃面として顕在化したものです。
行レベルセキュリティ(RLS)を完璧に張っても、その「外側」にこの穴は空きます。SECURITY DEFINER 関数は定義者の権限で動き、定義者がテーブル所有者ならRLSを飛び越えるからです。本記事は、(1) SECURITY DEFINER と SECURITY INVOKER の違い、(2) なぜ関数・RPCがRLSを迂回しうるのか、(3) search_path 未固定がどう攻撃に化けるのか、(4) 脆弱→修正の実SQL、(5) 検出の機械化、を一次情報と実コードで解説します。そして最後に正直な線引きをします——この穴の「検出」は機械化できますが、「その関数の権限設計が正しいか」は人間の判断です。これはアプリ層セキュリティ全体の地図の一部で、全体像はNext.js × Supabase アプリケーションセキュリティ完全ガイドに整理しています。
1. SECURITY DEFINER と SECURITY INVOKER —— 誰の権限で実行されるか
PostgreSQLの関数には、実行時に「誰の権限を使うか」を決める2つのモードがあります。
- SECURITY INVOKER(既定) —— 関数を呼び出したユーザーの権限で実行される。呼び出し元が
authenticatedなら、関数の中身もauthenticatedの権限で動く。 - SECURITY DEFINER —— 関数を**作成・所有するユーザー(定義者)**の権限で実行される。Unixの
setuidプログラムと同じで、呼び出し元が誰であっても、中身は定義者の権限で走る。
公式ドキュメント(PostgreSQL: CREATE FUNCTION)はこう述べています——「SECURITY DEFINER 関数は、それを所有するユーザーの権限で実行されるため、誤用されないよう注意が必要だ」。つまり SECURITY DEFINER は、意図的に権限の壁を越えるための機能です。便利ですが、越える先が高権限(Supabaseでは多くの関数が postgres 所有)なら、設計を一つ誤るだけで権限昇格の踏み台になります。
| 観点 | SECURITY INVOKER(既定) | SECURITY DEFINER |
|---|---|---|
| 実行時の権限 | 呼び出したユーザー | 関数の定義者(所有者) |
| RLSの効き方 | 呼び出し元に対して効く | 定義者がテーブル所有者なら飛び越える |
| 典型用途 | 通常のクエリ・計算 | 一般ユーザーに権限を「貸す」処理(集計・通知・横断更新) |
| 危険度 | 低い(権限の拡大が無い) | 高い(権限の壁を越える) |
SECURITY DEFINER 自体は悪ではありません。正当な用途もあります——「一般ユーザーには直接 select させたくないテーブルから、集計値だけを関数経由で返す」「監査ログのように、本人には更新させたくない行を関数の内側だけで追記する」といったケースです。鍵は、権限を貸す範囲を関数の内側に閉じ込め、貸した先で攻撃者に主導権を渡さないこと。search_path 未固定は、まさにこの「貸した先」で主導権を奪われる経路です。
重要なのは、RLS迂回の起点はこの「権限の差」そのものだという点です。SECURITY INVOKER の関数なら、search_path を乗っ取られても呼び出し元の権限以上のことはできません。SECURITY DEFINER だからこそ、乗っ取りが「権限昇格」に化けます。だから最初の問いは常に「この関数は本当に SECURITY DEFINER である必要があるか」です(第5節で詳述)。
2. なぜ関数・RPC が RLS を「飛び越える」のか
Supabaseは、データベース関数を PostgREST 経由で自動的にRPCエンドポイントとして公開します。anon / authenticated ロールに EXECUTE 権限があれば、ブラウザから次のように呼べます。
# データベース関数 is_admin() を RPC として呼ぶ(anon キーだけで叩ける)
curl "https://<project>.supabase.co/rest/v1/rpc/is_admin" \
-H "apikey: <anon-key>" \
-H "Authorization: Bearer <user-jwt>" \
-H "Content-Type: application/json" -d '{}'
ここでRLSとの関係が問題になります。PostgreSQLのRLSは「現在のユーザー」を基準に評価されますが、公式ドキュメント(PostgreSQL: Row Security Policies)が明記するとおり、テーブルの所有者は通常RLSをバイパスし(FORCE ROW LEVEL SECURITY を明示しない限り)、スーパーユーザーや BYPASSRLS 属性を持つロールは常にバイパスします。
Supabaseのダッシュボードやマイグレーションで作った関数は、多くの場合 postgres が所有します。postgres は public スキーマのテーブルも所有しています。したがって——
SECURITY DEFINER関数(postgres所有)の中でpublicのテーブルを触ると、その問い合わせはpostgresとして実行され、RLSが効かない。
これは、サーバー側で service_role キーを使ってRLSを飛び越えるのと同じ構図を、データベースの内側で作っていることになります。service_role キーの危険性とRLSバイパスについてはanonキーとservice_roleキーの露出で扱っていますが、SECURITY DEFINER 関数は「鍵を漏らしていないのに、DBの中に同じバイパス経路を作ってしまう」点で見落とされやすい穴です。
-- 例:profiles に完璧な RLS を張っていても…
alter table public.profiles enable row level security;
create policy "read own profile" on public.profiles
for select to authenticated using ( (select auth.uid()) = id );
-- この SECURITY DEFINER 関数経由なら、RLS を素通りして全行を集計できてしまう
create function public.count_all_profiles()
returns bigint language sql security definer as $$
select count(*) from public.profiles; -- postgres 権限=RLS が効かない
$$;
count_all_profiles() 単体は「件数を返すだけ」で無害に見えます。しかし**「RLSが効かない実行コンテキスト」をRPCとして外に開けた**こと自体がリスクの本体です。次節で見るように、search_path が未固定だと、この実行コンテキストを攻撃者に乗っ取られます。
3. search_path 未固定という攻撃面
search_path は、スキーマ修飾なしで書かれたオブジェクト名(テーブル・関数・型・演算子)を、どのスキーマから順に探すかを決める設定です。SECURITY DEFINER 関数に SET search_path を付けないと、関数は呼び出し側の search_path をそのまま継承します。
ここに攻撃の余地が生まれます。SECURITY DEFINER 関数の本体が、オブジェクトを非修飾で参照していると、その名前が「攻撃者の用意した別物」に解決されうるのです。
3-1. 脆弱な関数:非修飾の参照を持つ SECURITY DEFINER
-- 脆弱:SECURITY DEFINER なのに search_path 未固定。本体は profiles を非修飾で参照
create function public.is_admin()
returns boolean
language plpgsql
security definer -- 定義者(postgres)権限で動く=RLS を飛び越える
as $$
declare
result boolean;
begin
-- ↓ 非修飾の "profiles"。search_path 次第で別のオブジェクトに解決されうる
select role = 'admin' into result
from profiles
where id = auth.uid();
return coalesce(result, false);
end;
$$;
3-2. 攻撃:一時スキーマ・public に「同名の別物」を差し込む
攻撃者は認証済みの一般ユーザーで構いません。やることは、is_admin() の中の非修飾 profiles を、自分が作った同名オブジェクトにすり替えることです。
-- 攻撃者(authenticated):一時テーブルで profiles を差し替える
create temp table profiles (id uuid, role text);
insert into profiles values (auth.uid(), 'admin');
-- pg_temp(一時スキーマ)は、非修飾のテーブル参照では既定で「最初」に探索される。
-- そのため is_admin() 内の "profiles" が攻撃者の一時テーブルに化け、
-- 定義者(postgres)権限で実行されている関数が "admin" を返す
select public.is_admin(); -- → true(本来 admin でないのに昇格)
なぜ一時テーブルが効くのか。ここはPostgreSQLの探索規則の中でも誤解されやすい点なので、正確に押さえます。
- テーブル・ビュー・型などのリレーション名:一時スキーマ
pg_tempは、search_pathに明示されていない場合、既定で最初に探索されます(pg_catalogよりも前)。そして一時テーブルの作成権限(TEMP)は既定で全ユーザーに付与されているため、誰でもこの差し替えを実行できます。これが最も普遍的に成立する攻撃ベクトルです。 - 関数・演算子名:
pg_tempからは解決されません。ただし、search_pathに「攻撃者がCREATE権限を持つスキーマ」(典型的にはpublic)が含まれていれば、そこに同名関数を仕込んで乗っ取れます。
つまり search_path 未固定の SECURITY DEFINER 関数は、「攻撃者が書き込めるスキーマに置いた同名オブジェクト」で、定義者権限のコードを乗っ取られるのが本質です。is_admin() のように戻り値が認可判断に使われていれば権限昇格に、SECURITY DEFINER 関数の中で更新や EXECUTE を行っていれば任意処理の実行につながります。公式のCREATE FUNCTIONも、この「信頼できないユーザーが書き込めるスキーマを探索経路に含めないこと」を SECURITY DEFINER の必須の注意点として挙げています。
4. 安全パターン —— 脆弱→修正の実SQL
防御の核心は、**「探索経路を呼び出し側に委ねない」かつ「オブジェクトを攻撃者がすり替えられないよう特定する」**の2点です。具体的には3つの対策を同時に適用します。
4-1. search_path を固定し、すべてをスキーマ修飾する(Supabase 推奨の最厳)
-- 修正:search_path を空に固定し、本体のオブジェクトをすべてスキーマ修飾する
create or replace function public.is_admin()
returns boolean
language plpgsql
security definer
set search_path = '' -- ← 呼び出し側の search_path を無効化する
as $$
declare
result boolean;
begin
select role = 'admin' into result
from public.profiles -- ← スキーマ修飾。一時スキーマや public 差し込みに化けない
where id = (select auth.uid()); -- ← auth.uid() も auth スキーマで修飾済み
return coalesce(result, false);
end;
$$;
ここで正直に補足すべき重要な点があります。set search_path = '' は「魔法の盾」ではありません。空にすると、非修飾の名前は pg_catalog(組み込み関数)以外ほぼ解決できなくなり、書き忘れがエラーとして表面化する——これが狙いです。しかし前述のとおり、非修飾のリレーション名は空の search_path でも pg_temp が先に探索されます。したがって実際に攻撃を無効化しているのは「スキーマ修飾」そのものであり、set search_path = '' は「修飾し忘れを黙って public に落とさず、必ず明示させる」ための土台です。'' を付けただけで本体に非修飾参照が残っていれば、依然として乗っ取られえます。両者は必ずセットです。
pg_catalog は search_path を空にしても暗黙に探索されるため、now() や lower() などの組み込み関数は引き続き使えます。修飾が要るのは、public・auth など自前/拡張スキーマのオブジェクトです。
4-2. 代替:信頼スキーマに固定し、pg_temp を必ず最後に置く
set search_path = '' が厳しすぎる(既存の非修飾参照が多い)場合、PostgreSQL公式が示す作法は**「信頼できるスキーマに固定し、pg_temp を末尾に置く」**ことです。末尾に置けば、一時スキーマは最後にしか探索されず、リレーションのすり替えを無効化できます。
-- 代替:信頼スキーマを先に、pg_temp を最後に固定する(公式 CREATE FUNCTION の作法)
create or replace function public.is_admin()
returns boolean
language plpgsql
security definer
set search_path = public, pg_temp -- 信頼スキーマ → pg_temp は必ず末尾
as $$ ... $$;
ただしこの形は、public 自体が攻撃者に書き込み可能でないことが前提です。PostgreSQL 15以降は public への CREATE がデフォルトで PUBLIC に付与されなくなったため成立しやすくなりましたが、古い設定を引き継いだプロジェクトでは要確認です。迷ったら最厳の set search_path = '' + 完全修飾を選ぶのが安全側です。
4-3. GRANT を最小化する —— 誰が呼べるかを絞る
CREATE FUNCTION は、既定で PUBLIC(全ロール)に EXECUTE を付与します。Supabaseでは、これがそのまま「anon でも叩けるRPC」になります。SECURITY DEFINER 関数では、まず暗黙の EXECUTE を剥がし、必要なロールにだけ与え直します。
-- GRANT 最小化:PUBLIC への暗黙の EXECUTE を剥がし、必要なロールにだけ許可する
revoke execute on function public.is_admin() from public;
grant execute on function public.is_admin() to authenticated;
-- anon(未ログイン)に開ける必要が無いなら、絶対に grant しない
これは「探索経路の固定」とは別軸の防御です。search_path を固定しても、そもそも anon に開ける必要のない権限昇格関数を anon が叩ける状態なら、攻撃面は広いままです。「誰の権限で動くか(DEFINER)」と「誰が呼べるか(GRANT)」は独立に最小化します。
4-4. そもそも SECURITY DEFINER が要るかを問う
最も効くのは、危険な機能を使わないで済ませることです。多くの関数は SECURITY INVOKER(既定)で十分で、その場合RLSは呼び出し元に対して正しく効き、search_path 乗っ取りも権限昇格になりません(呼び出し元の権限を超えられないため)。SECURITY DEFINER を選ぶのは、「一般ユーザーの権限では届かない処理を、限定的に肩代わりさせる」明確な理由があるときだけにします。PostgreSQL 15以降は SECURITY INVOKER を明示できるので、意図をコードに残しておくとレビューが楽になります。
5. Supabase で見かける危険な RPC パターン
実際の現場で繰り返し見かける形を挙げます。いずれも「動く」ので、デモやレビューをすり抜けます。
パターンA:管理機能を SECURITY DEFINER で公開し、GRANT も搾れていない
-- 危険:ロール昇格を SECURITY DEFINER で公開。search_path 未固定 + 既定で PUBLIC に EXECUTE
create function public.promote_to_admin(target uuid)
returns void
language sql
security definer -- RLS を飛び越えて profiles を更新できる
as $$
update profiles set role = 'admin' where id = target; -- 非修飾&所有権チェックなし
$$;
-- 既定の PUBLIC EXECUTE が残るため、anon/authenticated から rpc/promote_to_admin を叩ける
この関数には3つの欠陥が同居しています。(1) search_path 未固定(非修飾 profiles)、(2) PUBLIC への EXECUTE が残存、そして (3) 「呼び出し元が誰であれ、任意の target を admin にできる」という認可ロジックそのものの欠陥です。
ここが分かれ道です。(1) と (2) は機械的に検出・修正できます。set search_path = '' を足し、revoke ... from public すればよい。しかし (3) は search_path を完璧に固めても残ります。 「この関数は誰が呼んでよく、誰を昇格してよいのか」——update public.profiles set role = 'admin' where id = target and <呼び出し元が管理者である条件> のような認可判断は、あなたの業務ルールにしか答えがありません。これが本記事の正直な核心です(第8節)。
パターンB:RLSが効かない集計・横断取得を素通しで開ける
-- 危険:ダッシュボード用の横断集計を SECURITY DEFINER で公開(search_path 未固定)
create function public.org_dashboard(org uuid)
returns table(total int, revenue numeric)
language sql
security definer
as $$
select count(*), coalesce(sum(amount), 0)
from invoices where org_id = org; -- 非修飾&「呼び出し元がこの org に属するか」未検証
$$;
便利な「横断取得」ほど SECURITY DEFINER にされがちで、しかも org をクライアントから受け取って所有権を確かめない——これは関数の内側で起こす IDOR(オブジェクト単位の認可欠陥)です。修正は search_path 固定+スキーマ修飾に加えて、本体に and exists (select 1 from public.memberships where user_id = auth.uid() and org_id = org) のような所有権条件を入れること。やはり機械化できるのは前半(探索経路の安全化)までで、所有権条件の定義は設計です。
6. 検出 —— migrations と pg_proc の静的検証
「SECURITY DEFINER かつ search_path 未固定」という形は、機械的に洗い出せます。ここは自動化が本領を発揮する領域です。
6-1. 稼働中DBを直接調べる(pg_proc)
カタログ pg_proc には、関数が SECURITY DEFINER か(prosecdef)、どんな SET を持つか(proconfig)が記録されています。これを使えば一覧化できます。
-- SECURITY DEFINER 関数のうち search_path を固定していないものを洗い出す
select
n.nspname as schema,
p.proname as function,
pg_get_userbyid(p.proowner) as owner, -- 定義者=この権限で動く
p.proconfig as config -- SET 句。null なら呼び出し側を継承
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where p.prosecdef -- SECURITY DEFINER のみ
and n.nspname not in ('pg_catalog', 'information_schema')
and (
p.proconfig is null
or not exists (
select 1 from unnest(p.proconfig) as cfg
where cfg like 'search_path=%'
)
)
order by 1, 2;
これで「未固定の SECURITY DEFINER 関数」がゼロ件であることを、リリース前のチェックとして確認できます。SupabaseのDatabase Linterにも、同種を警告する function_search_path_mutable ルールがあります。
6-2. migrations を静的検証する(CIで番をさせる)
稼働DBに繋がなくても、supabase/migrations/**.sql を読めば同じ欠陥を見つけられます。RLS設定ミスの体系的な洗い出しはRLS設定ミスの検出と監査にまとめていますが、SECURITY DEFINER 関数については特に次の3点をマイグレーションから機械的にフラグします。
security definerを含むが、同じ定義にset search_pathが無いset search_pathはあるが、空でもpg_temp末尾でもない(=攻撃者書き込み可能なスキーマが先頭にありうる)- 関数定義の後に
revoke execute ... from publicが無い(既定のPUBLICEXECUTE が残存)
私が公開しているOSS Aegis は、supabase/migrations を解析してこれらを検出します。インストール不要で走ります。
# インストール不要・設定不要でスキャン(未固定の SECURITY DEFINER/過剰 GRANT を可視化)
npx @aegiskit/cli scan
正規表現だけだと $$ ... $$ の本体や複数行定義で取りこぼすため、実用にはSQLをパースして「定義ブロック単位」で属性を見るのが確実です。重要なのは、この一次フィルタをCIに常設し、未固定の SECURITY DEFINER が新たに混入したらビルドを止めることです。人間の記憶ではなく、機械に番をさせます。
7. 本番前チェックリスト
外注でもAI生成でも、SECURITY DEFINER 関数を本番に出す前に最低限これだけは確認してください。
- そもそも
SECURITY DEFINERが必要か。SECURITY INVOKER(既定)で足りないかを先に問うた - すべての
SECURITY DEFINER関数にset search_path = ''(または信頼スキーマ+pg_temp末尾)を明示している - 関数本体のテーブル・型・関数はすべてスキーマ修飾(
public.foo・auth.uid())。非修飾参照がゼロ -
set search_path = ''を「付けただけ」で満足せず、修飾とセットになっている - 関数定義の直後に
revoke execute ... from publicし、必要なロールにだけgrantしている -
anon(未ログイン)に開ける必要のない関数を、anonから呼べる状態にしていない - 関数本体に、呼び出し元の所有権・権限を確かめる条件が入っている(昇格・横断取得・更新系は特に)
-
pg_procクエリ(第6節)で「未固定のSECURITY DEFINER」がゼロ件であることを確認した - migrations の静的検証(
npx @aegiskit/cli scan等)をCIに常設している
発注者・レビュアーの視点で最も効く質問は、**「この関数はなぜ SECURITY DEFINER なのですか?」「search_path は固定していますか?」「これは誰が呼べますか(anon でも叩けますか)?」**の3つです。設計を理解している開発者なら即答できます。
8. どこまで機械化でき、どこから設計か(正直に)
最後に、線を引きます。誇張は信頼を損なうので、できることとできないことを分けて述べます。
機械化できること(検出・警告)。 「SECURITY DEFINER かつ search_path 未固定」「非修飾参照の残存」「PUBLIC への過剰 EXECUTE」——これらは形の問題なので、pg_proc や migrations の静的検証で機械的に洗い出せます。OWASPのApplication Security Verification Standard(ASVS)が説く「セキュリティは『入れたか』ではなく『検証できるか』で測る」という規律に、この層はよく合致します。まずは Aegis(無料OSS、npx @aegiskit/cli scan)で現状を可視化するのが、最もコスパの良い第一歩です。
機械化できないこと(設計判断)。 一方で、ツールが答えられない問いが残ります——「この関数は本当に SECURITY DEFINER であるべきか」「定義者の権限で何をどこまで肩代わりさせてよいか」「誰がこのRPCを呼んでよいか」「本体の認可ロジック(所有権・権限の条件)は正しいか」。第5節の promote_to_admin が示したとおり、search_path を完璧に固めても、関数のロジックが『誰でも誰でも昇格できる』なら依然として脆弱です。これらはあなたのデータモデルと業務ルールを理解した人間にしか判断できません。いかなるツールも、search_path 未固定という罠は検出できても、関数の権限設計が『正しい』ことは証明しません。「スキャンが通った=よくある罠は踏んでいない」であって、「安全になった」ではない——この区別を曖昧にする製品は、むしろ油断を生みます。
だからこそ線引きが要ります。どこまで自動の検出で固め、どこから人間のレビューが要るか。 SECURITY DEFINER 関数の権限設計や、既存Supabaseアプリの認可・RLSレビューが必要なら、セキュリティ監査で承ります。私自身、木材流通業界のDX案件で、RLS・テナント分離・権限境界を含むデータ層の認可を実運用で設計・検証してきました。検出の機械化は、人間が本当に難しい設計判断に集中するための土台です。
よくある質問(FAQ)
Q. set search_path = '' を付ければ、それで安全になりますか?
A. それだけでは不十分です。空にする目的は「非修飾参照をエラーとして表面化させ、必ずスキーマ修飾を強制する」ことであり、実際に攻撃を無効化しているのはスキーマ修飾そのものです。非修飾のリレーション名は空の search_path でも一時スキーマ(pg_temp)が先に探索されるため、'' を付けても本体に非修飾参照が残れば乗っ取られえます。''(固定)と完全修飾は必ずセットにしてください。
Q. すべての関数で search_path を固定すべきですか?
A. SECURITY DEFINER 関数では必須です。SECURITY INVOKER(既定)でも固定する習慣は良い(挙動が呼び出し側に左右されなくなる)ですが、優先順位は圧倒的に SECURITY DEFINER が先。権限昇格に直結するのはそちらだからです。
Q. RLSさえ正しく張れば、SECURITY DEFINER の心配は要りませんか?
A. いいえ。SECURITY DEFINER 関数は定義者(多くは postgres=テーブル所有者)の権限で動くため、RLSを飛び越えます。RLSは「呼び出し元の権限」に対して効くもので、定義者権限で実行されるコードには効きません。RLSとは独立に、関数側で search_path 固定・修飾・GRANT最小化・認可ロジックを担保する必要があります。
Q. 一般ユーザーが本当に一時テーブルなんて作れるのですか?
A. 作れます。一時オブジェクトの作成権限(TEMP)は、PostgreSQLでは既定で全ロールに付与されています。public への CREATE は新しい環境では絞られていることが多い一方、pg_temp 経由のリレーションすり替えはより普遍的に成立するため、SECURITY DEFINER の主要な攻撃面として常に想定すべきです。
Q. 既存の大量の関数を、どう棚卸しすればいいですか?
A. まず第6節の pg_proc クエリで「未固定の SECURITY DEFINER」を全件抽出し、件数を把握します。次に npx @aegiskit/cli scan で migrations 側も突き合わせ、CIに常設して再発を止めます。修正は「固定+修飾+GRANT最小化」をテンプレ化すれば機械的に進みますが、各関数の認可ロジックの妥当性だけは1本ずつ人間が確認してください。
まとめ:探索経路を呼び出し側に委ねない
要点を整理します。
SECURITY DEFINER関数は定義者の権限で動く。Supabaseでは多くがpostgres所有のため、RPCとして呼ぶとRLSを飛び越える。RLS迂回の起点はSECURITY INVOKER(既定)との「権限の差」そのもの。search_pathを固定しないと、本体の非修飾のテーブル/関数参照が呼び出し側の探索経路で解決される。攻撃者は一時スキーマ(pg_temp)やpublicに同名オブジェクトを差し込み、定義者権限で自分のコードを実行させられる(権限昇格)。- 安全パターンは3点セット——(1)
set search_path = ''(または信頼スキーマ+pg_temp末尾)に固定、(2) オブジェクトをすべてスキーマ修飾、(3)EXECUTEをPUBLICから剥がし必要なロールにだけgrant。''は修飾の強制装置であって、修飾とセットで初めて効く。 - 「未固定の
SECURITY DEFINER」という形は、pg_procや migrations の静的検証で機械的に検出でき、CIに番をさせられる。 - ただし「その関数を
SECURITY DEFINERにすべきか」「誰に呼ばせるか」「本体の認可ロジックが正しいか」は設計判断。ツールは罠を検出できても、権限設計の正しさは証明しない。
AIで速く作ること自体は正しい。速く作ったSupabaseアプリのデータ層に潜む SECURITY DEFINER の落とし穴を検出・修正し、権限設計のレビューが必要であれば、お気軽にご相談ください。