行レベルセキュリティが「開いたまま」になる 5 つのケース

行レベルセキュリティは、WHERE 句の書き忘れに対する最良の防御ですが、気づかないうちに適用されなくなることがあります。PostgreSQL 17 で RLS が開いたままになる 5 つのケースを、再現手順、対処法、CI で検出するカタログクエリとともに紹介します。

Zhan Lu約 16 分で読めます
目次

行レベルセキュリティ(RLS)は、WHERE user_id = … の書き忘れに対して Postgres が用意している最良の防御です。しかし、気づかないうちに無効になりやすい機能でもあります。1 つの属性を持つロール、間違ったユーザーが作成したビュー、NULL ではなく空文字列として返ってくる設定などが原因です。この記事では、RLS が静かにデータを守らなくなる 5 つのケースを取り上げます。それぞれに、psql に貼り付けて試せる再現手順と対処法を付け、最後に、その多くを CI で検出できる 1 本のカタログクエリを紹介します。すべてのスニペットは、Relyt One と同じメジャーバージョンである PostgreSQL 17.11 で実行しました。

「Relyt One マネージド認証 入門」に沿って進めた方は、ここでテストするパターンをすでに使っています。アプリは各トランザクションの開始時に、検証済みのトークンから app.user_id を設定し、ポリシーが各行の所有者とその値を比較します。このパターン自体は健全です。以下の失敗は Postgres のバグではありません。パターンが静かに適用されなくなる場所であり、どれも同じ形で失敗します。エラーもログ行もなく、見るべきでない人に行が渡ってしまうのです。

1 つのポリシーが適用されなくなる 5 つのケース:ゲートを素通りする(RLS をバイパスするロール、FORCE のないオーナー、ビューと SECURITY DEFINER 関数)、または誤った行を通してしまう(空または古い設定、より広いポリシーや RLS のないテーブル)

準備

以下はすべて、スーパーユーザーとして 1 つの psql セッションで実行し、SET ROLE で ID を切り替えます。オブジェクト(テーブル、ポリシー、ビュー)を作成・変更する文は demo_owner かスーパーユーザーで実行するもので、アプリケーションとして動作させる文は SET ROLE demo_app から始まります。実際のプロジェクトにあるものを、3 つのロールで代用します。

  • demo_owner はテーブルを所有し、マイグレーションを実行します。
  • demo_app はアプリケーションが接続するロールで、NOBYPASSRLS です。
  • demo_admin は BYPASSRLS を持ちます。Relyt One では、すべてのプロジェクトに最初からあるオーナーロールの relytdb_owner がこれに当たります。
CREATE ROLE demo_owner NOLOGIN;
CREATE ROLE demo_app   NOLOGIN;
CREATE ROLE demo_admin NOLOGIN BYPASSRLS;
CREATE SCHEMA demo AUTHORIZATION demo_owner;
GRANT USAGE ON SCHEMA demo TO demo_app;
GRANT USAGE, CREATE ON SCHEMA demo TO demo_admin;

テーブルは 1 つ、ポリシーは 1 つ、行は 4 行です。最後の行は所有者が空です。理由はセクション 4 でわかります。

SET ROLE demo_owner;
CREATE TABLE demo.notes (id serial PRIMARY KEY, user_id text NOT NULL, body text NOT NULL);
INSERT INTO demo.notes (user_id, body)
  VALUES ('alice', 'a1'), ('alice', 'a2'), ('bob', 'b1'), ('', 'orphan');

ALTER TABLE demo.notes ENABLE ROW LEVEL SECURITY;
ALTER TABLE demo.notes FORCE ROW LEVEL SECURITY;
CREATE POLICY own ON demo.notes FOR ALL
  USING      (user_id = current_setting('app.user_id', true))
  WITH CHECK (user_id = current_setting('app.user_id', true));
RESET ROLE;

GRANT SELECT, INSERT, UPDATE, DELETE ON demo.notes TO demo_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA demo TO demo_app;
GRANT SELECT ON demo.notes TO demo_admin;

アプリケーションが送る通常のリクエストは次のとおりです。

SET ROLE demo_app;
BEGIN;
SELECT set_config('app.user_id', 'alice', true);  -- true: このトランザクション内だけ有効
SELECT body FROM demo.notes;                      -- a1, a2
COMMIT;

