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' を強制し、UPDATE と DELETE のポリシーは作らなかった。
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 は書いたら匿名キーで実際に攻撃して確かめる。 読み・書き・更新・削除に加えて、関数を呼べないかまで試す。
公開前に試したから間に合った。そのまま出していたら、実在の人物についての未確認の書き込みが表に出ていた。
コメント
この記事へのコメント
お名前だけで投稿できます。アカウント登録やログインは不要です。 いただいたコメントは確認のうえ公開します。