内容提要
PostgreSQL行级安全(RLS)存在五类隐患:表所有者默认绕过策略,需用FORCE;空租户上下文因空字符串报错,需nullif处理;连接池会串租户,须用SET LOCAL;外键检查绕过策略,导致越权与数据泄露,需复合租户键;后加表易漏配策略,应自动化审计。
延伸解读
表所有者绕过策略:最危险的默认行为
文章实测显示,即使启用了行级安全并创建了策略,表所有者仍然能看到所有行,除非显式启用 FORCE ROW LEVEL SECURITY。更关键的是,如果迁移和应用程序使用同一个数据库用户,该用户就是表所有者,策略形同虚设。因此,务必检查所有带策略的表是否同时启用了 ENABLE 和 FORCE,并将迁移与应用程序角色分离。
空租户上下文:从安全失败到服务中断
当会话变量从未设置时,current_setting 返回 NULL,策略比较结果为 NULL,查询返回零行,这是安全的。但一旦变量被设置过又重置,它会变成空字符串,直接转换为 UUID 会报错,导致所有查询失败。文章建议使用 nullif(..., '') 处理,确保三种状态都返回零行,避免生产环境中的意外中断。
连接池与租户串扰:必须使用 SET LOCAL
在事务模式下,连接池会在请求间切换后端连接。如果使用会话级变量(set_config 的第三个参数为 false),连接可能残留前一个请求的租户 ID,导致数据泄露。文章强调必须使用 SET LOCAL 或 set_config(..., true),并确保整个工作单元在一个显式事务内。此外,应通过探针验证连接是否会话绑定,否则应用应拒绝启动。
外键检查绕过策略:隐蔽的越权与泄露
外键约束检查会绕过行级安全,导致两种问题:一是可能插入指向其他租户不可见行的数据,造成数据损坏;二是通过成功或失败的错误信息,可以推断出其他租户是否存在某行,形成隐蔽通道。解决方案是让外键包含租户列,例如使用复合外键 (tenant_id, invoice_id) 引用 (tenant_id, id),并创建租户内唯一的索引。
Q&A
PostgreSQL 行级安全(RLS)为什么有时会失效?
RLS 失效通常是因为连接被允许绕过策略。表所有者默认绕过自己的策略,除非启用 FORCE ROW LEVEL SECURITY;超级用户和具有 BYPASSRLS 的角色始终绕过,即使启用了 FORCE。此外,RLS 还会通过三种与角色无关的途径泄漏:会话变量在连接上残留、外键检查绕过策略、以及后添加的表从未配置策略。
如何防止表所有者绕过 RLS 策略?
必须对表启用 FORCE ROW LEVEL SECURITY。如果迁移和应用程序使用同一个数据库用户,该用户会成为表所有者,策略将无效。建议迁移使用单独的 DDL 连接,应用程序使用 NOSUPERUSER NOBYPASSRLS 角色,并对所有带策略的表同时启用 ENABLE 和 FORCE。
为什么租户上下文为空字符串会导致 RLS 报错?如何解决?
当会话变量曾被设置然后被 RESET 或 SET LOCAL 事务结束后,current_setting('app.tenant_id', true) 返回空字符串 '' 而不是 NULL。直接转换为 uuid 会报错 invalid input syntax for type uuid: ""。解决方案是在策略表达式中使用 nullif(current_setting('app.tenant_id', true), '')::uuid,这样三种状态(从未设置、已重置、事务结束)都返回零行。
连接池如何导致租户数据串扰?如何避免?
连接池(如 PgBouncer 事务模式)会在事务间切换后端连接,导致会话变量丢失或残留前一个租户的值,从而返回零行或他人数据。避免方法是使用 SET LOCAL 或 set_config(..., true) 将变量作用域限制在当前事务内,并确保整个工作单元在一个显式事务中。同时应验证连接是否会话绑定,例如通过探针测试。
外键检查如何绕过 RLS 并导致数据泄露?如何修复?
外键引用检查始终绕过行级安全,因此可以插入引用不可见行的数据,或通过成功/错误差异推断其他租户的数据存在。修复方法是让键本身携带租户:创建复合唯一索引(如 (tenant_id, number))和复合外键(如 (tenant_id, invoice_id) REFERENCES invoices (tenant_id, id)),这样跨租户引用会失败,且成功/错误不再泄露信息。
如何检测遗漏 RLS 策略的表?
使用系统目录查询:从 pg_class、pg_namespace、pg_attribute 和 pg_policy 中找出所有包含 tenant_id 列但未启用 RLS、未强制 RLS 或没有策略的表。该查询应作为测试套件的一部分,并自动从模式派生表列表,而不是手动维护。
如何验证 RLS 策略确实生效?
通过三个测试:1) 以客户 A 写入一行,切换到客户 B 后用原始查询(绕过 ORM 作用域)计数,结果应为零;2) 无租户上下文查询,结果应为零且无数据库错误;3) 运行模式自检查询,检查所有带 tenant_id 的表是否都正确配置了 RLS。第一个测试最关键,能将应用层遗漏作用域的错误从数据泄露变为空列表。