RLSを有効にしても、TRUNCATE権限が残っていれば安全ではなかった
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
PostgreSQLのTRUNCATEはRLSポリシーの対象外の操作であるため、RLSを有効にしポリシーを1つも作らなくても、anon/authenticatedにTRUNCATE権限が残っていればテーブルを丸ごと空にできる。
結論
RLS(Row Level Security)という、行ごとのアクセスをポリシーで制限する仕組みを有効にし、ポリシーを1つも作らずに anon / authenticated を締め出したつもりでも、テーブルに TRUNCATE 権限が残っていればテーブルを丸ごと空にできる。TRUNCATE はRLSポリシーの対象外の操作だからで、Supabase の既定権限は明示的に外さない限り TRUNCATE まで含んだ状態で付いてくる。
症状
チャットボットの知識ベース(chat_knowledge)というテーブルがある。中身はそのまま LLM の system プロンプトに入るため、書き込めた人間がボットの発言内容を決められる。設計時点の意図はテーブルのコメントにそのまま書かれている。
-- チャットボットの知識ベース(RAG)
--
-- ⚠ この表の中身は、そのまま LLM の system プロンプトに入る。
-- 書き込めた人がボットの発言内容を決められるということ。RLS を有効にしたうえで
-- ポリシーを1つも作らない= anon / authenticated からは読めも書けもしない。
-- 触れるのは service role(サーバー側の API ルート)だけ。
ENABLE ROW LEVEL SECURITY を実行し、CREATE POLICY は1つも書かない。RLS はデフォルト拒否なので、ポリシーがゼロならどのロールも SELECT も INSERT も通らない、というのが設計の建て付けだった。ところがローカルに Supabase を立てて実データで検証していたところ、anon / authenticated に TRUNCATE 権限が付いたままになっていることに気づいた。RLS ポリシーは0件のままである。
原因
PostgreSQL の RLS ポリシーが対象にしているのは SELECT / INSERT / UPDATE / DELETE という行単位の操作で、TRUNCATE はそもそも対象に含まれない。テーブルを丸ごと空にする TRUNCATE はテーブル単位の操作であり、行ごとの可視性・書き込み可否を判定する RLS の仕組みそのものが関与しない。判定するのは「そのロールに TRUNCATE 権限があるかどうか」だけで、これは RLS とは独立した通常のテーブル権限(GRANT/REVOKE)の話になる。
そしてこのテーブルは、TRUNCATE 権限を明示的に付与した覚えが無いにもかかわらず、anon / authenticated に付いていた。Supabase はテーブルを作成すると既定でロールに広い権限(GRANT ALL 相当)を与える形になっており、何も指定しなければ TRUNCATE もその中に含まれる。「RLS を有効にしてポリシーをゼロにしたから安全」という設計上の建て付けは、RLS が及ばない TRUNCATE という操作の存在を計算に入れていなかった。
直す
anon / authenticated からはテーブルへの権限を全て剥がし、実際にアクセスする service_role にだけ必要な権限を明示的に付け直した。
-- ⚠ 権限は明示的に与える。Supabase の既定権限(ALTER DEFAULT PRIVILEGES)に頼ると、
-- migration をどのロールで流したかによって付いたり付かなかったりする。
-- 2026-08-16 にローカル Supabase で確認: CLI から流すと service_role に権限が付かず、
-- チャットが permission denied for table chat_knowledge で 502 になった。
-- ⚠ anon / authenticated からは全部剥がす。Supabase の既定権限は GRANT ALL なので、
-- 放っておくと TRUNCATE まで付く。**TRUNCATE は RLS を迂回する**ので、
-- 「RLS を有効にしたから安全」は成り立たない(2026-08-16 にローカルで確認)。
REVOKE ALL ON public.chat_knowledge FROM anon, authenticated;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.chat_knowledge TO service_role;
REVOKE ALL で TRUNCATE を含む全権限を anon / authenticated から剥がし、service_role には必要な4操作だけを明示的に GRANT する。RLS ポリシーをゼロにするだけでは閉め出せない TRUNCATE の穴は、RLS の設定ではなく通常の権限管理側で塞ぐしかない。
同じコミットでは service_role 側の抜けも一緒に直している。Supabase の既定権限は migration をどのロールで流したかによって挙動が変わり、ローカル検証時は CLI から流したことで service_role に権限が付かず、逆にチャットが permission denied for table chat_knowledge で 502 になっていた。「Supabase の既定に任せる」という前提そのものが、多い方にも少ない方にもぶれる不安定な土台だったため、権限は都度 GRANT / REVOKE で明示する方針に変えた。
再発防止
RLS を有効にしてポリシーを0件にする設計は、SELECT / INSERT / UPDATE / DELETE の4操作には正しく効く。ただしそれで「このテーブルはロックされている」と結論づけるのは早く、TRUNCATE・DROP・ALTER TABLE といったテーブル単位の操作は別枠として権限を確認する必要がある。RLS を入れたテーブルほど「もう安全なはず」という思い込みが働きやすく、通常の GRANT/REVOKE を見直す機会を素通りしやすい。Supabase のように既定権限が広めに付くプラットフォームでは、RLS の有無にかかわらず anon / authenticated に不要な権限が残っていないかをテーブルごとに確認する習慣が要る。
よくある質問
Q1なぜRLSを有効にしてポリシーをゼロにしただけでは不十分なのですか?
RLSのポリシーはSELECT/INSERT/UPDATE/DELETEといった行単位の操作にしか働かず、TRUNCATEには適用されないため。ポリシーが1つも無くてもTRUNCATE権限そのものを持っていればテーブル全体を空にでき、RLSは関与しない。
Q2なぜanon/authenticatedにTRUNCATE権限が付いていたのですか?
明示的に付与した覚えは無かったが、Supabaseの既定権限がテーブル作成時にGRANT ALLを与える形になっており、その中にTRUNCATEも含まれていた。RLSを有効にすれば安全という前提で、権限自体の見直しをしていなかった。
Q3service_roleに明示的にGRANTした理由はTRUNCATEとは別の話ですか?
別の問題。migrationをどのロールで流すかによってSupabaseの既定権限が付いたり付かなかったりし、ローカル検証ではservice_roleに権限が付かずチャットがpermission deniedで502になっていた。権限は既定に頼らず明示的に書く方針にした。
Q4この問題はどうやって見つけたのですか?
ローカルにSupabaseを立てて実際の埋め込みと回答で検証している最中に、権限周りをテーブルごとに洗い直して見つかった。4件の不備のうちの1つで、他は502エラーや検索閾値の緩さなど別種の問題だった。
確認した環境
- Supabase (@supabase/supabase-js ^2.53.0) / ローカル環境
- 2026-08-16 にローカル Supabase の実データ検証で発覚・修正
この記事の根拠
- SQLファイル 1〜9行目コミット 47a8735
- SQLファイル 37〜47行目コミット 47a8735
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。