Skip to content

fix(security): 公开表单不可再伪造服务端托管锚点(owner_id 等)(#3022)#3036

Merged
os-zhuang merged 4 commits into
mainfrom
claude/public-form-owner-bypass-ds4orf
Jul 16, 2026
Merged

fix(security): 公开表单不可再伪造服务端托管锚点(owner_id 等)(#3022)#3036
os-zhuang merged 4 commits into
mainfrom
claude/public-form-owner-bypass-ds4orf

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

背景

匿名公开表单(ADR-0056 Option A,POST /forms/:slug/submit)由声明派生的 publicFormGrant 授权,而该 grant 在安全中间件最前段return next()——CRUD/FLS/属主守卫(#3018 的 step 3.5)/租户 CHECK 全部不执行。字段侧唯一防线是路由的"已声明字段白名单",但表单声明零个 section 字段时会回退为 Object.assign(filteredData, rawBody) 原样合并。于是未认证访客可以 POST owner_id=<victim>(以及 organization_id/审计列/id)把记录挂到受害者名下或落到别的租户——正是 #3004 的 insert-forge,但无需任何凭证

修复

采纳 issue 的两个可选方案并同时落地(单一共享定义,双层执法,防止漂移):

共享定义(@objectstack/spec/security 新增 PUBLIC_FORM_SERVER_MANAGED_FIELDS):idowner_idorganization_idtenant_idcreated_at/created_by/updated_at/updated_byis_deleted/deleted_at__search。仅约束匿名公开表单面——已认证写路径语义不变(insert 播种 readonly、转移授权等仍按 #3018 规则)。

  • 数据层(权威边界,方案 1)plugin-securitypublicFormGrant 分支在放行 insert 前对每一行(含批量)剥离托管锚点,并 warn 记录。无论任何路由放过什么,伪造都到不了驱动;属主留空交由对象 hook / 首管理员 bootstrap 认领,与其它匿名种子行一致。
  • 路由层(方案 2) — submit 白名单无条件排除托管集:显式声明的 owner_id 不再放行;零声明 fallback 保留业务字段直通的既有约定但拒绝托管集。GET /forms/:slug 的 schema 展开与渲染 sections 同步剔除(表单不再收集 submit 必拒的值);GET /forms/:slug/lookup/:field 拒绝声明在托管锚点上的 publicPicker(否则等于向互联网开放匿名 sys_user 搜索)。顺带关闭 __proto__/constructor/prototype 键的原型走私(JSON.parse 产生的自有 __proto__ 键经 obj[k]=v 赋值会替换原型、让锚点以继承属性绕过 own-property 检查)。

验证

  • 单元:plugin-security/security-plugin.test.ts 新增 publicFormGrant 剥离矩阵(单条/批量/干净提交不受扰/非授权操作仍拒);rest/public-form-routes.test.ts 新增路由级矩阵(声明白名单+托管排除、零声明 fallback、__proto__ 走私、schema/sections 一致性、lookup 拒绝、grant 上下文)。
  • 真机 dogfood:showcase-public-form.dogfood.test.ts 新增伪造 owner_id/organization_id/created_by 的匿名提交用例(201 但锚点未落库)。
  • 账本:authz-conformance 新增 public-form-managed-anchors 行固定为 enforced。
  • 回归:spec(6864)、plugin-security(445)、rest(282)、dogfood public-form/form-self-auth/anonymous-deny/authz-conformance 全绿;pnpm build 71/71。
  • 文档:content/docs/ui/public-data-collection.mdx 补充托管锚点边界说明。

关联

🤖 Generated with Claude Code

https://claude.ai/code/session_01Lr7QwD4Fx9KYkLH1xDMwEo


Generated by Claude Code

…aged anchors (#3022)

The anonymous public-form surface (ADR-0056 Option A) is authorized by the
declaration-derived publicFormGrant, which short-circuits the security
middleware BEFORE every write gate (CRUD, FLS, the owner anchor guard, the
tenant CHECK). The only field-side defense was the submit route's
declared-field allow-list — and a FormView with zero declared section fields
fell back to merging the raw body wholesale, so an unauthenticated visitor
could POST owner_id=<victim> (or organization_id / audit columns / id) and
attach the record to another user or tenant: the #3004 insert-forge with no
credentials at all.

Enforce the server-managed anchors on this surface at BOTH layers from one
shared definition (PUBLIC_FORM_SERVER_MANAGED_FIELDS, new in
@objectstack/spec/security):

- plugin-security: the publicFormGrant branch strips the managed set from
  every row of a granted insert (batch included) before admitting the write —
  the data-layer boundary that holds no matter what any route lets through.
- rest: the submit allow-list excludes the set unconditionally (an explicitly
  declared owner_id no longer passes; the zero-sections fallback keeps its
  all-fields behavior for business columns only), the resolve route drops the
  fields from rendered sections + the embedded schema, and the lookup route
  refuses a publicPicker declared on a managed anchor (would open anonymous
  sys_user search). Dunder body keys (__proto__/constructor/prototype) are
  skipped so a JSON-parsed own __proto__ key cannot smuggle inherited anchors
  past own-property checks.

Proofs: publicFormGrant strip matrix in security-plugin.test.ts; route-level
matrix in public-form-routes.test.ts; end-to-end forged-owner submit in
showcase-public-form.dogfood.test.ts; authz-conformance ledger row
public-form-managed-anchors pins the boundary as enforced.

Closes #3022

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lr7QwD4Fx9KYkLH1xDMwEo
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 16, 2026 10:46am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/dogfood, @objectstack/plugin-security, @objectstack/rest, @objectstack/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via packages/dogfood, packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/dogfood)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)

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.

…ace snapshot (#3022)

The lint.yml api-surface gate flags any public-API change; the new export is
the intentional addition from the #3022 fix, so regenerate and commit the
snapshot per the gate's instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lr7QwD4Fx9KYkLH1xDMwEo
Integrates PR #3018 (owner_id anchor guard + bulk write scoping, #3004/#2982).
Conflict: authz-conformance.matrix.ts — kept both sides' new ledger rows
(ownership-anchor-guard, bulk-write-owner-scoping, public-form-managed-anchors).
api-surface.json regenerated post-merge (check passes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lr7QwD4Fx9KYkLH1xDMwEo
@os-zhuang os-zhuang marked this pull request as ready for review July 16, 2026 11:02
@os-zhuang os-zhuang merged commit aaec5db into main Jul 16, 2026
17 checks passed
@os-zhuang os-zhuang deleted the claude/public-form-owner-bypass-ds4orf branch July 16, 2026 11:02
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.

安全:公开表单(publicFormGrant)提交绕过 owner_id 属主守卫 → 匿名可伪造属主

2 participants