セキュリティ監査で、未認証のまま呼べるRPCがいくつも見つかりました。そこで権限を一括で締めたところ、今度は本番の主要データが読めなくなりました。安全にしようとして、自分でサービスを止めたときの記録です。Supabaseの権限変更で何を見落とし、どう復旧し、どの確認を運用へ残したかを書きます。 回帰確認は通っていました。それでも見逃した理由と、権限を触る前に作るようになった確認表の中身まで並べます。
きっかけは、必要なセキュリティ改善だった
Supabaseのデータベース関数は、作り方と権限設定によっては、明示的に閉じない限り広い実行権限を持ちます。監査では、ログインしていない状態から呼べるRPCや、内部だけで使うつもりだった関数が外部から実行できる状態を確認しました。
そこでpublic、anon、authenticatedから一度EXECUTE権限を剥がし、クライアントが本当に使う関数だけ戻す方針にしました。不要な入口を閉じ、許可した経路だけを残す考え方自体は間違っていません。
問題は「外から呼ぶ関数」と「データベース内部の判定で使う関数」を同じ一覧として扱ったことでした。名前や置き場所が似ていても、呼び出される主体と必要な権限は違います。変更対象を整理しないまま一括で閉じたことで、守るための仕組みまで動かなくしました。
権限を閉じた直後、主要テーブルが読めなくなった
適用後、募集、参加状態、メッセージ、ライブ位置など、アプリの主要データが読めなくなりました。ログインはできても、一覧や詳細を開くための読み取りが失敗します。利用者から見れば、アプリの中心が突然空になった状態です。
原因は、RLSポリシーの中から呼んでいた補助関数まで同じように権限を剥がしたことでした。SECURITY DEFINER関数の本体が定義者権限で動くことと、ポリシーの式を評価する経路で必要な権限がすべて満たされることを、同じものとして扱っていました。
RLSはデータを守る最後の境界です。その判定に必要な補助関数が実行できなければ、許可されるべき利用者まで拒否されます。権限を狭めた結果、攻撃者だけでなく正規の利用者も閉め出してしまいました。
テストが通ったのに、なぜ気づけなかったのか
変更後には回帰確認をしていました。それでも障害を見逃しました。確認していたのがRPC経由の読み取りに偏っていたからです。変更した関数が意図どおり拒否されることと、許可したRPCが動くことは確認していました。
一方、実際のFlutterアプリには、テーブルを直接読む経路もあります。RPC側が動いても、直接読み取りでRLSの補助関数を評価できなければ画面は成立しません。変更した部品のテストはしていた。でも、利用者が通る経路を全部は試していなかった。 それが本質的な失敗でした。
- RPC経由の読み取りだけで終わらせない
- クライアントが直接読むテーブルも確認する
- 未認証・一般利用者・所有者など役割を分けて試す
- 権限変更後は、画面単位の主要導線を通す
テストの件数ではなく、実際の経路を覆っているかが重要です。同じデータを取得する機能でも、RPC、直接クエリ、Storage、リアルタイム購読では通る権限の境界が違います。正常系をひとつ確認して全体が動くと考えないようにしました。
まず復旧し、そのあと安全な範囲を作り直した
障害時に最初にしたのは、変更を全部元へ戻すことではありませんでした。どの画面がどのテーブルを読み、どのRLSポリシーがどの補助関数を呼ぶかを追い、正規利用に必要な実行権限だけを特定しました。
復旧用の変更では、RLSから必要な補助関数の権限だけを戻し、外部から不要な関数は閉じたままにしました。広く開け直せば早く復旧できても、監査で見つかった入口まで復活します。障害対応中ほど、変更範囲を小さくし、戻す理由を説明できる状態にする必要があります。
復旧後はアプリの画面だけでなく、未認証のHTTP要求、ログイン済み利用者の要求、所有者ではないデータへの要求も確認しました。「使えるようになった」だけでなく、「閉じるべき経路は閉じたままか」を同時に確かめて、復旧完了としました。
監査で見つかった問題は、それでも直す必要があった
障害を起こしたからといって、元の権限へ戻して終わりにはできません。監査では、未認証で任意の通知処理へ到達できる経路、退会後もStorageにファイルが残る問題、年齢確認を回避できる条件なども見つかっていました。
これらは同じ「セキュリティ修正」に見えても、原因も確認方法も異なります。RPCの権限、RLS、Storageの削除、アプリ側の入力制御をひとつの大きな変更へまとめると、失敗時に切り分けられません。その後は境界ごとに変更し、ひとつずつ確認するようにしました。
セキュリティ監査は問題の一覧を作るだけでは足りません。各項目について、誰から何を守るのか、どの正規経路は残すのか、確認に使う要求は何かまで決めて、初めて実装へ安全に落とせます。
権限を変更する前に作るようになった確認表
今は、権限を触る前に利用経路を短い表へします。対象データ、呼び出す主体、経路、期待する結果を並べるだけでも、一括変更で巻き込む範囲が見えます。設定値の差分だけを眺めるより、利用者の操作と結びつけるほうが漏れを見つけやすくなります。
- 未認証:公開情報だけを読めるか
- ログイン済み:自分に許可された情報を読めるか
- 非所有者:他人だけの情報を読めないか
- サーバー処理:管理用の経路だけが必要な権限を持つか
- 削除後:DBとStorageの両方から不要データが消えるか
この確認表は自動テストの代わりではありません。どの経路を自動化し、どこを実機で見るかを決める地図です。権限設定を変えるたびに同じ観点で確認できるよう、実装記録と一緒に残しています。
変更を適用する順番も見直しました。まず影響範囲を限定できる環境で権限を変え、期待する拒否と許可を確認し、その後にアプリの主要画面を通します。本番へ反映するときは、戻す変更と確認方法を先に用意します。緊急時に考える項目を減らしておくことで、復旧を急ぐあまり広い権限を戻す判断を避けやすくなります。
位置情報を扱うアプリでは、表示側の配慮も境界になる
データベースの権限が正しくても、取得した情報を画面で出しすぎれば安全とは言えません。スグイコでは、位置情報を扱うため、誰が読めるかだけでなく、どの精度で見せるかも設計の一部です。
権限と表示は別の層ですが、利用者から見ればどちらもプライバシーの約束です。位置情報アプリなのに、正確な現在地を見せないことにした理由では、実際の位置をそのまま見せない判断をまとめています。また、失敗を次の確認項目へ変える方法は個人開発で失敗を記録する六項目。却下と障害から残した形式でも整理しました。
バックエンドで守る範囲と、画面で見せる範囲を別々に定義し、両方を満たして初めて安全と考えます。片方だけを強くしても、プロダクト全体の安心にはなりません。
セキュリティ変更ほど、機能テストが必要になる
権限、RLS、Storage、認証は、画面から見ると裏側の変更です。しかし、壊れたときに消えるのは画面そのものです。利用者は「RLSの補助関数に権限がない」とは見えず、募集がない、メッセージが開けない、位置が更新されないと感じます。
それ以来、権限を触ったときは「設定が意図どおりか」だけではなく、利用者が実際に通る経路がまだ成立しているかまでセットで確認するようにしました。セキュリティのテストと機能のテストを別の日にしないことも重要でした。
セキュリティを強くする変更だからこそ、「閉じるべき入口が閉じた」ことと「正規の利用者が使えるまま」なことの両方を証明しないと完成ではありません。今回の障害で残った一番大きな教訓です。
今回は権限を狭める変更で止めましたが、逆方向の事故もあります。復旧のために一時的に広げた権限を戻し忘れれば、画面は動いていても守りは弱いままです。だから復旧確認には、正常に読めることだけでなく、読めてはいけない条件で拒否されることも必ず含めます。可用性と安全性を同じチェックの中で扱うようになりました。



