现象
非 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 无写保护」)。
现象
非 system 上下文的普通 REST 调用者可以自由指定和改写 autonumber 字段的值:
POST /data/:object携带显式 autonumber 值 → 被原样落库(序列被绕过);PATCH /data/:object/:id携带 autonumber 字段 → 200 且改写落库(业务单号被篡改)。而引擎自己的文档说得很清楚(
packages/objectql/src/engine.tsapplyAutonumbers注释):"Required-validation exemptsautonumbereither way, so arequiredrecord number is never rejected for 'missing' — the runtime owns the value, not the client." —— 运行时声称拥有这个值,但写路径没有任何一层阻止客户端提交/改写它。复现步骤(HotCRM 17.0-rc2 验收环境,admin token,SQLite file 驱动)
两次独立运行均复现;在 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 实际
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.tsstripReadonlyFields——if (!def?.readonly) continue;,按字段级readonly标记剥离,type === 'autonumber'不参与;packages/objectql/src/engine.tsapplyAutonumbers(约 1829-1885)—— 只在值为空时生成,显式值原样保留;SQL 驱动supports.autonumber路径实测同样采纳调用者显式值(上面复现 1)。影响
created_at可被普通 PATCH 伪造)同一缺陷族;crm_case声明了(organization_id, case_number)复合唯一),伪造会产生静默重号;建议方向
写路径将
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 无写保护」)。