Skip to content

fix(security): 级联 set_null 豁免属主转移守卫 (#3023) - #3048

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

fix(security): 级联 set_null 豁免属主转移守卫 (#3023)#3048
os-zhuang merged 1 commit into
mainfrom
claude/owner-id-security-fix-m36705

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

背景

#3004(PR #3018)给 owner_id 加了属主锚点守卫:非特权写入者携带 owner_id(含置 null 的弃权)在无 transfer 授权时被拒。

owner_id 是指向 sys_userlookup,默认 deleteBehavior: 'set_null'。删除一个 sys_user 时,cascadeDeleteRelations 会对每个引用它的子记录执行 update(child, { id, owner_id: null }, { context }),沿用删除者的上下文重新进入写中间件。守卫把这个 owner_id = null 当成用户发起的弃权而抛 PermissionDeniedError —— 由于没有事务包裹,级联中途失败留下部分置空的子记录。触发面:删除者缺少子对象上的 transfer 授权(典型如成员清理一个 public_read_write 子对象,RLS 本会放行、却被守卫拦下)。

修复

级联 set_null 是引擎内部的引用完整性写入,是已授权父删除的强制后果,不是用户的属主变更——不应被属主转移守卫拦截。

  • engine(cascadeDeleteRelations):set_null 写入改带一个服务端派生的上下文标记 __referentialFieldClear(由引擎设置、绝不来自请求体——与既有的 __expandRead 同一信任模型)。
  • plugin-security(step 3.5 属主守卫):当该标记存在时跳过守卫。

普通用户写路径不受影响;标记无法从客户端伪造(执行上下文由运行时按会话解析,不透传请求字段),因此永远无法借它把真正的属主转移偷渡过守卫。

范围说明:本 PR 精确修复 #3004 守卫对级联的过度拦截,不改变级联的可见性范围(find 仍按调用者上下文发现子记录)。“引用完整性是否应覆盖删除者不可见的子记录(系统级 find)”是更大的独立设计问题,不在本 PR。

验证

  • engine(engine-cascade-delete.test.ts):证明级联 set_null 写入携带 __referentialFieldClear,而普通用户 update 不带(对照);且级联确实把 FK 置空。
  • plugin-security(security-plugin.test.ts):证明带标记的 owner_id: null 被成员放行,不带标记仍被拒(既有弃权用例)。
  • 回归:objectql 899、plugin-security 462 全绿;相关 dogfood(owner-anchor-and-bulk-writes、private OWD、controlled-by-parent、authz-conformance)全绿。

关联

🤖 Generated with Claude Code


Generated by Claude Code

…transfer guard (#3023)
owner_id is a lookup to sys_user with default deleteBehavior 'set_null', so
deleting a sys_user makes cascadeDeleteRelations null owner_id on every dependent.
That cascade write re-entered the write middleware under the deleter's context,
where the #3004 ownership-anchor guard read owner_id=null as a user disown and
denied it — aborting the cascade mid-way (no transaction → partial state) for any
deleter lacking the transfer grant on the child (e.g. a member clearing a
public_read_write child that RLS would otherwise allow).
The cascade FK clear is engine-mandated referential integrity consequent to an
already-authorized parent delete, not a user ownership change. cascadeDeleteRelations
now tags the set_null write with a server-derived __referentialFieldClear context
marker (set by the engine, never built from a request — same trust model as
__expandRead), and the ownership-anchor guard skips when it is present. Ordinary
user writes are unaffected; the marker can't be forged from client input, so it can
never slip a real ownership transfer past the guard.
Tests: engine-cascade-delete proves the set_null write carries the marker (and a
user update does not); plugin-security proves the guard honors it (a member's
owner_id=null with the marker is allowed, denied without). objectql 899 /
plugin-security 462 / related dogfood green.
@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 11:31am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/s labels Jul 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/plugin-security.

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

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/getting-started/cli.mdx(via @objectstack/plugin-security)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql)
  • 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/plugins/plugin-security)
  • content/docs/permissions/explain.mdx(via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx(via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx(via @objectstack/objectql, @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql, @objectstack/plugin-security)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql, @objectstack/plugin-security)
  • content/docs/releases/v9.mdx(via @objectstack/objectql)
  • content/docs/ui/audience-based-interfaces.mdx(via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx(via @objectstack/plugin-security)

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.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/steststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

属主守卫与级联 set_null 冲突:非特权删除 sys_user 时 owner_id 级联置空被 #3004 守卫拦截(级联中途失败)

2 participants

@os-zhuang@claude