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 权限。这种间接路径正是越权风险排查的重点。

角色继承链与越权路径 svc_report 账户(直接授予 app_readonly) svc_batch 账户(直接授予 app_writer) svc_ops 账户(直接授予 app_admin) app_readonly SELECT app_writer INSERT / UPDATE / DELETE app_admin TRUNCATE / CREATE dba_role SUPERUSER / CREATEROLE 继承 继承 风险继承 图例: 直接授予 / 正常继承 风险继承(间接获得高权限) 高权限角色 / 风险账户 结论: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 是间接继承。

验证间接权限是否真的可执行

找到可疑账户后,不能只凭继承关系就下结论,还需要验证该账户是否真的能执行未授权操作。验证方法有两种:

  1. 使用 SET ROLE 模拟:以超级用户连接后,执行 SET ROLE svc_ops;,然后尝试执行 CREATE DATABASE test_priv_check; 或 SELECT * FROM pg_shadow;。如果成功,说明间接权限确实生效。
  2. 使用 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 函数,它比人工推断更可靠;整改后必须重新执行查询和权限检查,形成闭环。