Skip to content

fix(security): 守卫 owner_id 属主锚点 + bulk 写按属主收敛 (#3004, #2982)#3018

Merged
os-zhuang merged 2 commits into
mainfrom
claude/owner-id-security-fix-m36705
Jul 16, 2026
Merged

fix(security): 守卫 owner_id 属主锚点 + bulk 写按属主收敛 (#3004, #2982)#3018
os-zhuang merged 2 commits into
mainfrom
claude/owner-id-security-fix-m36705

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

背景

owner_id 是行级属主的锚点——OWD 行级作用域据此判定 update/delete 是否属主。它故意不是 readonly(属主可转移,见 registry.ts applySystemFields),因此 #2948 的静态 readonly 剥离不覆盖它、FLS 默认也不守卫它。本 PR 关闭它在写路径上的两个洞。

#3004owner_id 客户端可写、服务端无守卫

问题:非特权写入者可以在 insert 时伪造属主(把记录挂到别人名下),或在 update 时转移/弃权属主(owner_id: other / owner_id: null),从而规避行级属主门。

修复(plugin-security 中间件 step 3.5,把 owner_id 作为非特权写入者的系统托管字段):

  • INSERT:空值回填为已认证用户(批量行也回填——此前只有单条路径回填,批量插入的行留下 owner_id = NULL,对自己的创建者都不可见);供给的非本人属主是伪造 → 拒绝。
  • UPDATE:供给 owner_id 即转移/弃权 → 拒绝;单条原值回显(表单保存把未变的属主发回)通过 pre-image 比对被容忍;批量 change-set 携带 owner_id 无 pre-image 可比 → fail-closed。
  • 两者的放行条件都是转移授权(allowTransfer,或蕴含它的 modifyAllRecords)。
  • ctx.isSystem 全程豁免(导入 / OAuth 登录 / cron 快照 / seed 认领);委托场景下双方都须持有授权(ADR-0090 D10 交集)。

#2982 — bulk update/delete 跳过 private 对象的属主收敛

问题:update({ multi: true }) / 批量 delete 在中间件链之后options.where 重建 driver AST,丢弃了 plugin-sharing(buildWriteFilter)与 plugin-security 已 AND-合成到 opCtx.ast 上的属主/RLS 写过滤——于是成员的批量写会命中每一条匹配行,包括同伴的。

修复(objectql/engine.ts):对无单一 id 的 update/delete,在中间件链之前用调用者谓词预置 opCtx.ast(与读路径同一 seam),再把中间件组合后的 AST 交给 driver.updateMany / driver.deleteMany——bulk 写从此和单条写一样被收敛到调用者可编辑的行。

验证

  • 单元测试:
    • objectql/engine.test.ts — bulk update/delete 尊重中间件注入的 opCtx.ast;单条 update 不被预置 AST(仍走 pre-image 路径)。
    • plugin-security/security-plugin.test.ts — owner 守卫矩阵:insert 伪造拒绝 / 本人放行 / 转移授权放行 / modifyAllRecords 放行、批量 insert 逐行回填 + 伪造拒绝、update 转移拒绝 / 弃权拒绝 / 原值回显容忍 / 授权转移放行、bulk change-set fail-closed、无 owner_id 变更不受影响。
  • 真机 dogfood 证明(packages/dogfood/test/owner-anchor-and-bulk-writes.dogfood.test.ts,走真实 HTTP + showcase app,9 用例):成员无法伪造/转移/弃权属主、原值回显通过、平台管理员可重新指派;成员 bulk update/delete 只动自己的行,同伴的行完好。
  • 在 ADR-0096 authz-conformance 账本新增两行(ownership-anchor-guardbulk-write-owner-scoping),固定为 enforced 防回归。
  • 回归:objectql(865)、plugin-security(452)、plugin-sharing(99)、rest(275)全绿;相关 dogfood 套件(private/public-read OWD、scope-depth、attachments、agent 交集、delegation、RLS、CBP)全绿。

关联

🤖 Generated with Claude Code


Generated by Claude Code

…ible rows (#3004, #2982)

owner_id is the row-ownership anchor OWD scoping keys off. Two write-path holes:

- #3004: owner_id was accepted verbatim from clients on insert/update with no
  server guard. It is deliberately not `readonly` (ownership transfers), so the
  static-readonly strip never covered it and FLS doesn't gate it — a member
  could forge a record under another user (insert) or transfer/disown one
  (update), evading the owner gate. plugin-security step 3.5 now treats owner_id
  as system-managed for non-privileged writers: empty is auto-stamped to the
  acting user (batch rows too), a foreign owner on insert is denied, and a
  supplied owner_id on update is a transfer/disown that is denied — the
  unchanged no-op echo is tolerated via a pre-image compare, a bulk change-set
  fails closed. Both require the transfer grant (allowTransfer / modifyAllRecords).
  isSystem stays exempt; delegation intersects both principals (ADR-0090 D10).

- #2982: update({multi:true}) / bulk delete rebuilt the driver AST from
  options.where AFTER the middleware chain, discarding the owner/RLS write
  filter plugin-sharing and plugin-security compose onto opCtx.ast — so a
  member's bulk write hit every matching row, including peers'. The engine now
  seeds opCtx.ast before the chain and hands the composed AST to
  updateMany/deleteMany, matching single-id write scoping.

Proven end-to-end (owner-anchor-and-bulk-writes.dogfood.test.ts) and pinned in
the authz-conformance ledger.
@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 Canceled Canceled Jul 16, 2026 5:53am

Request Review

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

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/dogfood, @objectstack/objectql, @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/spec.

106 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/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @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 @objectstack/objectql, 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 packages/objectql, @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/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • 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/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via packages/dogfood, packages/plugins/plugin-security, packages/plugins/plugin-sharing, @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, packages/plugins/plugin-sharing, @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/objectql, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/plugin-security, @objectstack/plugin-sharing, @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/index.mdx (via @objectstack/objectql)
  • 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/plugins/plugin-sharing, packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @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/objectql, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @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/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.

#2982)

Adversarial code review of the fix surfaced several edge holes; close them:

engine.ts (#2982 seam):
- delete() gains update()'s scalar-id guard, so an id-list bulk delete
  ({ id: { $in: [...] } }, multi) routes to the scoped deleteMany instead of a
  garbage single-id driver.delete — closing the same #2982 hole on delete.
- both multi branches now consume the seeded opCtx.ast and fail CLOSED (throw)
  if it is absent, instead of rebuilding an unscoped { object, where } fallback
  that would silently drop every composed row filter (AGENTS.md PD #12).

plugin-security step 3.5 (owner_id guard):
- own-property (hasOwnProperty) membership test — a polluted prototype can no
  longer spoof an ownership write.
- owner_id must be a non-empty SCALAR; a non-scalar (array/object) is denied,
  not String()-coerced (which let owner_id:[self] pass and corrupt the anchor).
- owner_id:undefined on update is now denied (mongo $set would persist null =
  an ungated disown).
- array-shaped update change-sets fail CLOSED instead of silently bypassing.
- the no-op-echo pre-image is read under the CALLER's context, not isSystem:
  threads the open transaction (no spurious in-tx denial) and closes the
  owner-enumeration oracle a system read opened.
- update short-circuits the field-set lookup when the change-set omits owner_id.

plugin-sharing composeAnd: preserve sibling top-level keys of a
{ $and:[...], k:v } filter (was dropping k) — prevents a caller's AND-ed
predicate from silently widening a bulk write.

spec: allowTransfer describe text corrected — it is enforced NOW via the
insert/update owner_id door, not only the pending M2 transfer op.

Follow-ups filed for out-of-scope findings: public-form owner_id forge (#3022)
and cascade set_null vs the disown guard on sys_user deletion (#3023).

Copy link
Copy Markdown
Contributor Author

对本 PR 做了一轮对抗式自审(多角度并行 + 校验),在第二个提交 e6f4cad 中收敛了若干边界洞:

engine(#2982 seam)

plugin-security(owner_id 守卫)

  • 键成员判定改用 hasOwnProperty(防原型污染伪造属主写)。
  • owner_id 必须为非空 scalar;数组/对象一律拒绝,不再 String() 强转(此前 owner_id:[self] 会被当作 self 并把锚点写坏 → 属主自锁)。
  • owner_id: undefined(mongo $set 会落成 null 的弃权)现在被拒。
  • 数组形态的 update change-set fail-closed,不再静默绕过。
  • no-op 回显的 pre-image 改用调用者上下文读取(而非 isSystem):既接上事务(同事务内回显不误拒),又关闭了 system 读带来的属主枚举 oracle。

plugin-sharing composeAnd 保留 { $and:[…], k:v } 的兄弟键(此前会丢 k),防止调用者的 AND 谓词在 bulk 写时被静默放宽。

spec allowTransfer 文案更正:它经 insert/update 的 owner_id 门现已强制,不再是“M2 前不生效”。

单测已补(数组update拒绝 / undefined弃权 / 非scalar拒绝 / 原型链安全 / delete $in 收敛 / fail-closed)。回归全绿:objectql 867、plugin-security 457、plugin-sharing 99、相关 dogfood 34。

两个越界发现单列跟踪:公开表单 owner_id 伪造 #3022、删除 sys_user 时级联 set_null 与弃权守卫冲突 #3023


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review July 16, 2026 09:03
@os-zhuang
os-zhuang merged commit a16972b into main Jul 16, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/owner-id-security-fix-m36705 branch July 16, 2026 09:03
os-zhuang pushed a commit that referenced this pull request Jul 16, 2026
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 added a commit that referenced this pull request Jul 16, 2026
…dupe pre-image reads (#3053)

Two hardening follow-ups from the #3018 adversarial review.

① The step-2.9 anti-oracle predicate guard is now middleware-order-independent
for writes. #2982 made bulk update/delete carry opCtx.ast, first bringing them
under the guard; it inspected opCtx.ast.where, which plugin-sharing (a sibling
middleware of unguaranteed order) may already have composed an owner_id match
into — so on an FLS-hidden-owner_id object a legitimate bulk write could 403.
The guard now inspects opCtx.options.where (the caller's untouched predicate) for
update/delete. Reads unchanged.

② Pre-image read dedup: a single fail-closed readRowById helper backs the
provenance gates, and a memoized getCallerPreImage collapses the owner-anchor
echo (3.5) and RLS check post-image (3.6) — the identical (object, id,
caller-context) row — into one read per operation. Pure refactor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

2 participants