mcpField
P
postgres-rls

postgres-rls

github.com/troykellyセキュリティ12234 回インストール41 週間前に更新
リポジトリ

postgres-rls について

postgres-rlsは、PostgreSQLの行レベルセキュリティのレビューと実装を支援するスキルです。テナント分離、認証関連のデータベースコード、マイグレーションを扱う開発者向けに、迂回リスク、ポリシーの適切さ、性能、テストを確認します。

主な機能

  1. 01スーパーユーザーとテーブル所有者の迂回確認
  2. 02USINGとWITH CHECKポリシーの検証
  3. 03コネクションプールでのテナント情報漏れ防止
  4. 04ビューとマテリアライズドビューのリスク確認
  5. 05性能ガイダンスとテスト項目

活用シーン

  1. 01マルチテナントRLSマイグレーションのレビュー
  2. 02テナント間のデータ分離の検証
  3. 03認証関連のデータベース変更の確認
  4. 04RLSポリシーと性能のテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/troykelly/claude-skills --skill postgres-rls

インストール後、Claude Code で/postgres-rlsを入力して読み込まれたか確認します。

詳細

PostgreSQL 行レベルセキュリティ

概要

行レベルセキュリティ(RLS)は、データ分離を多層的に保護します。正しく実装すれば、アプリケーションコードでフィルターを設定し忘れてもデータ漏えいを防げます。誤って実装すると、テナント間でデータが流出しているのに、誤った安心感を生み出します。

基本原則: RLS は最後の防衛線であり、唯一の防衛線ではありません。誤った実装はデータ侵害につながります。

開始時に宣言: 「Row Level Security の実装を検証するため、postgres-rls を適用します。」

このスキルを適用する場合

次のパターンのいずれかに触れる場合、このスキルは必須です。