Alice には自分の 2 行が見えます。では、これを壊してみましょう。

1. ロールが RLS をまるごとスキップする

PostgreSQL のドキュメントは明快です。「スーパーユーザー、および BYPASSRLS 属性を持つロールは、テーブルにアクセスするとき、常に行セキュリティシステムをバイパスします。」(PostgreSQL のドキュメントは英語です。この記事の引用は日本語に訳しています。)この場合、ポリシーは一切参照されません。

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes;                       -- 2   (ポリシーが適用される)
COMMIT;
SET ROLE demo_admin; SELECT count(*) FROM demo.notes;  -- 4   (BYPASSRLS: 全行)
RESET ROLE;          SELECT count(*) FROM demo.notes;  -- 4   (スーパーユーザー: 全行)

この属性はロールそのものに付くもので、メンバーには引き継がれません。demo_app が demo_admin のメンバーであっても、どこかのコードが SET ROLE demo_admin を実行するまでは、引き続きフィルタリングされます。

GRANT demo_admin TO demo_app;
SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes;                       -- 2   (メンバーであるだけではバイパスしない)
COMMIT;
SET ROLE demo_admin; SELECT count(*) FROM demo.notes;  -- 4

Relyt One では、この点は思っている以上に重要です。コンソールから作成したデータベースユーザーは BYPASSRLS を持ち、relytdb_owner も同様です。実際のブランチで確認したところ、FORCE を設定して正しいポリシーを付けたテーブルでも、relytdb_owner としてクエリするとすべての行が返りました。これらのロールのいずれかで接続するアプリには、行レベルセキュリティがまったく効きません。SQL で専用のロールを作成し、そのロールで接続してください。方法はマネージド認証のガイドで紹介しています。また、そのようなロールは、直接接続の接続文字列でも、プールされた接続の接続文字列でもログインできることを確認済みです。

テスト: アプリケーションが接続するロールとして、次のクエリを実行します。結果は false でなければなりません。

SET ROLE demo_app;
SELECT rolsuper OR rolbypassrls AS bypasses_rls FROM pg_roles WHERE rolname = current_user;  -- f

2. FORCE のないオーナー

デフォルトでは、「テーブルのオーナーも通常は行セキュリティをバイパスしますが、ALTER TABLE ... FORCE ROW LEVEL SECURITY を使うと、テーブルのオーナーを行セキュリティの対象にできます。」ENABLE だけでは、テーブルを所有するロール以外の全員が保護されます。

SET ROLE demo_owner;
CREATE TABLE demo.notes2 (id serial PRIMARY KEY, user_id text NOT NULL, body text NOT NULL);
INSERT INTO demo.notes2 (user_id, body) VALUES ('alice', 'x'), ('bob', 'y');
ALTER TABLE demo.notes2 ENABLE ROW LEVEL SECURITY;          -- 有効化のみ、強制はしない
CREATE POLICY own ON demo.notes2 FOR ALL
  USING      (user_id = current_setting('app.user_id', true))
  WITH CHECK (user_id = current_setting('app.user_id', true));

SELECT count(*) FROM demo.notes2;   -- 2  (alice の行と bob の行: ポリシーがスキップされた)

ALTER TABLE demo.notes2 FORCE ROW LEVEL SECURITY;
SELECT count(*) FROM demo.notes2;   -- 0  (オーナーもポリシーの対象になった)
RESET ROLE;

マイグレーションツールとアプリケーションが 1 つのロールを共有していると、そのロールがオーナーなので、アプリケーションは RLS をバイパスします。ユーザーデータを持つすべてのテーブルに FORCE を使い、オーナーをリクエストの経路に入れないでください。Relyt One ではオーナーが BYPASSRLS を持つため FORCE は効きません。その場合の対処はセクション 1 のとおりです。

3. 別の誰かとして動くコード

ビューは、そのオーナーの権限で動作します。CREATE VIEW のドキュメントには、「デフォルトでは、ビューが参照する基底リレーションへのアクセスは、ビューのオーナーの権限によって決まる」とあり、行レベルセキュリティのポリシーも同様です。SECURITY DEFINER 関数は、関数のオーナーの ID で同じことを行います。どちらも便利ですが、オーナーが RLS をバイパスする場合、RLS が防ぐはずだったことをまさに引き起こします。

