Skip to content

RLS 绕过机制 ​

RLS 的设计有个绕不开的现实矛盾:既要让普通用户严格受限,又要让备份、复制、运维、数据修复等特权操作能看到全部数据。

如果只有"策略"这一条路,那么备份工具要么漏数据、要么得为每张表单独放行。为此,PostgreSQL 提供了一组明确的绕过(bypass)机制——它们不是"后门",而是被规范定义、可被审计的特权通道。

本节讲清四类绕过的身份、控制它们的方法,以及绕过机制在备份与逻辑复制中的实际表现和风险。

前置知识

阅读本节前,建议先了解:USING 与 WITH CHECK 详解

基础概念 ​

四类绕过主体 ​

主体默认是否绕过能否被限制控制手段
超级用户(superuser)✅ 始终绕过❌ 无法限制撤销 SUPERUSER
BYPASSRLS 角色✅ 始终绕过❌ 无法限制撤销 BYPASSRLS
表属主✅ 默认绕过✅ 可用 FORCE 约束ALTER TABLE … FORCE ROW LEVEL SECURITY
引用完整性检查✅ 始终绕过❌ 不可限制设计上必须绕过

此外还有一类"看起来像绕过、其实不是"的东西:row_security = off。它不是绕过开关,而是"宁可报错也不静默漏行"的保险丝,本节会专门辨析。

绕过发生在哪一步 ​

text
                       查询
                        │
                        ▼
        ┌───────────────────────────────────┐
        │ 是不是超级用户?                    │──是──▶ 绕过 RLS,直接执行
        └───────────────┬───────────────────┘
                        │ 否
                        ▼
        ┌───────────────────────────────────┐
        │ 角色有没有 BYPASSRLS?              │──是──▶ 绕过 RLS,直接执行
        └───────────────┬───────────────────┘
                        │ 否
                        ▼
        ┌───────────────────────────────────┐
        │ 是不是表属主,且未 FORCE?           │──是──▶ 绕过 RLS,直接执行
        └───────────────┬───────────────────┘
                        │ 否
                        ▼
                  走策略组合检查
        RESTRICTIVE AND (PERMISSIVE OR …)

三种"身份"概念要分清 ​

RLS 判断"我是谁"依赖的是当前有效用户,三个概念要区分:

概念由什么决定典型用途
会话用户(SESSION_USER)建立连接时的登录身份,SET SESSION AUTHORIZATION 可改审计"谁连进来的"
当前用户(current_user / USER)会话用户 + SET ROLE 的效果策略里的权限判断
当前角色(CURRENT_ROLE)同 current_userSQL 标准兼容写法

策略表达式里的 current_user 用的是"当前有效身份",这正是 SET ROLE 能切换 RLS 视角的原因。

绕过与 security_invoker 的关系 ​

视图是另一个容易混淆的"身份切换点":

视图定义用谁的权限和策略
默认视图属主的
WITH (security_invoker = true)调用者的

默认行为下,只要视图属主能绕过 RLS,用户通过视图就能看到全部数据——这是一个隐式的绕过通道,而且不需要任何特权角色。

语法与用法 ​

查看当前身份与特权 ​

sql
SELECT
    session_user,
    current_user,
    current_setting('row_security') AS row_security;
text
 session_user | current_user | row_security
--------------+--------------+--------------
 app_user     | app_user     | on
(1 row)

查角色是否具备绕过能力:

sql
SELECT rolname, rolsuper, rolbypassrls, rolcreaterole
FROM pg_roles
WHERE rolname IN ('app_user', 'dba_backup', 'postgres')
ORDER BY rolname;
text
   rolname   | rolsuper | rolbypassrls | rolcreaterole
-------------+----------+--------------+---------------
 app_user    | f        | f            | f
 dba_backup  | f        | t            | f
 postgres    | t        | t            | t
(3 rows)

查看表级 RLS 状态 ​

sql
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');
text
  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 ​

sql
-- 新角色直接带 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——普通角色无法自我提权。

控制表属主是否绕过 ​

sql
-- 启用 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 之后谁都绕不过去了"。

用会话参数做"防漏行"保险 ​

sql
-- 默认值
SHOW row_security;

-- 关闭:若查询结果会被策略过滤,直接报错
SET row_security = off;

取值范围只有 on 和 off,没有 force。

临时切换身份 ​

