postgres-rls

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PostgreSQL Row Level Security

PostgreSQL Row Level Security

Overview

概述

Row Level Security (RLS) provides defense-in-depth for data isolation. When implemented correctly, it prevents data leaks even if application code misses a filter. When implemented incorrectly, it creates false security confidence while data bleeds between tenants.
Core principle: RLS is your last line of defense, not your only one. Get it wrong and you have a data breach.
Announce at start: "I'm applying postgres-rls to verify Row Level Security implementation."
Row Level Security (RLS) 为数据隔离提供纵深防御。如果实现正确,即使应用代码遗漏了过滤条件,它也能防止数据泄露。如果实现错误,会造成虚假的安全信心,同时租户之间的数据会发生泄露。
核心原则: RLS是你的最后一道防线,而非唯一防线。如果配置错误,会导致数据泄露。
开始时声明: "我正在应用postgres-rls来验证Row Level Security的实现。"

When This Skill Applies

本规范适用场景

This skill is MANDATORY when ANY of these patterns are touched:
PatternExamples
**/migrations/**/*tenant*
migrations/001_add_tenant_id.sql
**/migrations/**/*rls*
migrations/005_enable_rls.sql
**/migrations/**/*policy*
migrations/010_create_policies.sql
**/*policy*.sql
db/policies.sql
**/auth/**
src/auth/context.ts
**/*tenant*
lib/tenant.ts, services/tenantService.ts
**/*multi-tenant*
docs/multi-tenant-architecture.md
Check with:
bash
git diff --name-only HEAD~1 | grep -iE '(tenant|rls|policy|auth.*sql|multi.?tenant)'
当涉及以下任何一种模式时,必须遵循本规范:
模式示例
**/migrations/**/*tenant*
migrations/001_add_tenant_id.sql
**/migrations/**/*rls*
migrations/005_enable_rls.sql
**/migrations/**/*policy*
migrations/010_create_policies.sql
**/*policy*.sql
db/policies.sql
**/auth/**
src/auth/context.ts
**/*tenant*
lib/tenant.ts, services/tenantService.ts
**/*multi-tenant*
docs/multi-tenant-architecture.md
检查方式:
bash
git diff --name-only HEAD~1 | grep -iE '(tenant|rls|policy|auth.*sql|multi.?tenant)'

The Critical Vulnerabilities

关键漏洞

1. Superuser Bypass (CRITICAL)

1. 超级用户绕过(严重)

Superusers and roles with
BYPASSRLS
ignore ALL policies.
sql
-- 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
Checklist:
  • Application connects as non-superuser role
  • No roles have
    BYPASSRLS
    attribute
  • Tests run as application role, NOT superuser
超级用户和拥有
BYPASSRLS
权限的角色会忽略所有策略。
sql
-- 危险:以超级用户身份测试会显示RLS“正常工作”,但实际上已被绕过
SET ROLE postgres;
SELECT * FROM orders;  -- 返回所有行,RLS被忽略

-- 正确:以应用角色测试
SET ROLE app_user;
SELECT * FROM orders;  -- 仅返回允许的行
检查清单:
  • 应用以非超级用户角色连接
  • 没有角色拥有
    BYPASSRLS
    属性
  • 测试以应用角色运行,而非超级用户

2. Table Owner Bypass (CRITICAL)

2. 表所有者绕过(严重)

Table owners bypass RLS unless
FORCE ROW LEVEL SECURITY
is set.
sql
-- 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;
Checklist:
  • All RLS tables have both ENABLE and FORCE
  • Migration includes both statements
除非设置
FORCE ROW LEVEL SECURITY
,否则表所有者会绕过RLS。
sql
-- 不完整:所有者可绕过此设置
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- 完整:包括所有者在内的所有角色必须遵守策略
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
检查清单:
  • 所有启用RLS的表同时设置了ENABLE和FORCE
  • 迁移脚本包含这两条语句

3. View Bypass (CRITICAL)

3. 视图绕过(严重)

Views run with creator's privileges by default. Views owned by superusers bypass RLS entirely.
sql
-- 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;
Checklist:
  • All views on RLS tables use
    security_invoker = true
    (PG15+)
  • Views not owned by superuser roles
  • Materialized views documented as bypassing RLS
默认情况下,视图以创建者的权限运行。超级用户拥有的视图会完全绕过RLS。
sql
-- 危险:超级用户拥有的视图会绕过RLS
CREATE VIEW all_orders AS SELECT * FROM orders;

-- 安全(PostgreSQL 15+):security_invoker 会遵循调用者的RLS设置
CREATE VIEW user_orders
WITH (security_invoker = true)
AS SELECT * FROM orders;
检查清单:
  • 所有基于RLS表的视图都使用
    security_invoker = true
    (PG15+)
  • 视图不由超级用户角色拥有
  • 物化视图需明确标注会绕过RLS

4. USING vs WITH CHECK Mismatch (HIGH)

4. USING与WITH CHECK不匹配(高风险)

USING
filters reads;
WITH CHECK
validates writes. Missing
WITH CHECK
allows inserting data you can't see.
sql
-- 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);
Checklist:
  • All policies have both USING and WITH CHECK
  • WITH CHECK logic matches security intent
USING
用于过滤读取操作;
WITH CHECK
用于验证写入操作。缺少
WITH CHECK
会允许插入用户无法查看的数据。
sql
-- 不完整:用户可以插入自己无法查询到的行
CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- 完整:同时保护读取和写入操作
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. Thread-Local Context Leakage (HIGH)

5. 线程本地上下文泄露(高风险)

Connection pooling can leak tenant context between requests.
sql
-- 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
Application pattern:
typescript
// 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 ...
});
Checklist:
  • Always use
    SET LOCAL
    not
    SET
  • Context set inside transactions
  • Post-request handler resets context (defense in depth)
连接池可能在请求之间泄露租户上下文。
sql
-- 危险:上下文会在池化连接中持续存在
SET app.tenant_id = 'tenant-123';

-- 安全:在事务内使用SET LOCAL(会自动重置)
BEGIN;
SET LOCAL app.tenant_id = 'tenant-123';
-- ... 查询 ...
COMMIT;  -- 上下文会自动清除
应用模式:
typescript
// 危险:会在请求之间泄露
await db.query(`SET app.tenant_id = '${tenantId}'`);

// 安全:事务作用域内的上下文
await db.transaction(async (trx) => {
  await trx.raw(`SET LOCAL app.tenant_id = ?`, [tenantId]);
  // ... 查询 ...
});
检查清单:
  • 始终使用
    SET LOCAL
    而非
    SET
  • 在事务内设置上下文
  • 请求后处理程序重置上下文(纵深防御)

6. SQL Injection in Policy Functions (HIGH)

6. 策略函数中的SQL注入(高风险)

Functions used in policies can be injection vectors.
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;
策略中使用的函数可能成为注入载体。
sql
-- 危险:如果current_tenant()不安全地使用用户输入
CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_tenant());

-- 函数本身必须具备注入安全性:
CREATE OR REPLACE FUNCTION current_tenant()
RETURNS uuid AS $$
BEGIN
  -- 安全:转换为UUID,而非字符串拼接
  RETURN current_setting('app.tenant_id')::uuid;
END;
$$ LANGUAGE plpgsql STABLE;

7. Materialized Views and Data Export (MEDIUM)

7. 物化视图与数据导出(中风险)

Materialized views don't respect source table RLS. Data exports may bypass policies.
sql
-- 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
Checklist:
  • Materialized views documented as security-sensitive
  • Export jobs run as application role
  • Audit log for bulk data access
物化视图不遵循源表的RLS。数据导出可能绕过策略。
sql
-- 危险:包含所有租户的数据
CREATE MATERIALIZED VIEW order_stats AS
SELECT tenant_id, count(*) FROM orders GROUP BY tenant_id;

-- 拥有超级用户权限的后台任务可以导出所有数据
检查清单:
  • 物化视图需标注为安全敏感项
  • 导出任务以应用角色运行
  • 批量数据访问需有审计日志

Performance Considerations

性能考量

Index Policy Columns

为策略列创建索引

sql
-- 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);
sql
-- 无索引:每次查询都会进行全表扫描
CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- 为策略列添加索引
CREATE INDEX idx_orders_tenant_id ON orders(tenant_id);

Wrap Functions in Subqueries

在子查询中包装函数

Functions called per-row are expensive. Wrap in subquery for single evaluation:
sql
-- 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);
逐行调用函数会影响性能。在子查询中包装以实现单次评估:
sql
-- 缓慢:逐行调用函数
CREATE POLICY access_check ON documents
  USING (user_has_access(auth.uid(), id));

-- 更快:仅评估一次,结果缓存
CREATE POLICY access_check ON documents
  USING ((SELECT auth.uid()) = owner_id);

Use SECURITY DEFINER for Complex Checks

使用SECURITY DEFINER处理复杂检查

Avoid RLS policy chains with SECURITY DEFINER functions:
sql
-- 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())));
使用SECURITY DEFINER函数避免RLS策略链:
sql
-- 缓慢:同时评估权限表的RLS
CREATE POLICY access_check ON documents
  USING (id IN (SELECT document_id FROM permissions WHERE user_id = auth.uid()));

-- 更快:使用SECURITY DEFINER绕过RLS链
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())));

Denormalize for Performance

为性能进行反规范化

Store tenant_id on every table, even if "obvious" from joins:
sql
-- 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);
在每个表中存储tenant_id,即使通过关联可以“推断”出来:
sql
-- 缓慢:必须通过关联获取租户上下文
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
  ));

-- 快速:直接检查列
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);

Migration Pattern

迁移模式

Safe RLS Migration

安全的RLS迁移

sql
-- 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;
sql
-- 步骤1:添加列(如果需要)
ALTER TABLE orders ADD COLUMN IF NOT EXISTS tenant_id uuid;

-- 步骤2:回填数据(大数据表分批处理)
UPDATE orders SET tenant_id = (
  SELECT tenant_id FROM customers WHERE customers.id = orders.customer_id
) WHERE tenant_id IS NULL;

-- 步骤3:添加NOT NULL约束
ALTER TABLE orders ALTER COLUMN tenant_id SET NOT NULL;

-- 步骤4:创建索引
CREATE INDEX CONCURRENTLY idx_orders_tenant_id ON orders(tenant_id);

-- 步骤5:启用RLS(两条语句都需要!)
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

-- 步骤6:创建策略
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);

-- 步骤7:授予适当权限
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_role;

Testing RLS

测试RLS

Required Tests

必选测试

typescript
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');
  });
});
typescript
describe('RLS Policies', () => {
  it('租户A无法查看租户B的数据', async () => {
    // 以租户A身份插入数据
    await setTenantContext('tenant-a');
    await db('orders').insert({ id: 1, tenant_id: 'tenant-a', amount: 100 });

    // 切换到租户B
    await setTenantContext('tenant-b');

    // 不应看到租户A的数据
    const orders = await db('orders').select();
    expect(orders).toHaveLength(0);
  });

  it('无法为其他租户插入数据', async () => {
    await setTenantContext('tenant-a');

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

  it('应用未使用超级用户角色', async () => {
    const result = await db.raw('SELECT current_user');
    expect(result.rows[0].current_user).not.toBe('postgres');
  });
});

Test as Non-Superuser

以非超级用户身份测试

bash
undefined
bash
undefined

Create test role

创建测试角色

CREATE ROLE test_app_user; GRANT app_role TO test_app_user;
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
undefined
psql -U test_app_user -d testdb -f tests/rls_tests.sql
undefined

RLS Policy Artifact

RLS策略工件

When implementing RLS, post this artifact to the issue:
markdown
<!-- RLS_IMPLEMENTATION:START -->
实现RLS时,需将以下工件发布到问题中:
markdown
<!-- RLS_IMPLEMENTATION:START -->

Row Level Security Implementation

Row Level Security实现

Tables with RLS Enabled

已启用RLS的表

TableENABLEFORCEPoliciesIndex
orderstenant_isolationidx_orders_tenant_id
order_itemstenant_isolationidx_order_items_tenant_id
customerstenant_isolationidx_customers_tenant_id
ENABLEFORCE策略索引
orderstenant_isolationidx_orders_tenant_id
order_itemstenant_isolationidx_order_items_tenant_id
customerstenant_isolationidx_customers_tenant_id

Policy Details

策略详情

TablePolicyUSINGWITH CHECK
orderstenant_isolationtenant_id = current_tenant()tenant_id = current_tenant()
策略USINGWITH CHECK
orderstenant_isolationtenant_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
  • 应用以非超级用户角色连接
  • 所有RLS表都设置了FORCE ROW LEVEL SECURITY
  • 所有策略都包含WITH CHECK子句
  • 使用SET LOCAL设置上下文(事务作用域)
  • 视图使用security_invoker = true
  • 策略列已创建索引
  • 跨租户隔离测试已编写并通过

Application Role

应用角色

  • Role name:
    app_service
  • BYPASSRLS:
    false
  • Superuser:
    false
Verified At: [timestamp]
<!-- RLS_IMPLEMENTATION:END -->
undefined
  • 角色名称:
    app_service
  • BYPASSRLS:
    false
  • 超级用户:
    false
验证时间: [时间戳]
<!-- RLS_IMPLEMENTATION:END -->
undefined

Checklist

检查清单

Before completing RLS implementation:
  • All tables have ENABLE and FORCE ROW LEVEL SECURITY
  • All policies have both USING and WITH CHECK
  • Application connects as non-superuser, non-BYPASSRLS role
  • Context set with SET LOCAL inside transactions
  • Views use security_invoker = true (PG15+)
  • Policy columns indexed
  • Cross-tenant isolation tests passing
  • RLS artifact posted to issue
完成RLS实现前:
  • 所有表都已设置ENABLE和FORCE ROW LEVEL SECURITY
  • 所有策略同时包含USING和WITH CHECK
  • 应用以非超级用户、非BYPASSRLS角色连接
  • 在事务内使用SET LOCAL设置上下文
  • 视图使用security_invoker = true(PG15+)
  • 策略列已创建索引
  • 跨租户隔离测试通过
  • RLS工件已发布到问题中

Integration

集成

This skill is triggered by:
  • Changes to migration files with tenant/rls/policy patterns
  • Changes to auth-related database code
  • Multi-tenant architecture changes
This skill integrates with:
  • security-review
    - RLS is part of broader security review
  • database-architecture
    - RLS decisions are architectural
  • local-service-testing
    - Must test RLS against real Postgres
本规范触发场景:
  • 涉及tenant/rls/policy模式的迁移文件变更
  • 认证相关数据库代码变更
  • 多租户架构变更
本规范集成的其他规范:
  • security-review
    - RLS是更广泛安全审查的一部分
  • database-architecture
    - RLS决策属于架构范畴
  • local-service-testing
    - 必须针对真实Postgres测试RLS

References

参考资料