LabHub
学习 学习路径 课程

生产级后端 API 综合项目

只靠角色守不住资源

在 LabHub 中继续学习

一句话总结

认证用于确认请求主体是谁,授权则判断该主体能否对本组织中的这个资源执行这项操作。如果只检查 writer 这一角色,拥有相同角色的用户就可能读取或修改他人的订单,造成横向越权。

概念图: 路由前的中间件。 · 资源关系无法在这里判断。 · 处理函数内部。 · 每条路由都需要人工添加,因此很容易漏掉。

为什么角色检查并不充分

基于角色的访问控制可以缩小功能范围,却无法表达资源之间的关系。即使 Alice 和 Bob 都是 writer,Bob 也没有理由取消 Alice 的订单。组织管理员也只能在自己的组织内拥有管理权限。因此,必须同时比较订单的 owner_idorg_id、身份中的 suborg_idrole,以及请求执行的操作。若属于不同组织,即使是管理员也应先拒绝;若属于同一组织,再应用资源所有者或获准管理员的策略。

未认证时返回 401;已经认证但关系不匹配时返回 403。是否让不存在的资源与无权访问的资源返回相同响应,以减少枚举攻击,也应作为明确策略决定。本练习的读取边界会对无权访问的 ID 始终返回 403。测试必须至少覆盖三种情况:同一组织中的其他资源所有者、其他组织的管理员,以及合法的资源所有者。

实际工作中容易忽略的泄露

如果把完整认证头写入日志,或在令牌解析错误中包含原文,可观测系统就会变成秘密存储库。模块导入时执行调试 print,也可能把敏感环境变量泄露到评分器或工作进程日志中。库在导入时应保持安静,错误响应只提供 unauthorizedforbidden 之类的分类。审计日志应记录非敏感的主体 ID 和策略结果,而不是令牌。

应该把授权判断放在哪里

即使规则相同,放在代码的哪个层次也会决定哪里可能出现漏洞。常见位置有三处,各自防范的问题不同。

路由前的中间件。 对于认证和角色检查这类无需读取资源即可完成的判断,这里最合适。它可以统一应用到所有路由,不容易遗漏。但资源关系无法在这里判断。 因为必须先读取具体订单才能知道所有者是谁。

处理函数内部。 这里是在读取资源后比较所有者和组织的位置。表达能力最强,但每条路由都需要人工添加,因此很容易漏掉。 如果添加新路由的人忘记那一行,只有该路由会暴露。因此采用这种方式时,还必须用测试确认不存在绕过授权检查的路由。

存储查询内部。 即把所有者和组织加入查询条件。最大优点是应用检查与存储范围不可能不一致,即使前两处有所遗漏,也不会返回他人的数据。代价是“资源不存在”和“无权访问”会变得相同。这样有助于减少枚举攻击,却更难向用户解释失败原因。

实践中通常会叠加使用三层:由中间件保证认证,由存储查询限定范围,再由处理函数判断针对具体操作的策略。如果只有一层,遗漏该层的路由就会立刻成为漏洞;多层叠加时,即使忘记一层,其他层仍能拦截。

此外,还要统计被拒绝的请求。 正常拒绝的请求总会有一些,但如果数量突然增加,可能是有人在探查他人的资源,也可能是我们的策略刚刚被错误修改。这两种情况都必须知道;不统计就都无法发现。

实务判断标准

授权测试不能止于“writer 可以成功”。必须把最危险的相邻资源关系和租户边界固化为反例测试。资源所有权也应写入数据库查询条件,避免应用检查与存储范围不一致。下一模块将介绍如何在同一 trace 上下文中观察这些拒绝与成功,同时不留下秘密。