fix(security): 公开表单不可再伪造服务端托管锚点(owner_id 等)(#3022)#3036
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 102 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
匿名公开表单(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):id、owner_id、organization_id、tenant_id、created_at/created_by/updated_at/updated_by、is_deleted/deleted_at、__search。仅约束匿名公开表单面——已认证写路径语义不变(insert 播种 readonly、转移授权等仍按 #3018 规则)。plugin-security的publicFormGrant分支在放行 insert 前对每一行(含批量)剥离托管锚点,并 warn 记录。无论任何路由放过什么,伪造都到不了驱动;属主留空交由对象 hook / 首管理员 bootstrap 认领,与其它匿名种子行一致。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 上下文)。showcase-public-form.dogfood.test.ts新增伪造owner_id/organization_id/created_by的匿名提交用例(201 但锚点未落库)。public-form-managed-anchors行固定为 enforced。pnpm build71/71。content/docs/ui/public-data-collection.mdx补充托管锚点边界说明。关联
🤖 Generated with Claude Code
https://claude.ai/code/session_01Lr7QwD4Fx9KYkLH1xDMwEo
Generated by Claude Code