2. 数据库角色继承链分析与越权风险排查
角色继承链为什么会产生“看不见的高权限”
在多数关系型数据库中,角色可以授予给另一个角色,形成一条或多条继承链。账户本身可能只被授予了低权限角色,但该角色若继承了高权限角色,账户就会间接获得高权限。排查的核心不是看账户直接拥有什么角色,而是沿着继承链向上追溯,找出所有可达的高权限节点。
下面以 PostgreSQL 为例,假设业务库中存在如下角色结构:
app_readonly:只读角色,拥有SELECT权限。app_writer:读写角色,继承app_readonly,并拥有INSERT/UPDATE/DELETE权限。app_admin:业务管理员角色,继承app_writer,并拥有TRUNCATE和CREATE权限。dba_role:高权限角色,拥有SUPERUSER或CREATEROLE属性。- 账户
svc_report:只被直接授予app_readonly。 - 账户
svc_batch:只被直接授予app_writer。 - 账户
svc_ops:只被直接授予app_admin。
如果 app_admin 被授予了 dba_role,那么 svc_ops 就会通过 app_admin → dba_role 间接获得 DBA 权限。这种间接路径正是越权风险排查的重点。
用递归查询找出所有间接高权限账户
PostgreSQL 提供了 pg_auth_members 系统表,记录角色之间的成员关系。字段 roleid 是被授予的角色,member 是接收该角色的角色或账户。要找出所有通过继承链可达 dba_role 的账户,可以使用递归 CTE:
WITH RECURSIVE role_chain AS (
-- 起点:直接拥有 dba_role 的角色或账户
SELECT roleid, member, 1 AS depth
FROM pg_auth_members
WHERE roleid = (SELECT oid FROM pg_roles WHERE rolname = 'dba_role')
UNION ALL
-- 递归:继续向上追溯,谁继承了上一层的 member
SELECT m.roleid, m.member, rc.depth + 1
FROM pg_auth_members m
JOIN role_chain rc ON m.roleid = rc.member
)
SELECT DISTINCT
r.rolname AS account_or_role,
r.rolcanlogin AS can_login,
rc.depth
FROM role_chain rc
JOIN pg_roles r ON r.oid = rc.member
WHERE r.rolcanlogin = true
ORDER BY rc.depth, r.rolname;
这条查询会返回所有可登录账户,它们通过任意长度的继承链最终获得了 dba_role。其中 depth 表示距离直接授予的层数,depth=1 是直接拥有,depth>1 是间接继承。
验证间接权限是否真的可执行
找到可疑账户后,不能只凭继承关系就下结论,还需要验证该账户是否真的能执行未授权操作。验证方法有两种:
- 使用
SET ROLE模拟:以超级用户连接后,执行SET ROLE svc_ops;,然后尝试执行CREATE DATABASE test_priv_check;或SELECT * FROM pg_shadow;。如果成功,说明间接权限确实生效。 - 使用
has_*_privilege函数检查:例如SELECT has_database_privilege('svc_ops', 'postgres', 'CREATE');或SELECT has_table_privilege('svc_ops', 'pg_catalog.pg_shadow', 'SELECT');。这些函数会考虑角色继承,返回真实有效权限。
注意:
SET ROLE 验证应在测试环境或维护窗口进行,避免对生产库造成意外影响。验证完成后及时 RESET ROLE;。整改与复查
确认越权路径后,整改的核心是切断不必要的继承边。例如,如果 app_admin 不应拥有 DBA 权限,执行:
REVOKE dba_role FROM app_admin;
然后重新运行递归查询,确认 svc_ops 不再出现在结果中。最后用 has_database_privilege 复查,确保返回 false。
排查要点:角色继承链可能有多条路径,递归查询要覆盖所有分支;验证时优先使用 has_*_privilege 函数,它比人工推断更可靠;整改后必须重新执行查询和权限检查,形成闭环。