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

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 16, 2026 10:46am

Request Review

@github-actionsgithub-actionsBot 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
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.

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

2 participants

@os-zhuang@claude