|
| 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