Skip to content

Commit 4adb3c7

Browse files
committed
chore: add changeset for the #3545 posture fail-closed fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019288JyDfHtCiYVgAJLxmsg
1 parent 95c6b5f commit 4adb3c7

1 file changed

Lines changed: 41 additions & 0 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"@objectstack/plugin-security": patch
3+
"@objectstack/runtime": patch
4+
---
5+
6+
fix(security): fail closed when an object's security posture can't be resolved
7+
(#3545)
8+
9+
#3545 accepted the API-exposure gate's fail-open on unresolvable metadata on one
10+
load-bearing premise: that gate is a SURFACE-AREA control, while the real
11+
authorization boundary — auth + the ObjectQL security middleware (CRUD/FLS/RLS)
12+
— enforces unconditionally on the data call whatever the gate answers.
13+
14+
Verifying that premise rather than assuming it shows it did not hold. The
15+
middleware does run unconditionally, but two of its INPUTS were read from the
16+
same object metadata and defaulted permissively when it could not be resolved,
17+
so the very trigger the issue is about reached one layer PAST the gate, into the
18+
boundary itself: an unresolved `access.default` read as PUBLIC (so a plain `'*'`
19+
wildcard covered an object ADR-0066 D2 excludes from it) and an unresolved
20+
`requiredPermissions` read as NO CONTRACT (so the D3 capability AND-gate was
21+
skipped entirely).
22+
23+
`getObjectSecurityMeta` now flags `unresolved`, and the three consumers that turn
24+
posture into an access decision fail closed on it: the middleware denies (with an
25+
error log, so a persistent metadata outage is observable rather than a silent
26+
blanket-allow), `canExport` denies, and `getReadableFields` exposes no columns —
27+
the same stance already taken for a permission-resolution failure and a dangling
28+
delegator. `computeLayeredRlsFilter` keeps consuming the defaults deliberately:
29+
there the permissive value WITHHOLDS the cross-tenant exemption, so it is already
30+
the closed direction.
31+
32+
Blast radius is bounded to the risky case. System/boot writes (`isSystem`) and
33+
principal-less/anonymous contexts short-circuit earlier in the middleware, so
34+
reaching the new check means an authenticated principal with resolved grants
35+
asking for an object whose declaration is missing; the cold-start window is
36+
served by those short-circuits, not by the permissive default. The exposure
37+
gate's own tiered decision (transient unavailability → fail open) is therefore
38+
unchanged — it now rests on a boundary that actually holds.
39+
40+
The explain engine reports the denial on its existing `object_crud` layer naming
41+
the real cause, so the "why am I denied?" surface cannot drift from enforcement.

0 commit comments

Comments
 (0)