Skip to content

安全:静态字段 readonly: true 仅 UI 层生效、服务端不强制 → 审批/状态/金额字段可被直接 PATCH 绕过写入(FLS 缺口) #3003

Description

@baozhoutao

概述

字段声明为静态 readonly: true 时,该限制只在 UI 层生效,服务端不会从更新载荷里剥离该字段。因此任何能 update 该记录的用户,都可以用一次直接的 REST PATCH 写入这个字段。凡是依赖 readonly: true 来保护「审批状态 / 审批节点 / 金额」等字段完整性的对象,都会被绕过。

对比:readonlyWhen: <CEL>服务端强制的——命中条件的字段在更新时被静默剥离(HTTP 200,值不变)。两者不一致。

  • 框架版本:15.0.0
  • 受影响对象的共享模型:public_read_write(默认 OWD)

影响(真机复现)

对象 sporadic_application(零星派工申请单)带 4 级审批流。approval_statusapproval_stage 声明为 readonly: true;confirmed_total 是金额字段。

通过 Console UI + 同一登录会话内的浏览器 fetch(同一个已登录 worker 的 cookie,非管理员,isSystem=false)端到端复现:

  1. worker 在 Console UI 新建草稿。创建表单根本不渲染 approval_status / confirmed_total——UI 层看起来受保护,唯一合法动作是「提交审批」按钮。记录停在 draft
  2. 在同一登录会话的浏览器里直接发:
    PATCH /api/v1/data/sporadic_application/<id>
    { "approval_status": "approved", "approval_stage": "approved",
      "confirmed_total": 99999 }
    → HTTP 200
    
  3. 刷新详情页:状态显示审批通过,流水线跳到终节点,confirmed_total = 99,999.00,并冒出下游「派工」动作按钮。

最终效果:申请人自我批准、完全跳过 4 级审批,并自定结算金额。

系统性——在第二个对象上用同样手法坐实:assessment(考核单,3 级签核,assessment_status 同样是 readonly: true)——PATCH { assessment_status:'approved', approval_stage:'approved' } → 200,dept_head → gm → finance 三级全跳过。凡是依赖 readonly: true 保护审批 / 状态 / 金额 / 判定字段的对象,一律中招。

补充:RECORD_LOCKED 只在审批流**活动态(pending)**时拦直接 PATCH。记录仍在 draft(流从未启动)时不会被锁,而 readonly: true 又只是 UI 层——所以 draft → approved 是一步直跳。

期望行为

readonly: true 的字段级写入应当服务端强制,和 readonlyWhen 字段在更新时被剥离的方式一致(insert 豁免)。即:对非特权写入者,静态 readonly: true 字段应在更新载荷里被静默丢弃。

线索 / 指引

  • readonlyWhen 已有服务端剥离路径(在 beforeUpdate 之后、基于合并的 {...previous, ...data} 求值,insert 豁免)。不一致点在于:静态 readonly: true 没有走同一条剥离路径。
  • 我们应用侧的临时绕行:把 readonly: true 改成 readonlyWhen,或加一个高优先级 beforeUpdate 守卫,在 !ctx.isSystem 时剥离这些受保护字段。

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingsecurity

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions