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_normalized、line_number、is_closed、seed_key、view_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_number、autonumber overwrite/writable/forge,两仓 open+closed 均无本缺陷(#5495/#698 是序列计数器不同步问题,#4903 是 readonly 剥离与 hook 补写不对称,均不覆盖「autonumber 无写保护」)。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions