本文へスキップ

Supabase raw_user_meta_data:登録だけでOwnerに

Supabase

Markdownで表示Claudeで開くChatGPTで開くPerplexityで開く

質問もコピーされます。チャットが空で開いた場合は貼り付けてください。

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ポリシーや認可ロジックに使ってはならないと書かれています。ユーザーが何の検証もなしに編集できるからです。

新規ユーザーのロールの出どころ。修正前と修正後のhandle_new_userトリガー。修正前:サインアップした誰でも:signUp options.data → raw_user_meta_data → handle_new_user → Ownerロール、修正後:招待された人だけ:招待関数 → pending_invites → handle_new_user → 招待されたロール。
本番のSupabaseアプリのサインアップトリガーより、2026年6月。

何が修正になったのか?

ロールは、クライアントが触れないプライベートなテーブルから取得するようにしました。

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ルールに照らして調べますが、トリガーがサインアップのメタデータから何をコピーするかは対象外なので、この確認は手作業のままです。

© 2026 Denys Zaichenko. 元の記事: https://madsalt.tech/ja/notes/supabase-raw-user-meta-data-role/