TL;DR
raw_user_meta_dataには、ブラウザがsupabase.auth.signUp()に渡した内容がそのまま入ります。ある本番アプリではhandle_new_userトリガーが新規ユーザーのロールをここから読んでいたため、誰でもOwnerロールのIDでサインアップして、アカウントを所有できました。修正は、ロールをサーバーだけが書き込めるテーブルから取得し、そのテーブルを招待の送信前に埋めることです。
これは監査法人の工数管理アプリでの話です。アクセスルールがすべてデータベースにあるSupabaseアプリで、バグは新規ユーザーごとにプロフィールを作るトリガーにありました。
トリガーは何をしていたのか?
簡略化すると、次のようなものでした。
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
declare
_role_id uuid := (new.raw_user_meta_data ->> 'role_id')::uuid;
begin
insert into public.profiles (id, full_name, role_id)
values (new.id, new.raw_user_meta_data ->> 'full_name', _role_id);
return new;
end;
$$;
招待フローが正しいrole_idをそこに入れていたので、通常の使い方では動いていました。しかしraw_user_meta_dataはサインアップのoptions.dataであり、何を入れるかを決めるのはブラウザです。
await supabase.auth.signUp({
email: 'me@example.com',
password: '...',
options: { data: { role_id: '<owner role uuid>' } },
})
権限チェックは、他の何かを見る前にOwnerなら真を返していました。そのため、このフィールド1つでアカウントを完全に掌握できました。Supabase自身のUsersガイドにも、ユーザーメタデータはRLSポリシーや認可ロジックに使ってはならないと書かれています。ユーザーが何の検証もなしに編集できるからです。
何が修正になったのか?
ロールは、クライアントが触れないプライベートなテーブルから取得するようにしました。
create table private.pending_invites (
email text primary key,
role_id uuid,
invited_by uuid,
created_at timestamptz default now()
);
revoke all on table private.pending_invites from anon, authenticated;
招待の関数はサーバー上で動き、招待する側の権限を確認します。先に保留中の招待を書き込み、そのあとで招待リンクを作成します。トリガーはメールアドレスでロールを検索し、その行を削除します。名前は引き続きメタデータから取ります。表示用のデータなので問題ありません。
順序が重要です。招待するとユーザーはすぐに作成されます。SupabaseのUsersガイドには、新しいメールアドレスを招待すると未確認のユーザーが作成されると書かれています。トリガーはその時点で発火するため、保留中の行はすでに存在していなければなりません。
自分のアプリをどう確認する?
handle_new_user関数、またはauth.users上の任意のトリガーを開き、raw_user_meta_dataを検索してください。そこから名前やアバターを取るのは問題ありません。ロール、会社、プラン、テナントなど、アクセスを与えるものは、テーブルか、ユーザーが変更できないraw_app_meta_dataから取る必要があります。
そのうえで試してください。APIでoptions.dataにrole_idまたはrole: 'admin'を入れてサインアップします。新しいアカウントには、何も余分に付与されないはずです。
この種の確認は、私のRLSテスト方法の最初のステップです。マルチテナントのアプリでは、2つの組織に属するユーザーでRLSが見逃すものもあわせて読んでください。すべてのSupabaseアプリで私が実行しているその他の確認は、Lovableアプリのセキュリティチェックリストにあります。ダッシュボードで私が最初に見るのは、Data APIが公開しているスキーマです。Supabase セキュリティチェックは、ロール、grant、ポリシー、認証設定を45件のOWASP Top 10:2025ルールに照らして調べますが、トリガーがサインアップのメタデータから何をコピーするかは対象外なので、この確認は手作業のままです。