Skip to content

fix(rest): 零声明字段的公开表单拒收提交,而不是接受调用方发的任意键 (#6920) - #7013

Merged
os-project-manager merged 4 commits into
mainfrom
claude/issue-6920-section-less-form-refusal
Aug 9, 2026
Merged

fix(rest): 零声明字段的公开表单拒收提交,而不是接受调用方发的任意键 (#6920)#7013
os-project-manager merged 4 commits into
mainfrom
claude/issue-6920-section-less-form-refusal

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6920

实现维护者裁决 5229989845(对称拒收):声明了零字段的公开表单不接受任何字段,与`` #6601 的读侧同一条规则。

复现(改动前,origin/main 真实注册的 handler,匿名)

sections: [] 的公开表单,发业务键 + 全套 FORGED 锚点:

status            = 201
createData calls  = 1
submit accepted   = ["email","internal_margin","internal_tier",
                     "not_even_a_field","status","subject"]

与卡上的测量逐字一致。注意这不是「对象的每个字段」——是调用方发的每一个键,减去 #3022 的锚点和三个原型键。not_even_a_field 根本不是该对象的字段。匿名访客因此可以设 status、内部分层、公式列……凡是该对象有的都行。进入这个状态不需要任何刁钻配置:作者先建表单、后接 sections,就是普通的编辑中间态。

改动

packages/rest/src/rest-server.ts(仅 submit 路由段)去掉过滤器里的 allowedFields.size === 0 || 限支,并在收集完 allowedFields 之后前置一道拒收:

400  { code: 'VALIDATION_ERROR',
       error: 'This form declares no fields, so it cannot accept a submission. …' }

判据是「声明」,不是 body。 空 body 同样被拒——按 body 判会让空提交正好穿过去插一条空记录,那正是本卡要消灭的形态。三种作者写法(sections: []、有 section 但不声明字段、完全省略 sections)一视同仁。

为什么是拒收而不是静默丢弃。 丢弃会保留 201、不改变任何 wire 状态,但会吞掉调用方以为写进去的数据——访客被告知工单已提交,库里存了一行空记录,正是 AGENTS.md 的 warn/error 规则点名的那类静默。而且响亮是唯一能传到作者耳朵里的答案,只有作者能修。

错误码为什么选 VALIDATION_ERROR

#6307 readSingleQueryValue 的同一条推理(见 package-routes.ts 的 “Where this module's error codes came from”):通用条件复用标准目录,而不是为它铸一个已注册的同义词。这里的条件是通用的 400 —— VALIDATION_ERROR 是 ADR-0112 目录里的 generic validation failure,也正是 HttpStatusErrorCodeMap[400] 给裸 400 的命名。没有新增 FORM_* 码,ERROR_CODE_LEDGER 不动。

消息点名空声明、给出作者的修法(把字段接进 sections),且不含对象名、字段名或 slug —— 这条回复是全互联网可读的。

重判 #3022 的钉子(裁决明确要求)

public-form-routes.test.tszero declared sections: business fields fall through, anchors do NOT 把 fall-through 钉为「预期行为」,并用一个被接受的未声明 status 来证明它。该用例已删除,原处留下注释写明依据裁决 5229989845 及为什么它是错的不变量。

#3022 的锚点那一半原样保留,并且加强了:

  • declared-field whitelist… 未动:表单甚至误声明 owner_id,整套 FORGED 仍被丢弃。
  • 新增 anchors alone on a DECLARED form:body 只有锚点、走接受路径,data{}
  • a __proto__ body key…buildServer([]) 重拼到声明式表单。原型污染这条性质从来与零 sections 无关——它此前只是搭了 fall-through 的便车。挪到声明路径后是更强的钉子,因为它守护的赋值循环真的会跑。
  • 零 sections 版本的锚点断言现在是空洞的(拒收什么都没插,「锚点不在插入内容里」对一个不存在的集合恒真),所以是替换而非重拼——并由 expect(createData).toHaveBeenCalledTimes(0) 接手。

反向验证(先写预测,后跑)

预测:还原原码(删拒收 + 恢复 allowedFields.size === 0 ||)→ 恰好 7 条零 sections 用例转红;两条 NO-REGRESSION 与全部 #3022/#6601 用例双向常绿

实测:Tests 7 failed | 14 passed (21),逐条吻合。方向是标准的 red,无反转。

两条 NO-REGRESSION 双向常绿,故如实标注为 guard(证明能用的表单没被改坏),不是本次修复的证据。

未写入已单独钉住,不止钉状态码:expect(createData, 'a refusal must not reach the data plane at all').toHaveBeenCalledTimes(0) —— 一个「拒收了但仍插空行」的实现比原缺陷更糟。

破坏面

这是把一条已发布的成功路径变成拒收,故 changeset 为 minor。今天靠零 sections 公开表单收集提交的部署会开始收到 400,修法是把字段声明进 sections。该表单的读侧自 #6601 起本就发布 fields: {}(渲染不出来),两个面现在执行同一条规则。已声明 sections 的表单完全不受影响——那条路径从不经过被删的限支。

验证

  • pnpm --filter @objectstack/rest test73 files / 1151 tests passed(改前 main 为 1132)
  • pnpm --filter @objectstack/rest typecheck → clean
  • eslint 两个改动文件 → clean
  • 门禁:check:route-envelope / check:error-code-casing / check:empty-changeset / check:nul-bytes / check:spec-parsed-alias / check:doc-authoring / check:wildcard-fallthrough 全 PASS

范围

rest-server.ts 只动 submit 路由的字段过滤段(外加 GET /forms/:slug 里一处指向被删限支的注释交叉引用,已失效)。#6599/meta/_drafts 段未触碰,也未改动任何共用测试文件。

content/docs/ui/forms.mdx 加了两行(一条规则说明 + 错误码表一行):该表就是这条路由的对外契约说明,新增错误码不加行会让文档在落地当刻即错。经 PM 裁定纳入(#6920 线程),且 git diff origin/main 自查确认 delta 仅为这两行。非 content/docs/releases/


Generated by Claude Code

@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 9, 2026 9:02am

Request Review

@github-actions github-actions Bot added the size/m label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest.

12 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/permissions/system-context.mdx (via packages/rest)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)
  • content/docs/releases/implementation-status.mdx (via @objectstack/rest)
  • content/docs/releases/v12.mdx (via @objectstack/rest)
  • content/docs/releases/v17.mdx (via @objectstack/rest)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 9, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 9, 2026 09:51
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit d586366 Aug 9, 2026
27 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6920-section-less-form-refusal branch August 9, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: a section-less public form accepts ANY non-anchor field from an anonymous visitor (the write-side twin of #6601)

2 participants