パターン例
**/migrations/**/*tenant*migrations/001_add_tenant_id.sql
**/migrations/**/*rls*migrations/005_enable_rls.sql
**/migrations/**/*policy*migrations/010_create_policies.sql
**/*policy*.sqldb/policies.sql
**/auth/**src/auth/context.ts
**/*tenant*lib/tenant.ts, services/tenantService.ts
**/*multi-tenant*docs/multi-tenant-architecture.md

次のコマンドで確認します。

git diff --name-only HEAD~1 | grep -iE '(tenant|rls|policy|auth.*sql|multi.?tenant)'

重大な脆弱性

1. スーパーユーザーによるバイパス(重大)

スーパーユーザーと BYPASSRLS を持つロールは、すべてのポリシーを無視します。

-- DANGEROUS: Testing as superuser shows RLS "working" when it's bypassed
SET ROLE postgres;
SELECT * FROM orders;  -- Returns ALL rows, RLS ignored

-- CORRECT: Test as application role
SET ROLE app_user;
SELECT * FROM orders;  -- Returns only permitted rows

チェックリスト:

  • アプリケーションはスーパーユーザーではないロールで接続する
  • BYPASSRLS 属性を持つロールがない
  • テストはスーパーユーザーではなく、アプリケーションロールで実行する
2. テーブル所有者によるバイパス(重大)

FORCE ROW LEVEL SECURITY が設定されていない場合、テーブル所有者は RLS をバイパスします。

-- INCOMPLETE: Owners bypass this
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- COMPLETE: Everyone including owners must obey policies
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

チェックリスト:

  • すべての RLS 対象テーブルで ENABLE と FORCE の両方を設定する
  • マイグレーションに両方のステートメントを含める
3. ビューによるバイパス(重大)

ビューはデフォルトで作成者の権限で実行されます。スーパーユーザーが所有するビューは、RLS を完全にバイパスします。

-- DANGEROUS: View owned by superuser bypasses RLS
CREATE VIEW all_orders AS SELECT * FROM orders;

-- SAFE (PostgreSQL 15+): Security invoker respects caller's RLS
CREATE VIEW user_orders
WITH (security_invoker = true)
AS SELECT * FROM orders;

チェックリスト:

  • RLS 対象テーブルのすべてのビューで security_invoker = true を使用する(PG15+)
  • ビューをスーパーユーザーロールに所有させない
  • マテリアライズドビューが RLS をバイパスすることを文書化する
4. USING と WITH CHECK の不一致(高)

USING は読み取りをフィルターし、WITH CHECK は書き込みを検証します。WITH CHECK がないと、挿入したデータを参照できない事態が起こり得ます。

-- INCOMPLETE: User can INSERT rows they can't SELECT
CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- COMPLETE: Both read and write protected
CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.tenant_id')::uuid)
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

チェックリスト:

  • すべてのポリシーに USING と WITH CHECK の両方を設定する
  • WITH CHECK のロジックがセキュリティ上の意図と一致している
5. スレッドローカルコンテキストの漏えい(高)

コネクションプーリングにより、リクエスト間でテナントコンテキストが漏えいする可能性があります。

-- DANGEROUS: Context persists across pooled connections
SET app.tenant_id = 'tenant-123';

-- SAFE: Use SET LOCAL inside transaction (auto-resets)
BEGIN;
SET LOCAL app.tenant_id = 'tenant-123';
-- ... queries ...
COMMIT;  -- Context automatically cleared

アプリケーションのパターン:

// DANGEROUS: Leaks between requests
await db.query(`SET app.tenant_id = '${tenantId}'`);

// SAFE: Transaction-scoped context
await db.transaction(async (trx) => {
  await trx.raw(`SET LOCAL app.tenant_id = ?`, [tenantId]);
  // ... queries ...
});

チェックリスト:

  • 必ず SET ではなく SET LOCAL を使用する
  • コンテキストはトランザクション内で設定する
  • リクエスト後のハンドラーでコンテキストをリセットする(多層防御)
6. ポリシー関数の SQL インジェクション(高)

ポリシーで使用される関数は、インジェクションの攻撃経路になる可能性があります。

-- DANGEROUS: If current_tenant() uses user input unsafely
CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_tenant());

-- The function itself must be injection-safe:
CREATE OR REPLACE FUNCTION current_tenant()
RETURNS uuid AS $$
BEGIN
  -- SAFE: Casts to UUID, not string concatenation
  RETURN current_setting('app.tenant_id')::uuid;
END;
$$ LANGUAGE plpgsql STABLE;
7. マテリアライズドビューとデータエクスポート(中)

マテリアライズドビューは、ソーステーブルの RLS を適用しません。データエクスポートによってポリシーがバイパスされる場合があります。

-- DANGEROUS: Contains ALL tenants' data
CREATE MATERIALIZED VIEW order_stats AS
SELECT tenant_id, count(*) FROM orders GROUP BY tenant_id;

-- Background jobs with superuser access can export all data

チェックリスト:

  • マテリアライズドビューをセキュリティ上重要な対象として文書化する
  • エクスポートジョブをアプリケーションロールで実行する
  • 大量データアクセスの監査ログを用意する

パフォーマンスに関する考慮事項

ポリシー列にインデックスを作成する
-- Without index: Sequential scan on every query
CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- Add index for policy column
CREATE INDEX idx_orders_tenant_id ON orders(tenant_id);
関数をサブクエリでラップする

行ごとに呼び出される関数は負荷が高くなります。1 回だけ評価されるよう、サブクエリでラップします。

-- SLOW: Function called per row
CREATE POLICY access_check ON documents
  USING (user_has_access(auth.uid(), id));

-- FASTER: Evaluated once, cached
CREATE POLICY access_check ON documents
  USING ((SELECT auth.uid()) = owner_id);
複雑なチェックには SECURITY DEFINER を使用する

SECURITY DEFINER 関数を使った RLS ポリシーの連鎖は避けます。

-- SLOW: RLS on permissions table also evaluated
CREATE POLICY access_check ON documents
  USING (id IN (SELECT document_id FROM permissions WHERE user_id = auth.uid()));

-- FASTER: Bypass RLS chain with SECURITY DEFINER
CREATE OR REPLACE FUNCTION user_document_ids(uid uuid)
RETURNS SETOF uuid AS $$
  SELECT document_id FROM permissions WHERE user_id = uid;
$$ LANGUAGE sql STABLE SECURITY DEFINER;

CREATE POLICY access_check ON documents
  USING (id IN (SELECT * FROM user_document_ids(auth.uid())));
パフォーマンスのために非正規化する

結合から明らかに分かる場合でも、すべてのテーブルに tenant_id を格納します。

-- SLOW: Must join to get tenant context
CREATE POLICY order_items_policy ON order_items
  USING (order_id IN (
    SELECT id FROM orders WHERE tenant_id = current_setting('app.tenant_id')::uuid
  ));

-- FAST: Direct column check
ALTER TABLE order_items ADD COLUMN tenant_id uuid;
CREATE POLICY order_items_policy ON order_items
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

マイグレーションのパターン

安全な RLS マイグレーション
-- Step 1: Add column (if needed)
ALTER TABLE orders ADD COLUMN IF NOT EXISTS tenant_id uuid;

-- Step 2: Backfill data (batched for large tables)
UPDATE orders SET tenant_id = (
  SELECT tenant_id FROM customers WHERE customers.id = orders.customer_id
) WHERE tenant_id IS NULL;

-- Step 3: Add NOT NULL constraint
ALTER TABLE orders ALTER COLUMN tenant_id SET NOT NULL;

-- Step 4: Create index
CREATE INDEX CONCURRENTLY idx_orders_tenant_id ON orders(tenant_id);

-- Step 5: Enable RLS (both statements!)
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

-- Step 6: Create policies
CREATE POLICY tenant_isolation ON orders
  FOR ALL
  USING (tenant_id = current_setting('app.tenant_id')::uuid)
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

-- Step 7: Grant appropriate permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_role;

RLS のテスト

必須のテスト
describe('RLS Policies', () => {
  it('tenant A cannot see tenant B data', async () => {
    // Insert as tenant A
    await setTenantContext('tenant-a');
    await db('orders').insert({ id: 1, tenant_id: 'tenant-a', amount: 100 });

    // Switch to tenant B
    await setTenantContext('tenant-b');

    // Should not see tenant A's data
    const orders = await db('orders').select();
    expect(orders).toHaveLength(0);
  });

  it('cannot insert data for other tenant', async () => {
    await setTenantContext('tenant-a');

    await expect(
      db('orders').insert({ tenant_id: 'tenant-b', amount: 100 })
    ).rejects.toThrow(/violates row-level security/);
  });

  it('superuser role is not used in application', async () => {
    const result = await db.raw('SELECT current_user');
    expect(result.rows[0].current_user).not.toBe('postgres');
  });
});
スーパーユーザー以外でテストする
# Create test role
CREATE ROLE test_app_user;
GRANT app_role TO test_app_user;

# Run tests as this role
psql -U test_app_user -d testdb -f tests/rls_tests.sql

RLS ポリシーの成果物

RLS を実装する際は、次の成果物をイシューに投稿します。


## Row Level Security Implementation

### Tables with RLS Enabled

| Table | ENABLE | FORCE | Policies | Index |
|-------|--------|-------|----------|-------|
| orders | ✅ | ✅ | tenant_isolation | idx_orders_tenant_id |
| order_items | ✅ | ✅ | tenant_isolation | idx_order_items_tenant_id |
| customers | ✅ | ✅ | tenant_isolation | idx_customers_tenant_id |

### Policy Details

| Table | Policy | USING | WITH CHECK |
|-------|--------|-------|------------|
| orders | tenant_isolation | tenant_id = current_tenant() | tenant_id = current_tenant() |

### Security Verification

- [ ] Application connects as non-superuser role
- [ ] All RLS tables have FORCE ROW LEVEL SECURITY
- [ ] All policies have WITH CHECK clause
- [ ] Context uses SET LOCAL (transaction-scoped)
- [ ] Views use security_invoker = true
- [ ] Policy columns are indexed
- [ ] Cross-tenant tests written and passing

### Application Role
- Role name: `app_service`
- BYPASSRLS: `false`
- Superuser: `false`

**Verified At:** [timestamp]

チェックリスト

RLS の実装を完了する前に、次を確認します。

  • すべてのテーブルで ENABLE と FORCE ROW LEVEL SECURITY を設定している
  • すべてのポリシーに USING と WITH CHECK の両方を設定している
  • アプリケーションはスーパーユーザーでも BYPASSRLS ロールでもないロールで接続する
  • トランザクション内で SET LOCAL を使用してコンテキストを設定している
  • ビューで security_invoker = true を使用している(PG15+)
  • ポリシー列にインデックスを作成している
  • テナント間の分離テストに合格している
  • RLS の成果物をイシューに投稿している

統合

このスキルは、次の場合に起動します。

  • tenant/rls/policy パターンを含むマイグレーションファイルの変更
  • 認証関連のデータベースコードの変更
  • マルチテナントアーキテクチャの変更

このスキルは、次のスキルと連携します。

  • security-review - RLS は、より広範なセキュリティレビューの一部です
  • database-architecture - RLS に関する判断は、アーキテクチャ上の判断です
  • local-service-testing - 実際の Postgres に対して RLS をテストする必要があります

参考資料

コメント