背景
修复跨租户 UPDATE 写(#2946 / Finding 1)时发现一个更广的完整性缺口:字段的 readonly: true 声明在服务端 update 时未被强制。
record-validator.ts(~L357/367)对 system/readonly 字段只是跳过校验,不拒绝其变更。
engine.ts(~L2397)update 时只剥离 readonlyWhen(条件只读)字段,从不剥离静态 readonly 字段。
- 结果:用户上下文的 update 可以把任意
readonly 字段的新值直接传进 driver.update。
#2946 只堵了 organization_id 的跨租户面(经 Layer 0 post-image 检查,让 org_id 在非平台用户上下文里事实不可变)。但其余所有 readonly 字段仍可被 update 覆盖——这是 org_id 之外的、更广的租内完整性面。
影响
严重性低于跨租户(这是租内字段篡改,不跨租户),但仍是完整性漏洞:任何声明了 readonly 的字段(审计戳、provenance、系统计算值等)可被普通用户经 data API 的 update 覆盖,除非另有 FLS/护栏专门保护该字段。
建议
在引擎/安全层统一强制静态 readonly:用户上下文(非 system)的 update 若显式改动一个 readonly 字段,剥离该变更或 fail-closed 拒绝(与 readonlyWhen 的剥离对称)。需核实:
- 哪些「合法」写路径依赖覆盖 readonly(应走 system context);
- 与 FLS
allowEdit:false 的关系(是否已有部分覆盖);
- 是否需要区分「剥离静默」vs「显式拒绝」(倾向剥离,兼容性更好)。
关联
#2946(org_id 跨租户面已堵)· ADR-0092(身份表写守卫,相关但不同层)· tracking #2920
发现于 #2920 的安全审查。
背景
修复跨租户 UPDATE 写(#2946 / Finding 1)时发现一个更广的完整性缺口:字段的
readonly: true声明在服务端 update 时未被强制。record-validator.ts(~L357/367)对 system/readonly 字段只是跳过校验,不拒绝其变更。engine.ts(~L2397)update 时只剥离readonlyWhen(条件只读)字段,从不剥离静态readonly字段。readonly字段的新值直接传进driver.update。#2946 只堵了
organization_id的跨租户面(经 Layer 0 post-image 检查,让 org_id 在非平台用户上下文里事实不可变)。但其余所有readonly字段仍可被 update 覆盖——这是 org_id 之外的、更广的租内完整性面。影响
严重性低于跨租户(这是租内字段篡改,不跨租户),但仍是完整性漏洞:任何声明了
readonly的字段(审计戳、provenance、系统计算值等)可被普通用户经 data API 的 update 覆盖,除非另有 FLS/护栏专门保护该字段。建议
在引擎/安全层统一强制静态
readonly:用户上下文(非 system)的 update 若显式改动一个readonly字段,剥离该变更或 fail-closed 拒绝(与readonlyWhen的剥离对称)。需核实:allowEdit:false的关系(是否已有部分覆盖);关联
#2946(org_id 跨租户面已堵)· ADR-0092(身份表写守卫,相关但不同层)· tracking #2920
发现于 #2920 的安全审查。