もう一度 notes2 を使います。「有効化のみ、強制なし」の状態に戻し、その上にビューと definer 関数を作成します。どちらも demo_owner が所有し、alice の行を要求します。

ALTER TABLE demo.notes2 NO FORCE ROW LEVEL SECURITY;
GRANT SELECT ON demo.notes2 TO demo_app, demo_admin;

SET ROLE demo_owner;
CREATE VIEW demo.v_notes2 AS SELECT * FROM demo.notes2;
CREATE FUNCTION demo.count_notes2() RETURNS bigint
  LANGUAGE sql SECURITY DEFINER SET search_path = demo, pg_temp
  AS $$ SELECT count(*) FROM demo.notes2 $$;
RESET ROLE;
GRANT SELECT ON demo.v_notes2 TO demo_app;
GRANT EXECUTE ON FUNCTION demo.count_notes2() TO demo_app;

SET ROLE demo_app;
BEGIN;
SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes2;     -- 1
SELECT count(*) FROM demo.v_notes2;   -- 2   漏洩
SELECT demo.count_notes2();           -- 2   漏洩
COMMIT;

テーブルに FORCE を追加すると、オーナーもポリシーの対象になるため、どちらの穴もふさがります。ただし、オーナー自身が RLS をバイパスする場合、FORCE は何もしません。demo_admin(relytdb_owner の代役です)でビューを作成すると、強制済みのテーブルでも漏洩します。

alice のクエリが実行される場所alice に見える行数
テーブルに直接1
テーブルのオーナーが所有するビュー経由(テーブルは強制なし)2
SECURITY DEFINER 関数経由(テーブルは強制なし)2
FORCE 後の同じビューと関数1
BYPASSRLS ロールが所有するビュー経由、テーブルは強制済み2
同じビューに security_invoker = true を付けた場合1

最後の 2 行を確認するには、demo_admin でビューを作成し、テーブルを強制します。

RESET ROLE;
ALTER TABLE demo.notes2 FORCE ROW LEVEL SECURITY;
SET ROLE demo_admin;
CREATE VIEW demo.v_admin AS SELECT * FROM demo.notes2;     -- BYPASSRLS ロールが所有
RESET ROLE;
GRANT SELECT ON demo.v_admin TO demo_app;

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.v_admin;    -- 2   漏洩(テーブルは強制済みなのに)
COMMIT;

本当の対処法は、PostgreSQL 15 で追加された security_invoker です(「ビューによるテーブルアクセスを、オプションでビューの呼び出し元の権限で制御できるようにする」)。これを使うと、クエリを実行しているユーザーのポリシーが適用されます。

CREATE VIEW demo.v_safe WITH (security_invoker = true) AS SELECT * FROM demo.notes2;
ALTER VIEW demo.v_admin SET (security_invoker = true);   -- 既存のビューを修正する。alice に見えるのは 1 行になる

ユーザーデータを持つテーブルに対する SECURITY DEFINER 関数は、できれば持たないでください。どうしても必要な場合は、ユーザーを引数として渡して関数内で明示的にそれで絞り込み、上の例のように必ず search_path を固定してください。

4. 思っているものと違う設定

このパターンは current_setting('app.user_id', true) に依存しています。第 2 引数は「設定が存在しない場合、エラーを出さずに NULL を返す」という意味で、新しいセッションではそのとおりの値が返ります。その後、トランザクションが set_config(…, true) で値を設定して終了します。次のクエリには何が見えるでしょうか。

上のリクエストですでに設定が定義された状態になっているので、まず新しいセッションを開始します。

-- psql: 再接続して新しいセッションにする
\c
SET ROLE demo_app;
SELECT coalesce(current_setting('app.user_id', true), '<NULL>');       -- <NULL>

BEGIN; SELECT set_config('app.user_id', 'alice', true); COMMIT;

SELECT '[' || coalesce(current_setting('app.user_id', true), '<NULL>') || ']';  -- []
SELECT user_id || '/' || body FROM demo.notes;                         -- /orphan

