在 #4757 的「同形状排查」中发现。未认领,归属 plugin-sharing,不在 #4757 的 PR 范围内。
现象
packages/plugins/plugin-sharing/src/rule-hooks.ts 的 bindRuleHooks 为每个有共享规则的对象绑定 afterInsert / afterUpdate,处理函数第一步就靠单条记录 id 定位要重算的行:
const data = ctx?.result ?? ctx?.input?.data ?? {};
const id = String((data as any)?.id ?? ctx?.input?.id ?? '');
if (!id) return;
await service.evaluateAllForRecord(objectName, id, SYSTEM_CTX as any);
packages/objectql/src/engine.ts 的 update() 只在 where.id 是标量时提取 input.id;谓词式(multi: true)更新走 updateMany,input.id 为 undefined,input.data 里通常也没有 id,ctx.result 是受影响行数而非行本身。于是批量更新的每一行都不会重算共享规则。
后果与 #4757 同向 —— 是授权侧的 fail open,只是路径更隐蔽:
- 一条基于 criteria 的共享规则给记录发过
sys_record_share 行;
- 管理员用
multi: true 批量把这些记录改成不再匹配该规则的状态(改 owner、改 stage、改 region…);
- 重算没有发生,
sys_record_share 行原样留在表里 → 本应失去访问权的用户继续能读/能改这些记录。
反向也一样(批量改成匹配规则却没发出共享),但那一侧是「少给权限」,危害小得多。
复现方向
对某对象定义一条 criteria 共享规则,插入命中的记录(hook 正常发共享),然后:
await ql.update(obj, { where: { region: 'east' }, multi: true, data: { region: 'west' }, context: adminCtx });
再查 sys_record_share:原来的共享行仍在,evaluateAllForRecord 一次都没被调用。
建议修法
把「单条 id」的入口扩成「本次写入匹配的行集合」:在 beforeUpdate 阶段用谓词(带上界,超限告警或失败关闭)解析出受影响的 id 列表并暂存到 hook ctx(primary-bu-projection.ts 的 STASH_KEY 就是这个模式),afterUpdate 再逐行 evaluateAllForRecord。上界参考 attachment / comment 守卫的 1000 行。
需要维护者裁定的一点:超过上界时该怎么办 —— 是拒绝这次批量写入(fail closed,与 #4757 的姿势一致),还是放行但记一条显式告警并标记重算待办(共享规则重算本身是异步可补偿的)。这决定了它算「守卫」还是「投影」,建议在动手前先定。
相关
在 #4757 的「同形状排查」中发现。未认领,归属
plugin-sharing,不在 #4757 的 PR 范围内。现象
packages/plugins/plugin-sharing/src/rule-hooks.ts的bindRuleHooks为每个有共享规则的对象绑定afterInsert/afterUpdate,处理函数第一步就靠单条记录 id 定位要重算的行:packages/objectql/src/engine.ts的update()只在where.id是标量时提取input.id;谓词式(multi: true)更新走updateMany,input.id为 undefined,input.data里通常也没有 id,ctx.result是受影响行数而非行本身。于是批量更新的每一行都不会重算共享规则。后果与 #4757 同向 —— 是授权侧的 fail open,只是路径更隐蔽:
sys_record_share行;multi: true批量把这些记录改成不再匹配该规则的状态(改 owner、改 stage、改 region…);sys_record_share行原样留在表里 → 本应失去访问权的用户继续能读/能改这些记录。反向也一样(批量改成匹配规则却没发出共享),但那一侧是「少给权限」,危害小得多。
复现方向
对某对象定义一条 criteria 共享规则,插入命中的记录(hook 正常发共享),然后:
再查
sys_record_share:原来的共享行仍在,evaluateAllForRecord一次都没被调用。建议修法
把「单条 id」的入口扩成「本次写入匹配的行集合」:在
beforeUpdate阶段用谓词(带上界,超限告警或失败关闭)解析出受影响的 id 列表并暂存到 hook ctx(primary-bu-projection.ts的STASH_KEY就是这个模式),afterUpdate再逐行evaluateAllForRecord。上界参考 attachment / comment 守卫的 1000 行。需要维护者裁定的一点:超过上界时该怎么办 —— 是拒绝这次批量写入(fail closed,与 #4757 的姿势一致),还是放行但记一条显式告警并标记重算待办(共享规则重算本身是异步可补偿的)。这决定了它算「守卫」还是「投影」,建议在动手前先定。
相关
installAttachmentAccessHooksdoes not authorize an UNSCOPED multi-delete: no id + nowherereads as "nothing to authorize" anddeleteManyruns over the whole table #4757 ——sys_attachment的beforeDelete无作用域多行删除(同一 fail-open 家族,已修)。bindApprovalLockHook对谓词式(multi)更新完全失效:if (!id) return把「没解析到行」当成「允许」 #4778 —— 审批记录锁对谓词式更新失效(同一排查)。rule-hooks只绑了afterInsert/afterUpdate,没有afterDelete:记录删除后sys_record_share行会成为孤儿。今天危害有限(记录已不存在),但若 id 可复用就是真实的越权,一并记在这里。