Skip to content

[17.0-rc2验收] autonumber 字段可被普通调用者改写:POST 提交显式值绕过序列、PATCH 直接改号落库 —— readonly 剥离不保护 type:'autonumber' #5503

Description

@yinlianghui

现象

非 system 上下文的普通 REST 调用者可以自由指定和改写 autonumber 字段的值:

  • POST /data/:object 携带显式 autonumber 值 → 被原样落库(序列被绕过);
  • PATCH /data/:object/:id 携带 autonumber 字段 → 200 且改写落库(业务单号被篡改)。

而引擎自己的文档说得很清楚(packages/objectql/src/engine.ts applyAutonumbers 注释):"Required-validation exempts autonumber either way, so a required record number is never rejected for 'missing' — the runtime owns the value, not the client." —— 运行时声称拥有这个值,但写路径没有任何一层阻止客户端提交/改写它。

复现步骤(HotCRM 17.0-rc2 验收环境,admin token,SQLite file 驱动)

# 1) POST 显式提交 autonumber → 被采纳,序列绕过
curl -X POST :4092/api/v1/data/crm_account -H "Authorization: Bearer $TOK" \
  -d '{"name":"AN forge","account_number":"ACC-777777"}'
# → 201, record.account_number = "ACC-777777"

# 2) PATCH 改写既有单号 → 200,回读已变
curl -X POST :4092/api/v1/data/crm_account -d '{"name":"AN1"}'   # → ACC-000012
curl -X PATCH :4092/api/v1/data/crm_account/<id> -d '{"account_number":"ACC-888888"}'  # → 200
curl :4092/api/v1/data/crm_account/<id>   # → account_number = "ACC-888888"

# 3) 同样在 crm_product 复现
curl -X PATCH :4092/api/v1/data/crm_product/<id> -d '{"product_code":"PRD-8888"}'  # → 200,回读 PRD-8888

两次独立运行均复现;在 HotCRM 的 7 个对象上逐一验证了 PATCH 改写(account_number / product_code / campaign_code / member_number / contract_number / quote_number / attendee_number),机制上同样适用于 crm_case.case_number(8 个 autonumber 字段全部暴露)。

对照:同批测试中所有真正声明 readonly: true 的字段(name_normalizedline_numberis_closedseed_keyview_count…)PATCH 均被正确剥离(200 但值不变)——剥离机制本身工作正常,只是 autonumber 不在保护范围内。

期望 vs 实际

  • 期望:autonumber 是运行时生成的业务标识,非 system 写入中的调用者提交值应像 readonly 字段一样被剥离(insert 与 update 都是);
  • 实际:insert 端 applyAutonumbers 显式 "respect explicit value"(if (current != null && current !== '') continue),SQL 驱动路径同样采纳显式值;update 端 stripReadonlyFields 只看 def.readonly,autonumber 字段没有该标记,直接放行。

落点分析

  • packages/spec/src/data/field.zod.ts:895 —— autonumber: (config) => ({ type: 'autonumber', ...config }),builder 不隐含 readonly: true;
  • packages/objectql/src/validation/rule-validator.ts stripReadonlyFields —— if (!def?.readonly) continue;,按字段级 readonly 标记剥离,type === 'autonumber' 不参与;
  • packages/objectql/src/engine.ts applyAutonumbers(约 1829-1885)—— 只在值为空时生成,显式值原样保留;SQL 驱动 supports.autonumber 路径实测同样采纳调用者显式值(上面复现 1)。

影响

建议方向

写路径将 type: 'autonumber' 视为隐含 readonly(非 system 上下文一律剥离,insert 与 update 同权),或在 spec 层为 autonumber builder 注入 readonly: true 并让 applyAutonumbers / 驱动序列继续豁免 system 写(import/seed 的 preserveAudit 类场景可沿用现有白名单机制)。

环境

hotcrm@0899b4f + @objectstack 17.0.0-rc.2(pnpm exec objectstack dev,file:SQLite,--seed-admin)

查重:搜过 autonumber(24 条)、readonly autonumber/account_number/quote_numberautonumber overwrite/writable/forge,两仓 open+closed 均无本缺陷(#5495/#698 是序列计数器不同步问题,#4903 是 readonly 剥离与 hook 补写不对称,均不覆盖「autonumber 无写保护」)。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions