Supabase の RLS(行単位のアクセス制御)を書けば守れる、と思っていました。書いたあとに、RLS では塞がらない穴が 3 つあると分かりました。
やったこと(3 ステップ)
- RLS の位置づけを「主」から「保険」に書き換える
- 権限に関わる列と、RLS を回避する関数に、別の守りを足す
- 「ポリシーを書いた」で終わらせず、効いているかを測る
判断:RLS は「主」ではなく「保険」にする
サーバー側の読み書きは、管理者権限の鍵(service_role)で接続しています。この鍵は RLS を迂回します。
迂回するのは設計どおりにしました。理由は、判定を 1 か所に集めたいからです。たとえば「この非公開投稿を見てよいか」はアルバムのメンバーかどうかで決まり、SQL の条件だけでは書き切れません。2 か所に同じ判定を書けば、いつか片方だけ古くなります。
そのかわり、RLS の役目を言い直しました。
主はサーバー側の判定。RLS は「ブラウザから直接 DB を触られたとき」の保険。
ブラウザが直接触るのはログインとファイルのアップロードだけで、そこは RLS が効きます。「二重に守る」は同じ判定を 2 回することではなく、経路ごとに守りを置くことでした。
工夫:2 つの鍵で行数を比べる
「効いているか」はこう測りました。ブラウザに配られる鍵で、Supabase が自動で公開している API を画面を通さずに直接叩く。アプリ越しではないので、直接触られたときと同じ条件になります。
要点は、同じ問い合わせを 2 つの鍵で投げて行数を比べることです。配布される鍵で 0 行でも、それだけでは「隠した」のか「そもそも空」なのか分かりません。管理者権限の鍵で行があると確かめて、初めて「隠した」と言えます。
| 対象 | 結果 |
|---|---|
| テーブル 30 件 | 未ログインで読めた行は 0 |
| └ うち 24 件 | データがあるのに 1 行も見えない(= RLS が隠している) |
| 関数 11 件 | 未ログインで呼べる関数は 0 |
気をつけたこと:RLS は行しか守れない
RLS は「どの行を見せるか」の仕組みで、「どの列を書き換えてよいか」は決められません。
放っておくと、利用者が自分の行の管理者フラグを自分で立てられます。行の持ち主は本人なので、RLS は通してしまうのです。そこで列単位の権限で、本人が書き換えてよい列を表示名とアイコンだけに絞りました。
気をつけたこと:関数は既定で誰でも呼べる
RLS を回避する目的の関数(security definer)には、もう 1 つ落とし穴があります。PostgreSQL は新しい関数に既定で全員の実行権限を与え、Supabase は公開 API から呼べるようにします。
つまり絞らないと、ブラウザに配る鍵だけで誰でも好きな引数で呼べます。作ったら必ず実行権限をサーバー側だけに絞る、を手順書に入れました。
決めた決まり
- RLS は保険。主はサーバー側の判定。 どちらが主かを文書に書く
- 書き換えてよい列は、列単位の権限で絞る。
security definerの関数を足したら、実行権限を絞る。- 「書いた」で終わらせず、2 つの鍵で比べて確かめる。
次回
Google Maps の無料枠に収めるために決めたこと。