Skip to content

fix(rest): 公开表单发布的 schema 就是它声明的字段集,不再是「一个都没声明就发全部」(#6601) - #6933

Merged
os-project-manager merged 4 commits into
mainfrom
claude/issue-6601-public-form-schema-narrowing
Aug 9, 2026
Merged

fix(rest): 公开表单发布的 schema 就是它声明的字段集,不再是「一个都没声明就发全部」(#6601)#6933
os-project-manager merged 4 commits into
mainfrom
claude/issue-6601-public-form-schema-narrowing

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#6601

问题

GET /api/v1/forms/:slug(packages/rest/src/rest-server.ts)会把目标对象的 schema 一并内嵌进应答,好让匿名前端不必再走一次需要鉴权的 /meta 就能渲染表单。收窄依据是表单 sections 声明的字段集合,但判断写的是:

if(allowed.size===0||allowed.has(name)){fields[name]=def;}

allowed.size === 0 —— 表单没有 sections,或者 sections 一个字段都没声明 —— 落到「发布该对象每一个非 server-managed 字段」这一支。这条路由是匿名的,发出去的是完整字段定义:label、type、picklist 的选项值、formula 表达式。

开工前逐条核对了 issue 的三个前提(origin/main@970bb4c8,rest-server.ts:7115-7127),全部成立:

  1. 「表单先建、sections 之后再配」是正常的编写中间态,不是刁钻配置;
  2. 代码块上方注释里的 "limited to fields referenced by the form" 在这一支上不成立;
  3. 同一条注释提到的 "submit handler still enforces the field whitelist server-side" 是侧防线,挡不住侧披露。

也核对了 issue 关于 safeForm 的说法:safeForm 只重写表单自己的 sections(return { ...match.form, sections }),而 objectSchema 是同一份应答上并列的另一个键,从不被它收窄 —— 属实。

裁决的那道测量门

Issue 给了两个方向,派发裁决优先第二个(把披露面收窄成 submit 路由真正会接受的字段集,让读写两面按构造相等),但附了一道测量门:只有当 submit 对无 sections 表单的接受集比「全部」更窄时,这条路才有意义。

测得:不更窄。rest-server.ts:7259 是同一处退化:

if(allowedFields.size===0||allowedFields.has(k))filteredData[k]=v;

对一个无 sections 的公开表单实测(未修改代码,匿名 POST 一个含全部业务字段 + 一个对象上根本不存在的键的 body):

GATE submit accepted = ["email","internal_margin","internal_tier","not_even_a_field","status","subject"]

写面接受的是「除 server-managed 锚点与三个原型键之外的任何 body 键」,是「对象全部字段」的超集。所以把读面对齐到写面,发布出去的正是今天这一份 —— 该方案在这个 case 上是空转的。测量门触发,按裁决落到 fallback。

方案:发布是一次声明

-if(allowed.size===0||allowed.has(name)){-fields[name]=def;-}+if(!allowed.has(name))continue;+fields[name]=def;

一个字段都没声明的表单,objectSchema.fields 就是 {}

  • 没有新增任何可编写的键。 按裁决与 ADR-0049,不做「整对象发布」的显式 opt-in 开关 —— 那是 packages/spec 上一个没有需求牵引的新可编写面。真有这个场景,带着真实用例来提。
  • 信封形状不变。objectSchema 仍是 { name, label, fields },不会变成 null;object / label / form 都不变。已经正常声明 sections 的表单,应答逐字节不变。
  • PUBLIC_FORM_SERVER_MANAGED_FIELDS 的处理(安全:公开表单(publicFormGrant)提交绕过 owner_id 属主守卫 → 匿名可伪造属主 #3022)完全未动;POST /forms/:slug/submitGET /forms/:slug/lookup/:field 未动。

实测:修复前 / 修复后

fixture:ticket 对象,除业务字段外加了两个匿名调用者不该知道的字段 —— internal_tier(select,携带运营分类的选项值)与 internal_margin(formula,携带表达式);公开 FormView sections: [];匿名 GET /forms/test预测在跑之前已写下

修复前:

BEFORE zero-sections published = ["email","internal_margin","internal_tier","status","subject"]
BEFORE internal_tier.options = [{"label":"Strategic","value":"strategic"},{"label":"Churn Risk","value":"churn_risk"}]
BEFORE internal_margin.formula = "(amount - cost) / amount * 0.87"

修复后:

AFTER zero-sections published = []
AFTER internal_tier.options = null
AFTER internal_margin.formula = null

声明了 sections 的表单,两侧都是 ["email","subject"] —— 未变。

反向验证

预测方向写在跑之前:(被守护的是一个键集合,不是计数,fixture 在两个方向上都是 spec-valid,所以既不是「诊断变多」也不是「反转」那两类)。把 allowed.size === 0 || 放回去后实测:12 条里 4 条零声明 pin 全红,含 2 条 NO-REGRESSION 在内的 8 条全绿。方向与预测一致。

关于 pin 的一处如实汇报

Issue 说 allowed.size === 0 这一支「currently has no test asserting what it publishes」—— 这一句不成立public-form-routes.test.ts 原第 145-151 行的
zero declared sections: the all-fields schema expansion excludes managed anchors 就在断言它发布什么,断言值是 ['email','status','subject']。也就是说这一支不是没被 pin,而是被 pin 成了正确的:它从 #3022 的角度只检查锚点没混进来,顺带把 all-fields 展开钉成了预期行为。本 PR 翻转的正是这条断言,替换处留了注释说明覆盖没有被删(锚点性质在 submit 路由上仍有自己的零声明 pin,那里它才真正起作用)。

新增的 pin 在 GET /forms/:slug — the published schema IS the declared field set (#6601):零声明 / 空 sections / 省略 sections 三种形状各一条,选项值与表达式「不在整份 wire 上出现」一条,外加两条 NO-REGRESSION。后两条在修改前后都是绿的 —— 它们是「没弄坏已声明表单」的护栏,不是本次修复的证据,这里如实标注。

验证

pnpm --filter @objectstack/rest test → Test Files 72 passed (72) | Tests 1131 passed (1131)
npx tsc --noEmit -p packages/rest/tsconfig.json → exit 0
node scripts/check-nul-bytes.mjs → OK (6428 files, no raw control bytes)
node scripts/check-route-envelope.mjs → exit 0

已 merge origin/main(合并后重跑上述全部,均绿)。

顺手记录、未在本 PR 修的

两条都按 PD #10 单独立卡、未指派,不在本 PR 动手。

🤖 Generated with Claude Code

https://claude.ai/code/session_017uFVNMmTxLpmfQYiuKM1Yx


Generated by Claude Code

@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 9, 2026 4:35am

Request Review

@github-actions

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-actionsgithub-actionsBot added size/m 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 05:28
@os-project-manager
os-project-manager added this pull request to the merge queueAug 9, 2026
Merged via the queue into main with commit 54fe9d5Aug 9, 2026
26 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6601-public-form-schema-narrowing branch August 9, 2026 05:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: a section-less public form publishes EVERY field of its target object to anonymous callers

2 participants

@os-project-manager@claude