NULL ではなく、空文字列です。PostgreSQL 15、16、17 で同じ結果を確認しました。トランザクションの後も、設定は空の値のまま定義された状態で残ります。これはドキュメントに書かれた動作ではないため、お使いのバージョンで確認してください。user_id = '' と比較するポリシーは、所有者が空文字列であるすべての行に一致します。今回のテーブルにはそのような行が 1 行あります。この種の行は、バグやマイグレーションから生まれます。検証をすり抜けた空の sub や、誰も意図していないデフォルト値などです。最も公開したくない行だと言えます。

このセクションのもう半分は、第 3 引数のミスです。false にすると設定はセッション全体で有効になり、コネクションプールを使っている場合は、その接続の次のリクエストが設定を引き継ぎます。

SET ROLE demo_app;
SELECT set_config('app.user_id', 'alice', false);   -- セッション全体で有効
SELECT count(*) FROM demo.notes;                    -- 2。この接続を次に使う誰に対しても

対処法は、ポリシーが空の設定を拒否するようにすることと、空の所有者が存在できないようにすることです。

RESET ROLE;
ALTER POLICY own ON demo.notes
  USING      (nullif(current_setting('app.user_id', true), '') IS NOT NULL
              AND user_id = current_setting('app.user_id', true))
  WITH CHECK (nullif(current_setting('app.user_id', true), '') IS NOT NULL
              AND user_id = current_setting('app.user_id', true));

ALTER TABLE demo.notes ADD CONSTRAINT user_id_not_empty CHECK (user_id <> '') NOT VALID;

新しいセッションで確認します。

-- psql: 再接続して新しいセッションにする
\c
SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true); COMMIT;
SELECT count(*) FROM demo.notes;                         -- 0   (空の設定はもう何にも一致しない)
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes;                         -- 2   (alice の通常のリクエストは引き続き動作する)
COMMIT;

(NOT VALID を付けると、古い空の行が残っていても制約を追加できます。行を修正してから VALIDATE CONSTRAINT を実行してください。)データアクセス用の関数が呼び出す set_config(…, true) の形を、唯一の形にしてください。

5. より広いポリシー、またはポリシーがない

ポリシーは加算されます。「1 つのクエリに複数のポリシーが適用される場合、それらは OR(デフォルトである permissive ポリシーの場合)または AND(restrictive ポリシーの場合)で結合されます。」レポートやサポートツールのために permissive ポリシーを 1 つ追加しただけで、全員のアクセスが広がります。

RESET ROLE;
CREATE POLICY pub ON demo.notes FOR SELECT USING (true);

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes;    -- 4, alice に全行が見える
COMMIT;

RESTRICTIVE ポリシーは AND で結合されるため、あとから permissive ポリシーが追加されても、その下に最低限の防御線を残せます。

RESET ROLE;
CREATE POLICY tenant_guard ON demo.notes AS RESTRICTIVE FOR SELECT
  USING (nullif(current_setting('app.user_id', true), '') IS NOT NULL
         AND user_id = current_setting('app.user_id', true));

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes;    -- `pub` があっても再び 2
COMMIT;

新しいテーブルは RLS なしで始まります。 アプリケーションのロールにデフォルト権限も付与している場合、最初に作って忘れてしまったテーブルが全員に読み取り可能になります。

RESET ROLE;
ALTER DEFAULT PRIVILEGES FOR ROLE demo_owner IN SCHEMA demo GRANT SELECT ON TABLES TO demo_app;

SET ROLE demo_owner;
CREATE TABLE demo.notes_new (user_id text NOT NULL, body text NOT NULL);   -- 誰も ENABLE ROW LEVEL SECURITY を実行しない
INSERT INTO demo.notes_new VALUES ('alice', 'n1'), ('bob', 'n2');
RESET ROLE;

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes_new;    -- 2, alice に bob の行も見える
COMMIT;

逆は安全です。RLS が有効でポリシーがないテーブルは「デフォルト拒否」で、見える行も更新できる行もありません。

