Skip to content

fix(security): 元数据不可解析时安全姿态 fail-closed —— #3545 那条前提本身不成立 - #3748

Merged
os-zhuang merged 2 commits into
mainfrom
claude/metadata-fail-open-risk-6i6ylu
Jul 28, 2026
Merged

fix(security): 元数据不可解析时安全姿态 fail-closed —— #3545 那条前提本身不成立#3748
os-zhuang merged 2 commits into
mainfrom
claude/metadata-fail-open-risk-6i6ylu

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3545 的核心风险项。

背景:上一轮结论依赖一条未被验证的前提

#3545 上一轮(PR #3567)评估了 API 曝露 gate 在元数据不可解析时的 fail-open,结论是残余风险 = 低。这个结论完全建立在一条前提上:

gate 是曝露面控制,不是授权边界;真正的授权边界是 auth + ObjectQL 安全中间件(CRUD/FLS/RLS),它在数据调用上无条件执行,与 gate 结果无关。

这条前提是 load-bearing 的,所以本轮去验证它,而不是接着假设。结果:它不成立

发现:同一个触发条件穿透到了边界内部

中间件确实无条件运行 —— 但它的两个输入读自同一份对象元数据,并且在读不到时向宽松端默认。于是 #3545 关心的那个触发条件(元数据不可解析)绕过了曝露 gate 之后,又穿透了下面这层边界:

声明 读不到时的默认 后果
access.default: 'private' isPrivate: false ADR-0066 D2 明确规定 private 对象不被普通(非 superuser)'*' 通配授权覆盖;被当成 public 后就被覆盖了 —— 一条没有任何人授予过的权限
requiredPermissions [] ADR-0066 D3 的能力 AND-gate 因 if (required.length > 0) 被整段跳过

这不是理论路径。新增的回归用例先跑对照组(元数据可解析 → 按 ADR-0066 拒绝),再跑同一场景下元数据不可解析 —— 修复前两条都放行:一个只持有普通通配授权、不持有 manage_platform_settings 的普通成员,既读到了 private 对象,也读到了声明了能力契约的对象。

改动

getObjectSecurityMeta 现在标记 unresolved,三个把姿态转成访问决策的消费方一律 fail-closed:

  • 中间件 → 拒绝,并 logger.error(持续性元数据故障可观测,而不是静默全量放行)
  • canExport → 拒绝
  • getReadableFields → 不暴露任何列

与该文件既有的 fail-closed 立场一致(权限解析失败、悬空 delegator)。

computeLayeredRlsFilter 有意保持消费默认值:在那里宽松默认恰好是扣留跨租户豁免,已经是收紧方向 —— 一并改成"最严"反而会引入一个反方向的 fail-open。

explain 引擎在既有 object_crud 层报出真实原因,避免"为什么被拒"的解释面与执行面漂移(不新增 layer 类型,不动 spec)。

影响范围

严格限定在有风险的那一档。isSystem(系统/启动写入)与无主体/匿名上下文在中间件更早处就短路了,所以走到新检查意味着:一个已认证、已解析出授权的主体,请求一个声明缺失的对象

因此冷启动窗口不会被误伤 —— 那个窗口靠的是前面的短路,不是这个宽松默认。这正好回答了 issue 里"fail-closed 会不会在正常启动窗口误伤流量"这一问:不会。也因此,曝露 gate 那一侧的分级决策(瞬态不可用 → fail-open)维持不变 —— 现在它下面的边界是真的兜得住了。api-exposure.ts 里的 #3545 决策记录已相应更正。

验证

  • 新增 metadata-unresolvable-posture.test.ts:每条轴的两个方向(可解析 → 按 ADR-0066 拒绝;不可解析 → 仍然拒绝)、不受影响的匿名/系统/public 对象路径、错误日志、explain 一致性
  • plugin-security 645 passed · runtime / rest / objectql 全绿(28 tasks)
  • dogfood 套件 369 passed —— 真实 showcase 应用启动、真实认证用户流量,无一例合法请求踩中新的 fail-closed 分支

遗留

issue 里 ③ 那条(apiMethods 非数组 → fail-closed)已在 #3543 落地,resolveEffectiveApiMethods 现已解析为 deny-all


Generated by Claude Code

… resolved (#3545)

#3545 assessed the API-exposure gate's fail-open on unresolvable metadata and
accepted it, resting on one load-bearing premise: the gate is a SURFACE-AREA
control, while the real authorization boundary — auth + the ObjectQL security
middleware (CRUD/FLS/RLS) — enforces unconditionally on the data call whatever
the gate answers.

Verifying that premise instead of assuming it shows it did not hold. The
middleware does run unconditionally, but two of its INPUTS were read from the
same object metadata and defaulted permissively when it could not be resolved,
so the very trigger this issue is about reached one layer PAST the gate, into
the boundary itself:

  * `access.default: 'private'` -> `isPrivate` defaulted to false. ADR-0066 D2
    deliberately excludes a private object from a plain (non-superuser) `'*'`
    wildcard; read as public it IS covered — a grant nobody authored.
  * `requiredPermissions` -> defaulted to `[]`, which skips the ADR-0066 D3
    capability AND-gate entirely (`if (required.length > 0)`).

`getObjectSecurityMeta` now flags `unresolved`, and the three consumers that
turn posture into an access decision fail closed on it: the middleware denies
(with an error log, so a persistent metadata outage is observable rather than a
silent blanket-allow), `canExport` denies, `getReadableFields` exposes no
columns — the same stance already taken for a permission-resolution failure and
a dangling delegator. `computeLayeredRlsFilter` keeps consuming the defaults on
purpose: there the permissive value WITHHOLDS the cross-tenant exemption, so it
is already the closed direction.

Blast radius is bounded to the risky case. System/boot writes (`isSystem`) and
principal-less/anonymous contexts short-circuit earlier in the middleware, so
reaching the new check means an authenticated principal with resolved grants
asking for an object whose declaration is missing. The cold-start window is
served by those short-circuits, not by the permissive default — which is why
the tiered decision recorded for the exposure gate (transient unavailability ->
fail open) stands unchanged now that the boundary underneath it actually holds.

The explain engine reports the denial on its existing `object_crud` layer
naming the real cause, so the "why am I denied?" surface cannot drift from
enforcement.

Regression-pinned in metadata-unresolvable-posture.test.ts: both directions of
each axis (resolvable -> denied per ADR-0066; unresolvable -> still denied),
the unaffected anonymous / system / public-object paths, the error log, and
explain parity. Verified green: plugin-security (645), runtime, rest, objectql,
and the dogfood suite that boots the real showcase app (369).

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

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 1:42am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-security)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security, packages/runtime)
  • 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/plugin-security)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security, @objectstack/runtime)
  • 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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Jul 28, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 02:07
@os-zhuang
os-zhuang merged commit 7180ed5 into main Jul 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/metadata-fail-open-risk-6i6ylu branch July 28, 2026 02:07
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.

元数据不可解析时的 fail-open 残余风险评估(api-exposure / rest-server)

2 participants