RLS 绕过机制
RLS 的设计有个绕不开的现实矛盾:既要让普通用户严格受限,又要让备份、复制、运维、数据修复等特权操作能看到全部数据。
如果只有"策略"这一条路,那么备份工具要么漏数据、要么得为每张表单独放行。为此,PostgreSQL 提供了一组明确的绕过(bypass)机制——它们不是"后门",而是被规范定义、可被审计的特权通道。
本节讲清四类绕过的身份、控制它们的方法,以及绕过机制在备份与逻辑复制中的实际表现和风险。
前置知识
阅读本节前,建议先了解:USING 与 WITH CHECK 详解
基础概念
四类绕过主体
| 主体 | 默认是否绕过 | 能否被限制 | 控制手段 |
|---|---|---|---|
| 超级用户(superuser) | ✅ 始终绕过 | ❌ 无法限制 | 撤销 SUPERUSER |
BYPASSRLS 角色 | ✅ 始终绕过 | ❌ 无法限制 | 撤销 BYPASSRLS |
| 表属主 | ✅ 默认绕过 | ✅ 可用 FORCE 约束 | ALTER TABLE … FORCE ROW LEVEL SECURITY |
| 引用完整性检查 | ✅ 始终绕过 | ❌ 不可限制 | 设计上必须绕过 |
此外还有一类"看起来像绕过、其实不是"的东西:row_security = off。它不是绕过开关,而是"宁可报错也不静默漏行"的保险丝,本节会专门辨析。
绕过发生在哪一步
查询
│
▼
┌───────────────────────────────────┐
│ 是不是超级用户? │──是──▶ 绕过 RLS,直接执行
└───────────────┬───────────────────┘
│ 否
▼
┌───────────────────────────────────┐
│ 角色有没有 BYPASSRLS? │──是──▶ 绕过 RLS,直接执行
└───────────────┬───────────────────┘
│ 否
▼
┌───────────────────────────────────┐
│ 是不是表属主,且未 FORCE? │──是──▶ 绕过 RLS,直接执行
└───────────────┬───────────────────┘
│ 否
▼
走策略组合检查
RESTRICTIVE AND (PERMISSIVE OR …)三种"身份"概念要分清
RLS 判断"我是谁"依赖的是当前有效用户,三个概念要区分:
| 概念 | 由什么决定 | 典型用途 |
|---|---|---|
会话用户(SESSION_USER) | 建立连接时的登录身份,SET SESSION AUTHORIZATION 可改 | 审计"谁连进来的" |
当前用户(current_user / USER) | 会话用户 + SET ROLE 的效果 | 策略里的权限判断 |
当前角色(CURRENT_ROLE) | 同 current_user | SQL 标准兼容写法 |
策略表达式里的 current_user 用的是"当前有效身份",这正是 SET ROLE 能切换 RLS 视角的原因。
绕过与 security_invoker 的关系
视图是另一个容易混淆的"身份切换点":
| 视图定义 | 用谁的权限和策略 |
|---|---|
| 默认 | 视图属主的 |
WITH (security_invoker = true) | 调用者的 |
默认行为下,只要视图属主能绕过 RLS,用户通过视图就能看到全部数据——这是一个隐式的绕过通道,而且不需要任何特权角色。
语法与用法
查看当前身份与特权
SELECT
session_user,
current_user,
current_setting('row_security') AS row_security; session_user | current_user | row_security
--------------+--------------+--------------
app_user | app_user | on
(1 row)查角色是否具备绕过能力:
SELECT rolname, rolsuper, rolbypassrls, rolcreaterole
FROM pg_roles
WHERE rolname IN ('app_user', 'dba_backup', 'postgres')
ORDER BY rolname; rolname | rolsuper | rolbypassrls | rolcreaterole
-------------+----------+--------------+---------------
app_user | f | f | f
dba_backup | f | t | f
postgres | t | t | t
(3 rows)查看表级 RLS 状态
SELECT relname,
relrowsecurity AS rls_enabled,
relforcerowsecurity AS rls_forced,
pg_get_userbyid(relowner) AS owner
FROM pg_class
WHERE relname IN ('orders', 'invoices', 'audit_log'); relname | rls_enabled | rls_forced | owner
------------+-------------+------------+---------
orders | t | t | dba_app
invoices | t | f | dba_app
audit_log | f | f | dba_app
(3 rows)orders:已启用且FORCE,属主也受约束;invoices:已启用但未FORCE,属主dba_app绕过;audit_log:未启用,所有人都绕过。
授予与撤销 BYPASSRLS
-- 新角色直接带 BYPASSRLS(需要有超级用户或已具备 BYPASSRLS 的角色执行)
CREATE ROLE dba_backup LOGIN PASSWORD 'change-me' BYPASSRLS;
-- 给已有角色加上
ALTER ROLE dba_backup BYPASSRLS;
-- 撤销
ALTER ROLE dba_backup NOBYPASSRLS;BYPASSRLS 与 NOBYPASSRLS 的默认值是 NOBYPASSRLS。注意一条限制:只有超级用户,或已经具备 BYPASSRLS 的角色,才能指定 BYPASSRLS——普通角色无法自我提权。
控制表属主是否绕过
-- 启用 RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- 让表属主也受 RLS 约束
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
-- 恢复属主绕过
ALTER TABLE orders NO FORCE ROW LEVEL SECURITY;FORCE 只对表属主有效,对超级用户和 BYPASSRLS 角色完全无效。这一点经常被误解为"FORCE 之后谁都绕不过去了"。
用会话参数做"防漏行"保险
-- 默认值
SHOW row_security;
-- 关闭:若查询结果会被策略过滤,直接报错
SET row_security = off;取值范围只有 on 和 off,没有 force。
临时切换身份
-- 切换到某个角色(可分层的权限视角)
SET ROLE app_user;
SELECT current_user; -- 策略随即按 app_user 求值
RESET ROLE;
-- 切换整个会话身份(需要相应权限)
SET SESSION AUTHORIZATION app_user;
SELECT session_user, current_user;
RESET SESSION AUTHORIZATION;SET ROLE 改变的是 current_user,这是验证策略最常用的手段,也是"提权"最容易被忽视的途径——若某角色能被 SET ROLE 到一个绕过 RLS 的角色,它实际就获得了绕过能力。
详细说明
超级用户:最强的、也是最不可控的
超级用户在 PostgreSQL 中"可以覆盖数据库内的所有访问限制",RLS 不是例外。超户的绕过无法通过任何 RLS 配置关闭:
-- 以超级用户身份,无论策略怎么写,都返回全部行
SELECT count(*) FROM orders;
-- 审计:哪些角色是超级用户
SELECT rolname FROM pg_roles WHERE rolsuper;因此:绝不能让应用使用超级用户连接,也不能让应用账号能 SET ROLE 到超级用户。
BYPASSRLS:专门为"需要看全部"的角色设计
BYPASSRLS 是一个角色属性,语义明确:该角色绕过所有 RLS 策略。它的典型用户是备份工具账号、数据同步账号、运维只读账号。
CREATE ROLE dba_backup LOGIN PASSWORD 'change-me' BYPASSRLS;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dba_backup;BYPASSRLS 有两个必须记住的性质:不受 FORCE 影响(FORCE ROW LEVEL SECURITY 只约束表属主,对 BYPASSRLS 无任何作用);不可被 RLS 层限制(想让这个角色受限,只能撤销该属性)。撤销后立刻回到受策略约束的状态:
ALTER ROLE dba_backup NOBYPASSRLS;表属主:默认绕过,可用 FORCE 收回来
表属主绕过 RLS 是默认行为。官方文档的措辞是"表属主通常不受行安全策略约束",并给出了唯一的上锁方式——FORCE ROW LEVEL SECURITY。
-- 检查某张表的属主
SELECT relname, pg_get_userbyid(relowner) AS owner
FROM pg_class WHERE relname = 'orders';
-- 收回顾主绕过
ALTER TABLE orders FORCE ROW LEVEL SECURITY;这是生产环境的必做项。原因很实际:应用账号经常被设计成建表账号(即表属主),如果忘了 FORCE,策略写再多也不会对应用生效。检查 relforcerowsecurity 是否为 t:
SELECT relrowsecurity, relforcerowsecurity FROM pg_class WHERE relname = 'orders'; relrowsecurity | relforcerowsecurity
----------------+---------------------
t | f
(1 row)relforcerowsecurity = f 就是那个漏洞。
FORCE 的边界:只对属主有效
FORCE 不是"全局强制",它的作用范围很窄:
| 主体 | ENABLE 之后 | ENABLE + FORCE 之后 |
|---|---|---|
| 普通角色 | 受策略约束 | 受策略约束 |
| 表属主 | 绕过 | 受策略约束 |
BYPASSRLS 角色 | 绕过 | 仍然绕过 |
| 超级用户 | 绕过 | 仍然绕过 |
即使 FORCE 了,超级用户照样看到全部行——FORCE 的有效对象只有表属主。
引用完整性检查:设计上必须绕过
唯一约束、主键、外键、CHECK 约束的内部校验始终绕过 RLS,以保证数据库的完整性。这不是漏洞,是必需——否则一个无权看到某行的用户插入引用该行的外键时,数据库无法做一致性判断。
但它带来隐蔽信道(covert channel) 泄漏:
-- 通过"插入是否报唯一冲突"推断某值是否存在
INSERT INTO tenants (code, name) VALUES ('ACME', 'x');ERROR: duplicate key value violates unique constraint "tenants_pkey"用户借此得知 ACME 已存在,即使他看不到那一行。缓解手段是用无业务含义的代理键:
-- 不推荐:业务码做唯一键,可被外部探测
CREATE TABLE tenants (code text PRIMARY KEY, name text NOT NULL);
-- 推荐:代理键 + 业务码不加唯一约束(或加在受限 schema 里)
CREATE TABLE tenants (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
code text NOT NULL,
name text NOT NULL
);row_security = off:不是绕过,是保险丝
这是最容易被误解的一个参数。官方描述很明确:它本身不绕过行安全,作用是"如果某个查询的结果会被策略过滤,就抛错"。
SET row_security = off;
SELECT count(*) FROM orders;若当前身份会被策略过滤,直接报错;若是属主、超级用户或 BYPASSRLS 角色(本来就不会被过滤),则正常返回结果:
-- 受限用户
ERROR: query would be affected by row-level security policy for table "orders"
-- 属主 / 超户 / BYPASSRLS 角色
count
-------
1240
(1 row)它真正的用途是防止备份/导出时静默漏数据。pg_dump 就默认把 row_security 设为 off:官方说明是"pg_dump 默认将 row_security 设为 OFF,以确保表的所有内容都被导出。若执行 pg_dump 的用户权限不足,会返回错误。但超级用户与被转储表的属主总是绕过 RLS。"
这也解释了 RLS 环境下备份的一个常见坑:
# 若备份账号既非超级用户、也非表属主、又无 BYPASSRLS,可能直接失败
pg_dump -h localhost -U dba_backup -d mydb -f /backup/mydb.sql
# pg_dump: error: query would be affected by row-level security policy for table "orders"备份场景的三种可用方案
| 方案 | 做法 | 代价 |
|---|---|---|
| 超级用户备份 | 用 postgres 账号执行 pg_dump | 权限过高,凭据管理风险大 |
BYPASSRLS 专用账号 | 建独立备份账号并授予该属性 | 需要在备份后撤销或严格管控 |
| 表属主备份 | 用表属主账号执行 | 属主未 FORCE 时可行;一旦 FORCE 就失效 |
推荐第二种:专用、最小权限、可审计。
-- 专用备份账号
CREATE ROLE dba_backup LOGIN PASSWORD 'change-me' BYPASSRLS;
GRANT CONNECT ON DATABASE mydb TO dba_backup;
GRANT USAGE ON SCHEMA public TO dba_backup;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dba_backup;
-- 确保未来新建的表也被覆盖
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO dba_backup;pg_dump -h localhost -U dba_backup -d mydb -Fc -f /backup/mydb.dump逻辑复制的 RLS 行为
这是 RLS 与复制的交叉点,官方文档给出了精确规则:
- 复制连接使用的角色必须有
REPLICATION属性(或为超级用户); - 若该角色既不是
SUPERUSER也没有BYPASSRLS,发布端的行安全策略会被执行; - 若不想让策略被执行,可在连接串里加
options=-crow_security=off。此时如果表属主又新增了策略,复制会直接停止,而不是去执行策略; - 订阅端的应用进程按订阅属主的权限运行,具体操作某张表时会切换到表属主身份执行。
-- 发布端:复制角色
CREATE ROLE r_repl LOGIN REPLICATION PASSWORD 'change-me';
-- 若不授 BYPASSRLS,则需在订阅端连接串里显式设置:
-- host=... port=... user=r_repl options=-crow_security=off
-- 订阅的 run_as_owner 默认 false
-- false:逐表切换到表属主身份执行 → 订阅属主需要能 SET ROLE 到每个表属主
-- true :不切换身份,统一用订阅属主权限 → 只需目标表的 DML 权限
CREATE SUBSCRIPTION sub_orders
CONNECTION 'host=pub_host dbname=mydb user=r_repl'
PUBLICATION pub_orders
WITH (run_as_owner = false);官方明确警告:把 run_as_owner 设为 true 会让任何表属主都能以订阅属主的权限执行任意代码(例如在自己拥有的表上挂一个触发器)。因此除非数据库内部的用户安全无关紧要,否则不应使用。另外,发布端权限只在复制连接建立时检查一次,之后读取变更记录不再重复检查;订阅端则在每个事务应用时重新检查订阅属主权限。这决定了"事后改权限"在两端的效果不同。
视图默认行为:一个隐式的绕过通道
前面提过,视图默认使用视图属主的权限与策略。如果视图属主的身份能绕过 RLS,那么任何有该视图 SELECT 权限的用户都能看到全部数据——不需要任何显式特权。
-- 假设 admin 是超级用户,这等于给所有人开了一道绕过通道
SET ROLE admin;
CREATE VIEW v_all_orders AS SELECT * FROM orders;
GRANT SELECT ON v_all_orders TO PUBLIC;
-- 切换到受限用户
SET ROLE app_user;
SELECT count(*) FROM v_all_orders; -- 看到的是全部行,不是本租户的 count
-------
1240
(1 row)修复方式是显式启用 security_invoker(PostgreSQL 15+):
CREATE VIEW v_own_orders WITH (security_invoker = true) AS
SELECT * FROM orders;SET ROLE 带来的间接绕过
SET ROLE 让一个角色能"变成"其成员角色。如果目标角色绕过 RLS,那么源角色就间接获得了绕过能力:
-- 把绕过能力"传"了下去
GRANT dba_backup TO app_user;SET ROLE app_user;
SET ROLE dba_backup; -- 一旦可切换,RLS 白做
SELECT count(*) FROM orders;审计要点:检查是否存在从普通角色到 BYPASSRLS / 超户角色的角色继承路径。
-- 列出所有带 BYPASSRLS 或 SUPERUSER 的角色
SELECT rolname, rolsuper, rolbypassrls
FROM pg_roles
WHERE rolsuper OR rolbypassrls;
-- 列出成员关系,排查提权路径
SELECT r.rolname AS member_role, g.rolname AS granted_role
FROM pg_auth_members m
JOIN pg_roles r ON r.oid = m.member
JOIN pg_roles g ON g.oid = m.roleid
ORDER BY 1, 2;用 SECURITY DEFINER 做"受控提权"
有时业务确实需要"让受限用户间接访问他看不到的数据"。SECURITY DEFINER 函数是比 BYPASSRLS 更精细的工具:提权范围被严格限定在一个函数里。
CREATE FUNCTION order_total_for_tenant(p_tenant int)
RETURNS numeric
LANGUAGE sql
SECURITY DEFINER
STABLE
AS $$
SELECT sum(amount) FROM orders WHERE tenant_id = p_tenant;
$$;
REVOKE ALL ON FUNCTION order_total_for_tenant(int) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION order_total_for_tenant(int) TO app_user;关键配套措施:
SECURITY DEFINER函数必须显式写死search_path,否则可能被搜索路径劫持;- 必须撤销
PUBLIC的默认EXECUTE,只授权给需要的角色。
-- 更安全的写法
CREATE FUNCTION order_total_for_tenant(p_tenant int)
RETURNS numeric
LANGUAGE sql
SECURITY DEFINER
STABLE
SET search_path = pg_catalog, public
AS $$
SELECT sum(amount) FROM public.orders WHERE tenant_id = p_tenant;
$$;注意:
SECURITY DEFINER函数本身仍受 RLS 约束——它用的是函数属主(通常是超级用户)的身份,因此能绕过。这既是它有用的原因,也是它危险的原因:任何一个SECURITY DEFINER函数配合不当,都是一个绕过通道。
RLS 与 TRUNCATE / REFERENCES
官方明确说明:针对整表的操作,如 TRUNCATE 和 REFERENCES,不受行安全约束。
-- 即使有 RLS,只要角色有 TRUNCATE 权限就能清空整表
TRUNCATE orders;这意味着:
- RLS 保护不了
TRUNCATE,要防它只能靠不给TRUNCATE权限; - 引用外键所需的对被引用表的
REFERENCES权限,同样不受 RLS 影响。
-- 防 TRUNCATE:只授 DML,不授 TRUNCATE
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user;
-- 不要 GRANT TRUNCATE实战示例
示例一:判断"为什么 app_user 看到了全部数据"
排查步骤固定,按顺序执行即可定位。
-- 1. 应用账号是什么身份?
SELECT session_user, current_user; session_user | current_user
--------------+--------------
app_user | app_user
(1 row)-- 2. 它有特权吗?
SELECT rolname, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = 'app_user'; rolname | rolsuper | rolbypassrls
-----------+----------+--------------
app_user | f | f
(1 row)不是超户,也不带 BYPASSRLS。
-- 3. 它是表属主吗?表启用/强制了吗?
SELECT relname,
pg_get_userbyid(relowner) AS owner,
relrowsecurity, relforcerowsecurity
FROM pg_class WHERE relname = 'orders'; relname | owner | relrowsecurity | relforcerowsecurity
---------+----------+----------------+---------------------
orders | dba_app | t | f
(1 row)属主是 dba_app,不是 app_user,所以"属主绕过"不成立。继续查策略:
-- 4. 有哪些策略?
SELECT policyname, permissive, roles, cmd, qual
FROM pg_policies WHERE tablename = 'orders'; policyname | permissive | roles | cmd | qual
----------------+------------+----------+--------+----------------------------------
p_tenant | PERMISSIVE | {app_user} | ALL | (tenant_id = (current_setting('app.tenant_id'::text))::integer)
p_open_read | PERMISSIVE | {public} | SELECT | true
(2 rows)找到了:p_open_read 是一条 TO PUBLIC 的宽松策略,USING (true) 放行全部行。两条宽松策略之间是 OR,所以 p_tenant 的隔离被完全绕过。
修复:
DROP POLICY p_open_read ON orders;示例二:为备份与运维建立受控绕过
需求:让备份账号能导出全部数据,但它不能被应用借用。
-- 1. 专用备份角色,带 BYPASSRLS
CREATE ROLE dba_backup LOGIN PASSWORD 'strong-password' BYPASSRLS;
-- 2. 只给读权限
GRANT CONNECT ON DATABASE mydb TO dba_backup;
GRANT USAGE ON SCHEMA public TO dba_backup;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dba_backup;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO dba_backup;
-- 3. 关键:不要把这个角色授予任何应用角色
-- (如果不小心 GRANT dba_backup TO app_user,应用就能 SET ROLE 提权)验证应用账号无法提权:
SET ROLE app_user;
SET ROLE dba_backup;ERROR: permission denied to set role "dba_backup"执行备份:
pg_dump -h localhost -U dba_backup -d mydb -Fc -f /backup/mydb-$(date +%F).dumppg_dump: dumping contents of table "public.orders"
pg_dump: dumping contents of table "public.invoices"
pg_dump: dumping contents of table "public.audit_log"对比:如果换成没有 BYPASSRLS 的账号,且它既非超户也非表属主:
pg_dump -h localhost -U app_readonly -d mydb -f /backup/mydb.sqlpg_dump: error: query would be affected by row-level security policy for table "orders"这条报错正是 pg_dump 把 row_security 设为 off 的体现:它宁可失败,也不愿悄悄导出一份残缺的数据。
示例三:审计数据库里所有的绕过通道
把下面的查询做成例行巡检脚本,能在权限膨胀早期发现问题。
-- ① 所有具备绕过能力的角色
SELECT rolname, rolsuper, rolbypassrls, rolcanlogin
FROM pg_roles
WHERE rolsuper OR rolbypassrls
ORDER BY rolsuper DESC, rolname; rolname | rolsuper | rolbypassrls | rolcanlogin
--------------+----------+--------------+-------------
postgres | t | t | t
dba_backup | f | t | t
(2 rows)-- ② 启用 RLS 但未 FORCE 的表(属主仍是绕过通道)
SELECT c.relname,
pg_get_userbyid(c.relowner) AS owner,
count(p.polname) AS policy_count
FROM pg_class c
LEFT JOIN pg_policy p ON p.polrelid = c.oid
WHERE c.relkind = 'r'
AND c.relnamespace = 'public'::regnamespace
AND c.relrowsecurity
AND NOT c.relforcerowsecurity
GROUP BY 1, 2
ORDER BY 1; relname | owner | policy_count
-----------+---------+--------------
invoices | dba_app | 2
(1 row)invoices 已有策略但未 FORCE,属主 dba_app 随时能看到全部行——若应用使用的正是 dba_app,隔离就是失效的。
-- ③ 面向 PUBLIC 的宽松策略(最容易造成越权)
SELECT tablename, policyname, roles, cmd, qual
FROM pg_policies
WHERE permissive = 'PERMISSIVE'
AND 'public' = ANY (roles)
ORDER BY tablename, policyname; tablename | policyname | roles | cmd | qual
-----------+---------------+------------+--------+------
orders | p_open_read | {public} | SELECT | true
(1 row)-- ④ 面向 PUBLIC 且带 security_invoker 缺失的视图(隐式绕过通道)
SELECT c.relname AS view_name,
pg_get_userbyid(c.relowner) AS view_owner,
c.reloptions
FROM pg_class c
WHERE c.relkind = 'v'
AND c.relnamespace = 'public'::regnamespace;若 reloptions 中没有 security_invoker=true,则该视图以属主权限执行。这四组查询合起来,基本覆盖了 RLS 绕过的所有显式与隐式入口。
注意事项
FORCE 只约束表属主,不约束 BYPASSRLS 与超户
三者的边界互不等价,未 FORCE 的属主表正是"策略看起来生效、实际没生效"的头号原因:
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
CREATE POLICY p ON orders USING (tenant_id = 1);
-- 以上三条都挡不住 BYPASSRLS 角色与超级用户
-- 检查是否漏了 FORCE
SELECT relname, relrowsecurity, relforcerowsecurity FROM pg_class WHERE relname = 'orders';不要把 BYPASSRLS 给应用账号
一旦授予,所有策略包括 FORCE 全部失效:
-- 错误!
ALTER ROLE app_user BYPASSRLS;不要把绕过角色授予应用角色
SET ROLE 会让提权变成一次普通调用:
-- 错误!app_user 因此可 SET ROLE dba_backup
GRANT dba_backup TO app_user;row_security 只有 on / off,且 off 不是绕过
off 的语义是"宁可报错也不静默漏行",没有 force 取值:
-- 错误!
SET row_security = force;
-- 误解:以为 off 可以借此看全部数据
SET row_security = off;
SELECT count(*) FROM orders; -- ERROR: query would be affected by row-level security policy注意 pg_dump 的默认行为
它把 row_security 设为 off,权限不足时报错而不是漏数据,这是有意设计:
# 用普通受限账号会失败
pg_dump -U app_readonly -d mydb -f /tmp/x.sql视图默认使用属主权限,是隐式绕过通道
-- 危险:以超级用户为属主创建的视图,PUBLIC 可见即等于绕过
SET ROLE admin;
CREATE VIEW v_all AS SELECT * FROM orders;
GRANT SELECT ON v_all TO PUBLIC;SECURITY DEFINER 函数是受控绕过,但必须加固
漏掉 search_path 会让它本身变成漏洞:
-- 不推荐:未固定 search_path
CREATE FUNCTION f() RETURNS int SECURITY DEFINER LANGUAGE sql AS $$ SELECT 1 $$;
-- 推荐
CREATE FUNCTION f() RETURNS int SECURITY DEFINER LANGUAGE sql
SET search_path = pg_catalog, public AS $$ SELECT 1 $$;TRUNCATE / REFERENCES 不受 RLS 约束,引用完整性检查也绕过
前者只能靠权限控制;后者会带来隐蔽信道泄漏,应用代理键而非业务码做主键:
-- 不要给应用 TRUNCATE 权限
REVOKE TRUNCATE ON orders FROM app_user;最佳实践
生产表一律 ENABLE + FORCE
两句话,缺一不可:
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;把 BYPASSRLS 限制在专用、可审计、不复用的账号上
一个账号只做一件事:
CREATE ROLE dba_backup LOGIN PASSWORD 'strong-password' BYPASSRLS;
-- 只授予 SELECT 与 CONNECT,不授予任何应用角色禁止把绕过角色授予应用角色
这是一条硬性红线,应写进团队规范并定期审计:
SELECT r.rolname AS member_role, g.rolname AS granted_role
FROM pg_auth_members m
JOIN pg_roles r ON r.oid = m.member
JOIN pg_roles g ON g.oid = m.roleid
WHERE g.rolsuper OR g.rolbypassrls;面向多租户的视图一律加 security_invoker = true
CREATE VIEW v_orders WITH (security_invoker = true) AS SELECT * FROM orders;SECURITY DEFINER 函数固定 search_path 并撤销 PUBLIC 执行权
CREATE FUNCTION f() RETURNS int
LANGUAGE sql SECURITY DEFINER
SET search_path = pg_catalog, public
AS $$ SELECT 1 $$;
REVOKE ALL ON FUNCTION f() FROM PUBLIC;备份使用专用 BYPASSRLS 账号,并复核导出结果
不要用超户备份,也不要依赖"碰巧是属主":
pg_dump -U dba_backup -d mydb -Fc -f /backup/mydb.dump
# 恢复前抽查行数,确认没有漏数据订阅复制时优先用 row_security=off,并保持 run_as_owner 默认 false
row_security=off 会在策略出现时停下,暴露问题而不是默默执行策略;run_as_owner = true 则被官方明确指出会带来"表属主可执行任意代码"的风险:
CREATE SUBSCRIPTION sub_conn
CONNECTION 'host=... options=-crow_security=off'
PUBLICATION pub_x;
-- 不显式写 run_as_owner,即保持默认 false把绕过通道审计纳入例行巡检
四组查询固定跑,覆盖全部入口:
-- ① 特权角色 ② 未 FORCE 的表 ③ PUBLIC 宽松策略 ④ 缺 security_invoker 的视图
SELECT rolname FROM pg_roles WHERE rolsuper OR rolbypassrls;应用账号一律使用最小权限,且不做表属主
让"属主绕过"这条路从根上不成立:
-- 建表用专门的 owner 角色,应用只用 app_user
CREATE ROLE dba_owner NOLOGIN;
CREATE ROLE app_user LOGIN PASSWORD 'change-me';
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user;验证策略时用 SET ROLE 模拟真实身份
不要用超户自测通过就上线:
BEGIN;
SET LOCAL ROLE app_user;
SET LOCAL app.tenant_id = '7';
SELECT count(*) FROM orders; -- 这才是应用看到的结果
COMMIT;定期复核 pg_dump 与复制的权限前提
备份与复制是最容易因 RLS 静默失败的地方,纳入监控:
# 在 CI 或定时任务里跑一次试导出,失败即告警
pg_dump -U dba_backup -d mydb -Fc -f /dev/null || echo "backup blocked by RLS"下一节
继续学习:RLS 实战场景
参考链接
- PostgreSQL 官方文档:Row Security Policies
- PostgreSQL 官方文档:CREATE ROLE(BYPASSRLS)
- PostgreSQL 官方文档:Logical Replication Security
- PostgreSQL 官方文档:pg_dump
- PostgreSQL 官方文档:Client Connection Defaults(row_security)
- PostgreSQL 官方文档:CREATE VIEW(security_invoker)
- PostgreSQL 官方文档:CREATE FUNCTION(SECURITY DEFINER)
- PostgreSQL 官方文档:pg_roles