穴ではない 2 つのこと

  • USING だけのポリシー。 ALL と UPDATE のポリシーでは、「WITH CHECK 式が定義されていない場合、USING 式が」新しい行にも使われます。INSERT ポリシーには USING 句をそもそも指定できません。実際の Relyt One ブランチで、ユーザーが他人所有の行を書き込もうとするテストを行ったところ、ポリシーによって拒否されました。
  • ポリシーのないコマンド。 RLS が有効で適用可能なポリシーがない場合、ドキュメントによると「デフォルト拒否」ポリシーが前提となります。FOR SELECT のポリシーしかないテーブルでは、挿入は拒否されます。

RLS が決してカバーしない 2 つのこと

一意制約。「一意制約や主キー制約、外部キー参照といった参照整合性のチェックは、データの整合性を維持するため、常に行セキュリティをバイパスします。」ドキュメントは、この結果を「隠れチャネル(covert channel)」と呼んでいます。Bob は alice の slug を読めませんが、それが存在することは知ることができます。

SET ROLE demo_owner;
CREATE TABLE demo.slugs (id serial PRIMARY KEY, user_id text NOT NULL, slug text NOT NULL UNIQUE);
INSERT INTO demo.slugs (user_id, slug) VALUES ('alice', 'secret-project');
ALTER TABLE demo.slugs ENABLE ROW LEVEL SECURITY;
ALTER TABLE demo.slugs FORCE ROW LEVEL SECURITY;
CREATE POLICY own ON demo.slugs FOR ALL
  USING      (user_id = current_setting('app.user_id', true))
  WITH CHECK (user_id = current_setting('app.user_id', true));
RESET ROLE;
GRANT SELECT, INSERT ON demo.slugs TO demo_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA demo TO demo_app;

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'bob', true);
SELECT count(*) FROM demo.slugs WHERE slug = 'secret-project';              -- 0
INSERT INTO demo.slugs (user_id, slug) VALUES ('bob', 'secret-project');
-- ERROR:  duplicate key value violates unique constraint "slugs_slug_key"
ROLLBACK;

一意性をテナントごとにすれば、この漏洩はなくなり、bob の挿入は成功します。値をすべてのユーザーにわたって一意にする必要がある場合は、その値が存在すること自体が公開情報になると受け入れてください。

RESET ROLE;
ALTER TABLE demo.slugs DROP CONSTRAINT slugs_slug_key, ADD UNIQUE (user_id, slug);

マテリアライズドビュー。 ドキュメントには記載がないので、ご自身の環境でテストしてください。マテリアライズドビューは、誰かがリフレッシュした時点の行を保存しており、保存された行を読むときにテーブルのポリシーは実行されません。今回のテストでは、alice の設定が有効な間に作成したビューが、bob に alice の 2 行を見せました。

SET ROLE demo_owner;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
CREATE MATERIALIZED VIEW demo.m_notes AS SELECT * FROM demo.notes;   -- alice の行を保存する
COMMIT;
RESET ROLE;
GRANT SELECT ON demo.m_notes TO demo_app;

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'bob', true);
SELECT count(*) FROM demo.m_notes;    -- 2   bob が alice の行を読む
COMMIT;

アプリケーションのロールに、ユーザーデータを持つマテリアライズドビューへのアクセスを付与しないでください。ビューにテナント列を含めてクエリでそれを使って絞り込むか、基底テーブルを読んでください。

ドキュメントからもう 1 点です。ポリシー式は、各行に対して「ユーザーのクエリに含まれる条件や関数よりも前に」評価されますが、例外が 1 つあります。「情報を漏らさないことが保証されている leakproof 関数は、オプティマイザーが行セキュリティのチェックより前に適用することがあります。」LEAKPROOF の指定は、守り続けられなければならない約束として扱ってください。

CI で実行できるセルフ監査

ここまでの項目の多くは、カタログに痕跡を残します。次のクエリは、検出した問題ごとに 1 行を返します。対象は public スキーマです(他のスキーマの場合は名前を変えてください)。アプリケーションが接続するロールとして実行し、何か返ってきたらビルドを失敗させます。

-- rls_audit.sql: あるユーザーの行が別のユーザーに届きうる原因(スキーマ "public")
WITH tbl AS (
  SELECT c.oid, c.relname, c.relrowsecurity, c.relforcerowsecurity
  FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
  WHERE n.nspname = 'public' AND c.relkind IN ('r', 'p')
)
SELECT 'connected role bypasses RLS' AS problem, current_user::text AS object
  FROM pg_roles WHERE rolname = current_user AND (rolsuper OR rolbypassrls)
