積み上げログ技術ブログ記事一覧へ

自主執筆記事

SupabaseのRLSを固めたのに、匿名キーで承認関数を呼べた|PostgreSQLの関数はPUBLICに実行権限がある

静的サイトの投稿機能をSupabaseで作った。行レベルセキュリティ(RLS)は3つの条件を満たすよう慎重に書いた。ところが匿名キーで実際に攻撃してみたら、投稿者が自分の投稿を承認済みにできてしまった。PostgreSQLの関数は既定でPUBLICにEXECUTE権限が付くためだった。

セキュリティ公開: 2026/08/24更新: 2026/08/24
SupabasePostgreSQLRLSセキュリティ静的サイト

01 / 本文

結論:PostgreSQLの関数は、作った時点でPUBLICが実行できる

CREATE FUNCTION で関数を作ると、PUBLIC ロールに EXECUTE 権限が自動で付く。これはPostgreSQLの既定の動作だ。

Supabase では、ブラウザから匿名キー(anon)で RPC を呼べる。つまり、承認や削除のような管理者だけが実行すべき関数も、誰でも呼べる状態になっている。

テーブルのRLSをどれだけ丁寧に書いても、関数経由で迂回されれば意味がない。REVOKE を明示的に書く必要がある。

-- これだけでは、匿名キーから呼べてしまう
create or replace function public.approve_review(review_id bigint)
returns void language sql security definer set search_path = public as $$
    update public.performer_reviews
       set status = 'approved', reviewed_at = now()
     where id = review_id;
$$;

-- PUBLIC から取り消す。anon / authenticated だけでは足りない
revoke execute on function public.approve_review(bigint) from public;
revoke execute on function public.approve_review(bigint) from anon, authenticated;

-- 今後作る関数にも既定で付かないようにする
alter default privileges in schema public revoke execute on functions from public;

02 / 本文

何を作っていたか

GitHub Pages で配信している静的サイトに、投票と口コミの機能を付けたかった。保存先が要るので Supabase を使った。

ブラウザから匿名キーで直接読み書きする構成なので、守りは RLS だけが頼りになる。次の3つを満たすよう設計した。

1. 口コミは、承認したものしか読めない

2. 投稿は必ず未承認から始まる(投稿者が承認済みで入れられない)

3. 投稿者は、自分の投稿を後から書き換えたり消したりできない

ポリシーは丁寧に書いたつもりだった。INSERT のポリシーで status = 'pending' を強制し、UPDATEDELETE のポリシーは作らなかった。

03 / 本文

実際に匿名キーで攻撃してみた

書いたポリシーが意図どおり効いているか、公開する前に自分で試した。 これをやっていなければ気づかなかった。

確認したのは4つだ。

未承認の口コミが読めないか → 読めない(正しい)

承認済みで挿入できないか → できない(正しい)

他人の投稿を書き換えられないか → できない(正しい)

承認関数を呼べないか → 呼べてしまった

投稿したあと、返ってきた id を使って approve_review(id) を叩くと、自分の投稿が公開状態になった。実在の人物についての書き込みを、投稿者が自分で公開できる状態だった。

# 匿名キーで承認関数を呼んでみる
curl -s -X POST "$SUPABASE_URL/rest/v1/rpc/approve_review" \n  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY" \n  -H 'Content-Type: application/json' \n  -d '{"review_id": 1}'
# → 通ってしまった

04 / 本文

なぜ見落としたのか

RLS はテーブルに対する仕組みだ。「テーブルを守る」ことばかり考えていて、関数という別の入り口を見ていなかった。

しかも security definer を付けていた。これは「関数の定義者の権限で実行する」という指定で、RLS を迂回するために必要なものだ。管理者が使う前提だったので付けた。

実行できる人を絞っていないのに、実行時の権限だけ強くしていた。 一番まずい組み合わせだった。

05 / 本文

直したあと、もう一度攻撃して確かめる

REVOKE を書いて適用したあと、同じリクエストをもう一度投げた。

今度は permission denied for function approve_review が返ってきた。塞がったことを、塞がる前と同じ方法で確認する。

セキュリティの修正は、書いただけでは終わらない。効いていることを確かめるまでが対応だ。

06 / 本文

静的サイト+匿名キーの構成で気をつけること

この構成は手軽だが、サーバー側の検証が一切ない。ブラウザに配ったキーで何ができるかが、そのまま攻撃面になる。

匿名キー自体は公開してよい。 守りは RLS と権限設定が担う。だから、その2つを実際に試して確かめる必要がある。

確認するのは次の4点だ。

読めてはいけないものが読めないか

書けてはいけない状態で書けないか

書き換えや削除ができないか

関数(RPC)が呼べないか

4つ目を忘れやすい。私は忘れた。

07 / 本文

まとめ

PostgreSQL の関数は、作った時点で PUBLIC に EXECUTE 権限が付くanon から取り消すだけでは塞がらない。PUBLIC から取り消す。

alter default privileges in schema public revoke execute on functions from public; を書いておくと、今後作る関数にも既定で付かなくなる。

そして、RLS は書いたら匿名キーで実際に攻撃して確かめる。 読み・書き・更新・削除に加えて、関数を呼べないかまで試す。

公開前に試したから間に合った。そのまま出していたら、実在の人物についての未確認の書き込みが表に出ていた。

コメント

この記事へのコメント

お名前だけで投稿できます。アカウント登録やログインは不要です。 いただいたコメントは確認のうえ公開します。