sql
-- 切换到某个角色(可分层的权限视角)
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 配置关闭:

sql
-- 以超级用户身份,无论策略怎么写,都返回全部行
SELECT count(*) FROM orders;

-- 审计:哪些角色是超级用户
SELECT rolname FROM pg_roles WHERE rolsuper;

因此:绝不能让应用使用超级用户连接,也不能让应用账号能 SET ROLE 到超级用户。

BYPASSRLS:专门为"需要看全部"的角色设计 ​

BYPASSRLS 是一个角色属性,语义明确:该角色绕过所有 RLS 策略。它的典型用户是备份工具账号、数据同步账号、运维只读账号。

sql
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 层限制(想让这个角色受限,只能撤销该属性)。撤销后立刻回到受策略约束的状态:

sql
ALTER ROLE dba_backup NOBYPASSRLS;

表属主:默认绕过,可用 FORCE 收回来 ​

表属主绕过 RLS 是默认行为。官方文档的措辞是"表属主通常不受行安全策略约束",并给出了唯一的上锁方式——FORCE ROW LEVEL SECURITY。

sql
-- 检查某张表的属主
SELECT relname, pg_get_userbyid(relowner) AS owner
FROM pg_class WHERE relname = 'orders';

-- 收回顾主绕过
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

这是生产环境的必做项。原因很实际:应用账号经常被设计成建表账号(即表属主),如果忘了 FORCE,策略写再多也不会对应用生效。检查 relforcerowsecurity 是否为 t:

sql
SELECT relrowsecurity, relforcerowsecurity FROM pg_class WHERE relname = 'orders';
text
 relrowsecurity | relforcerowsecurity
----------------+---------------------
 t              | f
(1 row)

relforcerowsecurity = f 就是那个漏洞。

FORCE 的边界:只对属主有效 ​

FORCE 不是"全局强制",它的作用范围很窄:

主体ENABLE 之后ENABLE + FORCE 之后
普通角色受策略约束受策略约束
表属主绕过受策略约束
BYPASSRLS 角色绕过仍然绕过
超级用户绕过仍然绕过

即使 FORCE 了,超级用户照样看到全部行——FORCE 的有效对象只有表属主。

引用完整性检查:设计上必须绕过 ​

唯一约束、主键、外键、CHECK 约束的内部校验始终绕过 RLS,以保证数据库的完整性。这不是漏洞,是必需——否则一个无权看到某行的用户插入引用该行的外键时,数据库无法做一致性判断。

但它带来隐蔽信道(covert channel) 泄漏:

sql
-- 通过"插入是否报唯一冲突"推断某值是否存在
INSERT INTO tenants (code, name) VALUES ('ACME', 'x');
text
ERROR:  duplicate key value violates unique constraint "tenants_pkey"

用户借此得知 ACME 已存在,即使他看不到那一行。缓解手段是用无业务含义的代理键:

sql
-- 不推荐:业务码做唯一键,可被外部探测
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:不是绕过,是保险丝 ​

这是最容易被误解的一个参数。官方描述很明确:它本身不绕过行安全,作用是"如果某个查询的结果会被策略过滤,就抛错"。

sql
SET row_security = off;
SELECT count(*) FROM orders;

若当前身份会被策略过滤,直接报错;若是属主、超级用户或 BYPASSRLS 角色(本来就不会被过滤),则正常返回结果:

text
-- 受限用户
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 环境下备份的一个常见坑:

bash
# 若备份账号既非超级用户、也非表属主、又无 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 就失效

推荐第二种:专用、最小权限、可审计。

sql
-- 专用备份账号
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;
bash
pg_dump -h localhost -U dba_backup -d mydb -Fc -f /backup/mydb.dump

逻辑复制的 RLS 行为 ​

这是 RLS 与复制的交叉点,官方文档给出了精确规则:

  1. 复制连接使用的角色必须有 REPLICATION 属性(或为超级用户);
  2. 若该角色既不是 SUPERUSER 也没有 BYPASSRLS,发布端的行安全策略会被执行;
  3. 若不想让策略被执行,可在连接串里加 options=-crow_security=off。此时如果表属主又新增了策略,复制会直接停止,而不是去执行策略;
  4. 订阅端的应用进程按订阅属主的权限运行,具体操作某张表时会切换到表属主身份执行。
sql
-- 发布端:复制角色
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 权限的用户都能看到全部数据——不需要任何显式特权。

