背景
在 B1(ADR-0095 D1,PR #2933)落地 Layer 0 期间核实写路径时发现一处既有(非 B1 引入)的跨租户写缺口。B1 的 Layer 0 门控读 + update/delete 的 pre-image,但不门控 insert 的 post-image,因此没有顺带堵上它。单独立项跟踪,避免夹带进行为保持的 B1。
缺口
多组织模式(tenancy.mode='multi' + @objectstack/organizations)下,一个普通用户可以 insert 一条伪造 organization_id(指向别的租户)的业务记录,使其落进受害租户的数据里。三个条件叠加:
organization_id 是普通可写 lookup 字段(Field.lookup('sys_organization', …)),用户 insert 时可直接提供,未被系统字段守卫剥离。
- 企业版 auto-stamp 只在缺省时填充、不覆盖:
cloud/packages/organizations/src/organizations-plugin.ts:147
if (data.organization_id == null || data.organization_id === '') {
data.organization_id = opCtx.context.tenantId;
}
用户提供了伪造值 → 非空 → auto-stamp 不覆盖。
- 写侧从无租户 post-image 检查:
computeWriteCheckFilter(plugin-security/src/security-plugin.ts:1981)只纳入显式带非空 check 子句的策略;种子 tenant_isolation 只有 using 无 check,故从不参与写检查。
结果:伪造 organization_id 的 insert 既不被 auto-stamp 纠正,也不被任何租户写检查拒绝。Layer 0 的 AND 过滤器作用于读和 update/delete 的 pre-image 匹配,但 insert 无 pre-image,未被门控。
影响
- 仅多组织(企业版)部署受影响;单组织/物理隔离(env-per-DB, ADR-0002)不受影响。
- 攻击者可向别的租户注入行(数据污染 / 潜在的钓鱼或配额攻击);注入的行对攻击者自己反而不可见(Layer 0 读过滤会隐藏它)。
- open-core dogfood 跑不到多组织路径(需企业版
@objectstack/organizations),故自动化回归门未覆盖——需在企业版环境验证。
建议修法(是 net-new delta,需独立评审)
两处配合,任一单独都不足:
- Layer 0 增加 insert post-image 租户检查:insert 时校验
organization_id == ctx.organization_id(platform-admin 姿态 + 对象 posture 允许时豁免,与 D1 读侧同规则)。落点 plugin-security/src/tenant-layer.ts + computeWriteCheckFilter 接线。
- 企业版 auto-stamp 改为权威覆盖(或在 stamp 后校验):
organizations-plugin.ts Middleware A 对租户对象始终用 ctx.tenantId 覆盖 organization_id,而非仅填充缺省。需确认是否有合法「代客设置 org_id」的 insert 场景(import/admin 工具、跨组织迁移)——若有,这些走 system context 或专用端点,不受用户上下文的覆盖影响。
注意这会引入一处新的行为 delta(此前能成功的「带 org_id 的用户 insert」会被拒正或被覆盖),需像 ADR-0095 的四处 delta 一样进 role×object×insert 矩阵门并写 release-notes callout。
关联
发现于 tracking #2920 的 B 组内核链开发过程中。
背景
在 B1(ADR-0095 D1,PR #2933)落地 Layer 0 期间核实写路径时发现一处既有(非 B1 引入)的跨租户写缺口。B1 的 Layer 0 门控读 + update/delete 的 pre-image,但不门控 insert 的 post-image,因此没有顺带堵上它。单独立项跟踪,避免夹带进行为保持的 B1。
缺口
多组织模式(
tenancy.mode='multi'+@objectstack/organizations)下,一个普通用户可以insert一条伪造organization_id(指向别的租户)的业务记录,使其落进受害租户的数据里。三个条件叠加:organization_id是普通可写 lookup 字段(Field.lookup('sys_organization', …)),用户 insert 时可直接提供,未被系统字段守卫剥离。cloud/packages/organizations/src/organizations-plugin.ts:147computeWriteCheckFilter(plugin-security/src/security-plugin.ts:1981)只纳入显式带非空check子句的策略;种子tenant_isolation只有using无check,故从不参与写检查。结果:伪造
organization_id的 insert 既不被 auto-stamp 纠正,也不被任何租户写检查拒绝。Layer 0 的 AND 过滤器作用于读和 update/delete 的 pre-image 匹配,但 insert 无 pre-image,未被门控。影响
@objectstack/organizations),故自动化回归门未覆盖——需在企业版环境验证。建议修法(是 net-new delta,需独立评审)
两处配合,任一单独都不足:
organization_id == ctx.organization_id(platform-admin 姿态 + 对象 posture 允许时豁免,与 D1 读侧同规则)。落点plugin-security/src/tenant-layer.ts+computeWriteCheckFilter接线。organizations-plugin.tsMiddleware A 对租户对象始终用ctx.tenantId覆盖organization_id,而非仅填充缺省。需确认是否有合法「代客设置 org_id」的 insert 场景(import/admin 工具、跨组织迁移)——若有,这些走 system context 或专用端点,不受用户上下文的覆盖影响。注意这会引入一处新的行为 delta(此前能成功的「带 org_id 的用户 insert」会被拒正或被覆盖),需像 ADR-0095 的四处 delta 一样进
role×object×insert矩阵门并写 release-notes callout。关联
==policies (extractTargetField only parses=/IN) #2936(extractTargetField只认=不认==的盲区,同属租户写路径 hardening)发现于 tracking #2920 的 B 组内核链开发过程中。