rls不能防止sql注入,但能限制注入成功后的数据越权访问;因其在查询执行前由数据库重写器自动添加where过滤,仅作用于启用了rls的目标表dml操作,不干预函数调用、系统表查询等绕过场景。

RLS 本身不能防止 SQL 注入,但能有效阻断注入成功后的数据越权提权。 换句话说:SQL 注入若已发生,攻击者仍可能执行任意语句;但只要 RLS 策略正确启用并覆盖所有操作类型,他最多只能读/写自己权限范围内的行——哪怕他拼出了 SELECT * FROM users,数据库也会自动加上 WHERE tenant_id = '当前会话值' 过滤条件。
为什么 RLS 不是 SQL 注入的“防火墙”
SQL 注入发生在应用层拼接字符串阶段,而 RLS 是查询执行前由 PostgreSQL 查询重写器注入的 WHERE 条件。两者不在同一防线:
- 注入成功 → 攻击者获得数据库连接上下文 → 可执行任意合法 SQL(如
SELECT pg_sleep(10)、UNION SELECT password FROM users) - RLS 生效 → 仅对目标表的 DML 操作(
SELECT/INSERT/UPDATE/DELETE)自动加过滤,不干预函数调用、元数据查询、系统表访问等 - 典型绕过场景:
SELECT * FROM pg_user不受任何 RLS 约束;SELECT * FROM app_data WHERE id = 1 OR 1=1会被 RLS 截断为... AND tenant_id = 'xxx',但前提是app_data启用了 RLS 且策略未被TO PUBLIC或角色遗漏绕过
配置 RLS 策略时必须避免的三个提权漏洞点
很多团队启用了 RLS 却仍被越权,问题常出在策略定义或上下文管理上:
-
USING和WITH CHECK混用错误:比如只写了USING (tenant_id = current_setting('app.tenant_id')::UUID)用于SELECT,却没给INSERT加WITH CHECK,导致攻击者可伪造tenant_id插入其他租户数据 - 会话变量未设默认值或未校验:若应用忘记执行
SET app.tenant_id = 'xxx',current_setting('app.tenant_id', true)返回NULL,而tenant_id = NULL永远为 false → 启用 RLS 后无策略匹配,用户查不到任何数据(看似安全),但若策略写成tenant_id = current_setting('app.tenant_id')::UUID OR current_setting('app.tenant_id') IS NULL,就等于放行全部数据 - 超级用户/表所有者未被强制约束:默认情况下
ALTER TABLE ... FORCE ROW LEVEL SECURITY未启用,超级用户和表所有者执行查询时 RLS 策略自动失效。生产环境必须显式执行该命令,否则攻击者一旦拿到 superuser 权限,RLS 形同虚设
带 LEAKPROOF 函数的安全策略写法
直接在策略中调用非 LEAKPROOF 函数(如自定义的 get_tenant_id())可能导致侧信道信息泄露,甚至被用于盲注推断数据。正确做法是确保所有策略内函数都声明为 LEAKPROOF:
CREATE OR REPLACE FUNCTION get_current_tenant_id()
RETURNS UUID
LEAKPROOF STABLE LANGUAGE SQL AS $$
SELECT NULLIF(current_setting('app.tenant_id', TRUE), '')::UUID;
$$;
然后策略使用它:
CREATE POLICY tenant_isolation_policy ON app_data FOR ALL USING (tenant_id = get_current_tenant_id()) WITH CHECK (tenant_id = get_current_tenant_id());
注意:LEAKPROOF 表示该函数不会通过运行时行为(如执行时间、错误消息)泄露输入参数内容,是防御基于函数行为的盲注关键。
验证策略是否真正在生效
别只信 pg_policies 里有记录,要实测策略是否被查询计划实际采纳:
- 用非管理员角色连接,执行
EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM app_data LIMIT 1;,确认输出中出现类似Filter: ((tenant_id = 'xxx'::uuid))的行 - 故意 unset 会话变量后查询,检查是否返回 0 行(而非报错或全量返回)
- 用
pg_has_role(current_user, 'admin', 'MEMBER')写管理员例外策略时,务必测试 admin 角色能否真正绕过限制——常见错误是把USING写成AND而非OR,导致管理员也被过滤
最危险的疏忽不是策略没写,而是策略写了但没覆盖 FOR INSERT 或漏了 FORCE ROW LEVEL SECURITY,让 RLS 在关键路径上静默失效。