sql
-- 假设 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;   -- 看到的是全部行,不是本租户的
text
 count
-------
  1240
(1 row)

修复方式是显式启用 security_invoker(PostgreSQL 15+):

sql
CREATE VIEW v_own_orders WITH (security_invoker = true) AS
    SELECT * FROM orders;

SET ROLE 带来的间接绕过 ​

SET ROLE 让一个角色能"变成"其成员角色。如果目标角色绕过 RLS,那么源角色就间接获得了绕过能力:

sql
-- 把绕过能力"传"了下去
GRANT dba_backup TO app_user;
sql
SET ROLE app_user;
SET ROLE dba_backup;      -- 一旦可切换,RLS 白做
SELECT count(*) FROM orders;

审计要点:检查是否存在从普通角色到 BYPASSRLS / 超户角色的角色继承路径。

sql
-- 列出所有带 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 更精细的工具:提权范围被严格限定在一个函数里。

sql
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,只授权给需要的角色。
sql
-- 更安全的写法
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,不受行安全约束。

sql
-- 即使有 RLS,只要角色有 TRUNCATE 权限就能清空整表
TRUNCATE orders;

这意味着:

  • RLS 保护不了 TRUNCATE,要防它只能靠不给 TRUNCATE 权限;
  • 引用外键所需的对被引用表的 REFERENCES 权限,同样不受 RLS 影响。
sql
-- 防 TRUNCATE:只授 DML,不授 TRUNCATE
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user;
-- 不要 GRANT TRUNCATE

实战示例 ​

示例一:判断"为什么 app_user 看到了全部数据" ​

排查步骤固定,按顺序执行即可定位。

sql
-- 1. 应用账号是什么身份?
SELECT session_user, current_user;
text
 session_user | current_user
--------------+--------------
 app_user     | app_user
(1 row)
sql
-- 2. 它有特权吗?
SELECT rolname, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = 'app_user';
text
  rolname  | rolsuper | rolbypassrls
-----------+----------+--------------
 app_user  | f        | f
(1 row)

不是超户,也不带 BYPASSRLS。

sql
-- 3. 它是表属主吗?表启用/强制了吗?
SELECT relname,
       pg_get_userbyid(relowner) AS owner,
       relrowsecurity, relforcerowsecurity
FROM pg_class WHERE relname = 'orders';
text
 relname |  owner   | relrowsecurity | relforcerowsecurity
---------+----------+----------------+---------------------
 orders  | dba_app  | t              | f
(1 row)

属主是 dba_app,不是 app_user,所以"属主绕过"不成立。继续查策略:

sql
-- 4. 有哪些策略?
SELECT policyname, permissive, roles, cmd, qual
FROM pg_policies WHERE tablename = 'orders';
text
   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 的隔离被完全绕过。

修复:

sql
DROP POLICY p_open_read ON orders;

示例二:为备份与运维建立受控绕过 ​

需求:让备份账号能导出全部数据,但它不能被应用借用。

sql
-- 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 提权)

验证应用账号无法提权:

sql
SET ROLE app_user;
SET ROLE dba_backup;
text
ERROR:  permission denied to set role "dba_backup"

执行备份:

bash
pg_dump -h localhost -U dba_backup -d mydb -Fc -f /backup/mydb-$(date +%F).dump
text
pg_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 的账号,且它既非超户也非表属主:

bash
pg_dump -h localhost -U app_readonly -d mydb -f /backup/mydb.sql
text
pg_dump: error: query would be affected by row-level security policy for table "orders"

这条报错正是 pg_dump 把 row_security 设为 off 的体现:它宁可失败,也不愿悄悄导出一份残缺的数据。

示例三:审计数据库里所有的绕过通道 ​

把下面的查询做成例行巡检脚本,能在权限膨胀早期发现问题。

sql
-- ① 所有具备绕过能力的角色
SELECT rolname, rolsuper, rolbypassrls, rolcanlogin
FROM pg_roles
WHERE rolsuper OR rolbypassrls
ORDER BY rolsuper DESC, rolname;
text
   rolname    | rolsuper | rolbypassrls | rolcanlogin
--------------+----------+--------------+-------------
 postgres     | t        | t            | t
 dba_backup   | f        | t            | t
(2 rows)
sql
-- ② 启用 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;
text
  relname  |  owner  | policy_count
-----------+---------+--------------
 invoices  | dba_app |            2