UNION ALL
SELECT 'table without RLS', relname FROM tbl WHERE NOT relrowsecurity
UNION ALL
SELECT 'RLS enabled but not forced', relname FROM tbl WHERE relrowsecurity AND NOT relforcerowsecurity
UNION ALL
SELECT 'view without security_invoker', c.relname
  FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
  WHERE n.nspname = 'public' AND c.relkind = 'v'
    AND NOT EXISTS (SELECT 1 FROM unnest(coalesce(c.reloptions, '{}')) o
                    WHERE o ~* '^security_invoker=(true|t|yes|y|on|1)$')
UNION ALL
SELECT 'materialized view or foreign table (RLS does not apply)', c.relname
  FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
  WHERE n.nspname = 'public' AND c.relkind IN ('m', 'f')
UNION ALL
SELECT 'SECURITY DEFINER function', p.proname
  FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
  WHERE n.nspname = 'public' AND p.prosecdef
UNION ALL
SELECT 'permissive policy that allows every row', p.polname
  FROM pg_policy p
  WHERE p.polrelid IN (SELECT oid FROM tbl) AND p.polpermissive AND pg_get_expr(p.polqual, p.polrelid) = 'true'
ORDER BY 1, 2;
set -euo pipefail
out=$(psql "$DATABASE_URL" -X -A -t -v ON_ERROR_STOP=1 -f rls_audit.sql)   # 接続エラーや SQL エラーでビルドが失敗します
if [ -n "$out" ]; then echo "$out"; exit 1; fi

このクエリを、上記の問題をすべて含むように作ったスキーマに対して実行したところ、すべてを報告しました(ロール、RLS のない新しいテーブル、強制されていないテーブル、通常のビュー、definer 関数、マテリアライズドビュー、外部テーブル、すべてを許可するポリシー)。security_invoker がどの表記でも有効になっているビューは無視し、スキーマを修正すると何も返さなくなりました。一方で、設定が空かどうかや、ポリシーが組み合わさって意図したより広い条件になっていないかは検出できません(検出できるのは、文字どおり true であるポリシーだけです)。それらには、ユーザーとしてリクエストを実行して行数を数えるテストが必要です。TODO サンプルにあるようなテストです。ルックアップテーブルなど、正当な理由でユーザーデータを持たないテーブルは「table without RLS」に表示されるので、それらの許可リストを用意してください。

まとめ

  • アプリケーションは NOBYPASSRLS でスーパーユーザーではないロールで接続し、監査の最初のチェックは空になる。Relyt One では、それが relytdb_owner やコンソールで作成したユーザーになることはない。
  • ユーザーデータを持つすべてのテーブルに ENABLE と FORCE ROW LEVEL SECURITY が設定されており、オーナーはリクエストの経路に入らない。
  • それらのテーブルに対するビューは security_invoker = true を使い、ユーザー向けの SECURITY DEFINER 関数は避けるか、明示的に絞り込む。
  • ポリシーは空の設定を拒否し、CHECK (user_id <> '') がそれを裏付ける。set_config は常に true でのみ呼び出す。
  • RESTRICTIVE ポリシーが permissive ポリシーの下に置かれ、監査が CI で実行されるので、RLS のない新しいテーブルが出荷されることはない。
  • ユーザーデータに対する一意制約にはテナント列が含まれ、アプリケーションのロールには、ユーザーデータを持つマテリアライズドビューへのアクセスを付与しない。

行レベルセキュリティは、他のアクセス制御のコードと同じようにテストして初めて報われます。ユーザーの種類ごとにログインし、クエリの種類ごとに実行して、行数を数えてください。マネージド認証の上に構築している場合は、ユーザー ID がデータベースに届く仕組みをプレビュー版の記事で解説しています。テストは TODO サンプルにあります。

無料でプロジェクトを作成

タグPostgresセキュリティrls

執筆者

Zhan Lu

次のデータベースは、ブランチひとつ先に。

1 分以内に無料プロジェクトを作成。クレジットカードもセットアップも不要です。

ブログに戻る