权限从 fail-open 翻到 fail-closed,上线后用户看到空白页
写一个安全收口 PR 不难——把 if (!perm) return true 改成 if (!perm) return false,方向绝对正确。难的是你改完之后,系统里已经存在的一批角色,之前靠着"无配置 = 默认放行"活得好好的,这次上线直接从"全部可见"变成"什么都看不到"。用户打开后台,页面空白,打电话问是不是系统挂了。
现象
某后台管理系统的报表权限谓词长期是 fail-open 写法:
// 旧逻辑:缺配置 = 放行
function canView(reportId, perms, activeRoleId) {
if (!activeRoleId) return true // 角色还没加载完,放行
const p = perms[reportId]
if (!p) return true // ← 没有配置行,默认允许
if (p.viewRoles.length === 0) return true // ← 空数组,默认允许
return p.viewRoles.includes(activeRoleId)
}安全收口 PR 把它改成 fail-closed:
// 新逻辑:缺配置 = 拒绝
function canView(reportId, perms, activeRoleId) {
if (!activeRoleId) return true // loading 态保持放行(这没问题)
const p = perms[reportId]
if (!p) return false // ← 没有配置行,直接拒绝
return p.viewRoles.includes(activeRoleId) // 空数组也是拒绝
}改动本身逻辑无误,PR review 通过,测试用例绿。上线后,几个真实角色的用户反映报表列表全空了。
根因
问题出在爆炸半径的隐形性:权限表(report_role_permissions)里从来没有给某些角色写配置行——历史原因,这些角色当初"没什么要限制的,就不配置了"。fail-open 时代这没问题,但 fail-closed 一上线,perms[reportId] 找不到行就立即返回 false,整个报表列表对这些角色归零。
这类爆炸半径的反直觉之处在于:你改的是新逻辑,受影响的是旧数据。测试环境数据库往往是干净的,不会暴露生产里这些"空白配置"角色。
fail-open → fail-closed 是正确的安全方向,但凡是"缺配置 = 放行"活了一段时间的系统,一定存在靠这个隐式默认活着的角色。上线前如果不做存量审计,这些角色的用户会在生产环境里撞到空白页。
正解
上线前,先跑一条审计 SQL,找出所有在线活跃角色中、在权限表里零配置行的角色,并统计它们绑定的活跃用户数:
SELECT
r.id,
r.code,
r.name,
COUNT(p.id) AS config_count,
(
SELECT COUNT(*)
FROM user_role_binding ur
WHERE ur.role_id = r.id AND ur.is_deleted = 0
) AS active_users
FROM roles r
LEFT JOIN report_role_permissions p
ON p.role_id = r.id AND p.is_deleted = 0
WHERE r.is_deleted = 0
AND r.is_active = 1
GROUP BY r.id, r.code, r.name
HAVING config_count = 0
ORDER BY active_users DESC;拿到结果后,对每个受影响角色做出明确决策,在 PR 合并前完成补救:
| 情况 | 对策 | |---|---| | 重要操作角色 + 有活跃用户 | 补写基线配置行(明确该角色能看哪些报表) | | 角色刻意留空(权限未决) | 上线前先合 UX 空状态 PR("无权限,请联系管理员")作兜底 | | 角色零活跃用户 | 风险低,记录在 PR 描述里即可,不急处理 |
部署顺序同样关键:先补数据 → 再上 UX 空状态 → 最后上 fail-closed 谓词。顺序反了会有一个窗口期,用户直接撞空白格而不是友好提示。
对任何将"缺配置 = 放行"翻转为"缺配置 = 拒绝"的 PR,上线前必须执行:1) 审计零配置活跃角色;2) 决定是补配置还是上空状态 UX;3) 按正确顺序部署。审计本身只是一条 SQL,5 分钟能做,但省掉的是生产事故。
一句话外卖
安全收口改的是代码默认值,但受害者是生产数据库里那些"依赖旧默认值活着"的存量数据——上线前跑一次角色审计,把隐性依赖变成显性决策。