(1 row)

invoices 已有策略但未 FORCE,属主 dba_app 随时能看到全部行——若应用使用的正是 dba_app,隔离就是失效的。

sql
-- ③ 面向 PUBLIC 的宽松策略(最容易造成越权)
SELECT tablename, policyname, roles, cmd, qual
FROM pg_policies
WHERE permissive = 'PERMISSIVE'
  AND 'public' = ANY (roles)
ORDER BY tablename, policyname;
text
 tablename |  policyname   |   roles    |  cmd   | qual
-----------+---------------+------------+--------+------
 orders    | p_open_read   | {public}   | SELECT | true
(1 row)
sql
-- ④ 面向 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 的属主表正是"策略看起来生效、实际没生效"的头号原因:

sql
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 全部失效:

sql
-- 错误!
ALTER ROLE app_user BYPASSRLS;

不要把绕过角色授予应用角色

SET ROLE 会让提权变成一次普通调用:

sql
-- 错误!app_user 因此可 SET ROLE dba_backup
GRANT dba_backup TO app_user;

row_security 只有 on / off,且 off 不是绕过

off 的语义是"宁可报错也不静默漏行",没有 force 取值:

sql
-- 错误!
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,权限不足时报错而不是漏数据,这是有意设计:

bash
# 用普通受限账号会失败
pg_dump -U app_readonly -d mydb -f /tmp/x.sql

视图默认使用属主权限,是隐式绕过通道

sql
-- 危险:以超级用户为属主创建的视图,PUBLIC 可见即等于绕过
SET ROLE admin;
CREATE VIEW v_all AS SELECT * FROM orders;
GRANT SELECT ON v_all TO PUBLIC;

SECURITY DEFINER 函数是受控绕过,但必须加固

漏掉 search_path 会让它本身变成漏洞:

sql
-- 不推荐:未固定 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 约束,引用完整性检查也绕过

前者只能靠权限控制;后者会带来隐蔽信道泄漏,应用代理键而非业务码做主键:

sql
-- 不要给应用 TRUNCATE 权限
REVOKE TRUNCATE ON orders FROM app_user;

最佳实践 ​

生产表一律 ENABLE + FORCE

两句话,缺一不可:

sql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE  ROW LEVEL SECURITY;

把 BYPASSRLS 限制在专用、可审计、不复用的账号上

一个账号只做一件事:

sql
CREATE ROLE dba_backup LOGIN PASSWORD 'strong-password' BYPASSRLS;
-- 只授予 SELECT 与 CONNECT,不授予任何应用角色

禁止把绕过角色授予应用角色

这是一条硬性红线,应写进团队规范并定期审计:

sql
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

sql
CREATE VIEW v_orders WITH (security_invoker = true) AS SELECT * FROM orders;

SECURITY DEFINER 函数固定 search_path 并撤销 PUBLIC 执行权

sql
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 账号,并复核导出结果

不要用超户备份,也不要依赖"碰巧是属主":

bash
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 则被官方明确指出会带来"表属主可执行任意代码"的风险:

sql
CREATE SUBSCRIPTION sub_conn
    CONNECTION 'host=... options=-crow_security=off'
    PUBLICATION pub_x;
-- 不显式写 run_as_owner,即保持默认 false

把绕过通道审计纳入例行巡检

四组查询固定跑,覆盖全部入口:

sql
-- ① 特权角色  ② 未 FORCE 的表  ③ PUBLIC 宽松策略  ④ 缺 security_invoker 的视图
SELECT rolname FROM pg_roles WHERE rolsuper OR rolbypassrls;

应用账号一律使用最小权限,且不做表属主

让"属主绕过"这条路从根上不成立:

sql
-- 建表用专门的 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 模拟真实身份

不要用超户自测通过就上线:

sql
BEGIN;
SET LOCAL ROLE app_user;
SET LOCAL app.tenant_id = '7';
SELECT count(*) FROM orders;   -- 这才是应用看到的结果
COMMIT;

定期复核 pg_dump 与复制的权限前提

备份与复制是最容易因 RLS 静默失败的地方,纳入监控:

bash
# 在 CI 或定时任务里跑一次试导出,失败即告警
pg_dump -U dba_backup -d mydb -Fc -f /dev/null || echo "backup blocked by RLS"

下一节

继续学习:RLS 实战场景

参考链接 ​