越权、租户隔离、接口签名与重放:对象级权限怎样被证明
从 BOLA、租户上下文、数据层约束、规范化签名、nonce、时间窗与幂等建立对象级授权。
OWASP API Security 的对象级授权风险说明每个使用客户端对象标识的入口都需校验权限;密码学签名只能证明密钥和消息,不能替代业务授权。
“已登录”距离“能操作这个对象”还很远
授权决策至少输入 principal、tenant、object、action、对象状态和策略版本。管理员角色通常只在所属租户或管理域内有效;知道订单 id、把 URL 换成另一个 id、修改请求体 tenantId 都不能扩大范围。列表、详情、导出、批量、文件下载和异步任务必须使用同一对象权限模型。
避免先按 id 查询再在 Java 中比较租户,因为遗漏一次比较就越权,还会泄露对象存在性。查询条件直接包含 tenant_id 与授权范围,更新使用 tenant + id + version 条件;数据库行级安全可做纵深,但连接池租户上下文必须设置和清理。
租户上下文来自可信身份链
网关写入内部头时要先删除外部同名头,并用受认证通道传递;服务端仍验证服务身份与允许代表的租户。消息消费者从签名事件或权威任务记录恢复租户,不能信任 payload 中孤立字段。缓存 key、幂等 key、对象存储前缀和指标隔离都要包含租户域。
租户迁移与管理员代操作需要显式状态:原租户、新租户、批准人、期限和审计。禁止用“超级管理员”永久绕过所有数据约束。
签名先定义唯一的规范化请求
双方固定方法、规范路径、排序后的查询参数、选定 Header、body 摘要、时间、nonce、keyId 和算法;编码、空值、重复参数与大小写规则必须唯一。先解析后重建与原字节签名要选一种,不同代理重写路径时必须有清晰边界。
校验签名后仍要检查权限、新鲜度和一次性。时间窗吸收时钟误差但允许窗口内重放,nonce 存储或 operationId 幂等负责去重。nonce 记录与业务写入最好同一权威事务,否则“已占 nonce 但业务回滚”会制造不可重试失败。
数据层约束要覆盖所有访问形态
ORM Repository、原生 SQL、报表、搜索索引、缓存、对象存储和消息投影都可能成为旁路。租户隔离测试生成两个租户同形数据,对每个读取、更新、删除、导出和批量入口交换 id;结果应统一拒绝或不可见,审计不能把另一租户对象详情写入错误响应。
缓存 key 至少包含租户、对象、视图权限和 Schema 版本;共享搜索索引的 filter 必须由服务端注入且不可被客户端覆盖。后台任务保存发起主体与授权快照还不够,执行时需要决定是按快照完成还是重新校验当前权限,并对长任务定义撤销语义。
签名验证是可重放状态机的一部分
keyId 映射到固定主体、算法、用途和有效期;规范化后的请求摘要采用常量时间比较。服务端接受时间窗要结合可靠时钟和偏差告警,nonce 按主体隔离并保留超过窗口。大规模 nonce 存储可按时间分区清理,但不能在窗口未结束前驱逐。
业务 operationId 与 nonce 分工:nonce 阻止同一签名包重放,operationId 让调用方在结果未知时安全查询或重试。签名正确仍要执行对象授权、额度和状态检查;密钥泄露后按 keyId 撤销并回查窗口内高风险操作。
批量接口和间接引用最容易暴露 BOLA
批量删除若只校验调用者有 delete 权限,却没有逐对象验证租户与状态,一次请求就能扩大越权。更稳的实现让数据库用 tenant + allowed scope + id set 完成条件删除,并比较请求数与实际影响数;部分无权时选择整体拒绝或逐项稳定结果,不能悄悄成功。
子资源也必须从父对象重新建立范围,例如 /projects/{p}/files/{f} 要证明 f 属于 p 且 p 属于主体租户,不能分别按两个 id 查询后假设关系。外部可见 id 使用随机值可降低枚举,但真正控制仍是关系约束。
签名密钥轮换与多区域 nonce 要一起设计
轮换先发布新 keyId 验证能力,再切调用方签名,观察旧 key 使用归零后撤销。验证端缓存 key 时保留明确 notBefore/notAfter 与用途,未知 keyId 不从客户端给出的 URL 动态获取。不同伙伴和环境不共享密钥,泄露时才能缩小撤销范围。
多区域 active-active 若各自保存 nonce,重放可跨区成功。选择全局一致去重、按 keyId 固定路由或把 operationId 落入权威业务表;异步复制只能提供有窗口的防护,必须在安全承诺中说明。时钟窗口也不能替代 nonce,因为同一个合法包在窗口内仍完全有效。
观测分别统计签名格式错误、未知 key、过期、未来时间、nonce 重复、对象拒绝和幂等命中。把所有失败合成“验签失败”会丢失攻击与时钟故障的区别,但外部响应仍应克制,避免成为密钥与对象探针。
租户迁移是隔离模型的压力测试
迁移期间对象可能在源租户只读、目标租户预创建、索引和缓存尚未收敛。使用 migrationId 与对象版本定义权威归属,所有读写按当前阶段选择唯一租户,不能简单双写后允许两边管理员操作。切换前校验引用、ACL、对象存储前缀、搜索文档与异步任务,切换后拒绝旧归属版本。
代操作场景把 actor 与 effectiveSubject 分开记录:客服本人是谁、代表哪个用户、原因、批准、时限和允许动作。业务授权使用 effectiveSubject 的对象范围,再叠加 actor 的代操作权限;审计同时保留两者,不能把客服伪装成用户导致无法追责。
签名协议发布新规范化版本时,keyId 之外还要携带 protocolVersion。验证端先支持 v1/v2,调用方切换后观察旧版归零;两套规则独立实现和测试,不能“v2 验证失败就删除部分字段按 v1 再试”,否则攻击者可利用降级改变被签名语义。
安全状态机必须让失败停在安全位置
一次安全决策沿“解析—认证—授权—执行—审计”推进。任何阶段超时或证据不足都不能伪装成允许;同时要区分未认证、无权限、输入拒绝、依赖不可用和结果未知,避免客户端盲目重试。
反向验证要证明旁路也被关闭
测试不止提交非法字符串,还要更换租户、对象、HTTP 方法、内容类型、转发头、异步线程、重定向目标和旧版本凭证;在认证服务、审计存储、DNS、密钥源和策略缓存超时时观察默认行为。拒绝必须无业务副作用,错误响应不泄露资源存在性或内部实现。
容量同样属于安全边界。密码派生、签名验证、文件扫描、令牌内省和审计写入都可能被放大;为主体、租户、IP、操作和下游分别设置预算,并记录拒绝原因。限流不能只按可伪造 Header,也不能让攻击流量占满合法用户恢复通道。
生产证据与治理
指标聚合 authentication result、authorization decision、policy version、token age、replay reject、cross-tenant reject、outbound policy、upload quarantine、secret version 与 provenance result。主体和对象标识进入受控审计,不进入高基数指标;敏感值在采集源头删除,不靠下游二次脱敏。
上线门禁要求:威胁场景有 owner;允许和拒绝路径都有测试;默认拒绝与应急旁路已演练;秘密可轮换;审计可查询且防越权;依赖与构件来源可证明;旧版本兼容窗口和撤销路径明确。安全控制只有在故障、升级和应急状态仍成立时才算完成。
用两个 Java 17 模型固定安全不变量
模型不尝试实现密码算法或攻击载荷,只把控制输入、判定顺序和确定输出固化,真实系统再替换为框架、密钥服务与持久化策略。
javac --release 17 -Xlint:all -Werror examples/backend-development/security/tenant-signature-replay/ObjectAuthorizationDemo.java examples/backend-development/security/tenant-signature-replay/ReplayWindowDemo.java
java -cp examples/backend-development/security/tenant-signature-replay ObjectAuthorizationDemo
java -cp examples/backend-development/security/tenant-signature-replay ReplayWindowDemoprincipalTenant=north objectTenant=south role=ADMIN objectAllowed=false reason=TENANT_MISMATCH
signatureValid=true ageSeconds=12 withinWindow=true nonceFirstUse=false accepted=false reason=REPLAY输出变化必须对应一条明确策略或迁移决定;禁止为了让测试通过而放宽 issuer、audience、tenant、路径、算法或来源校验。
把控制成本写进容量与事故恢复
“客户端给出的租户和对象标识只能定位候选,不能直接成为授权结论”不是免费承诺。认证与密码派生消耗 CPU,远程策略和 Session 占用连接,签名与哈希增加计算,扫描和隔离消耗磁盘与队列,审计增加写放大。容量模型按峰值合法流量叠加恶意放大:每请求验证次数、下游往返、最大输入、并发隔离任务、失败重试、日志体积和安全状态保留期。平均延迟正常而拒绝队列持续增长,仍然意味着控制即将失效。
事故恢复按“阻止继续扩大—保存证据—撤销资格—修复权威状态—重建派生状态—验证不变量”推进。只修代码却不撤销 token、secret 或错误策略缓存,攻击窗口仍然存在;只轮换凭证却不检查已发生业务写,也无法证明资产恢复。对 ObjectAuthorizationDemo 与 ReplayWindowDemo 对应的决策输出建立线上指标或审计查询,才能把模型与生产证据连接起来。
升级时先部署严格兼容的读取端,再切换签发者或写入端,观察旧版本使用归零后删除兼容分支。任何临时兼容都记录 owner、截止、拒绝指标和回滚条件;为了兼容而跳过验证不属于迁移策略,而是新增旁路。
