Skip to content

Commit 055ece5

Browse files
committed
fix(core): the auth-gate allow-list matches at a mount boundary
`isAuthGateAllowlisted` decides which paths are exempt from the ADR-0069 authentication-policy gate — the gate that holds a session with an expired password or a required MFA enrollment away from protected resources. It matched with two UNANCHORED tests: `path.includes('/auth/')` at any position, and an `endsWith` test over five suffixes at any depth. A path segment whose VALUE merely spelled one of those tokens therefore carried the exemption, and object names and record ids are tenant-controlled. Both seams hand the predicate a data-plane path directly — the dispatcher passes `cleanPath`, REST passes `req.path` — so these were reachable requests. Measured on the built package before the repair: `/data/auth/123`, `/meta/auth/objects`, `/data/x/health` and `/data/xyz/me/apps` were exempt, with `/auth/me` exempt and `/data/contacts/1` gated as controls. The path is read as segments now, and every test is anchored to a mount base (`/api/v1`, `/api`, or the empty base the dispatcher sees) plus at most one environment scope immediately after it — the scoped spelling reaches the predicate because the dispatcher evaluates the gate before its scoped-URL strip. `/auth/…` at that position stays exempt; the five bootstrap reads are exact routes there instead of suffixes. Only exemptions are removed: over a generated corpus of 111,152 paths, newly exempt = 0 and 25,979 stopped being exempt. That subset check is kept as a test with the pre-anchoring predicate transcribed beside it, so a later widening cannot arrive quietly. The platform-checklist citation that named the deleted `ALLOW_SUFFIXES` is re-authored onto the successor symbol rather than dropped, and the REST fault-window measurement pin that recorded the over-broad answer is re-aimed in place with its superseded text quoted, per that file's own instruction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
1 parent 96537da commit 055ece5

2 files changed

Lines changed: 14 additions & 1 deletion

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/core": patch
3+
---
4+
5+
`isAuthGateAllowlisted` matches allow-listed routes at a mount boundary, so an object named `auth` or a record whose id is `health` no longer bypasses the ADR-0069 authentication-policy gate.
6+
7+
The predicate that decides which paths are exempt from the password-expiry / enforced-MFA gate matched with two UNANCHORED tests: `path.includes('/auth/')` matched at any position, and an `endsWith` test over `['/health', '/ready', '/discovery', '/me/apps', '/me/localization']` matched at any depth. A path segment whose VALUE merely spelled one of those tokens therefore carried the exemption — and object names and record ids are tenant-controlled. Both transport seams hand the predicate a data-plane path directly (`HttpDispatcher.enforceAuthGate` passes `cleanPath`, `RestServer.enforceAuth` passes `req.path`), so these were reachable requests. Measured on the built package before the repair: `/data/auth/123`, `/meta/auth/objects`, `/data/x/health` and `/data/xyz/me/apps` were all exempt, while `/auth/me` (exempt) and `/data/contacts/1` (gated) held as controls.
8+
9+
- **What replaced them.** The path is read as segments and each test is anchored to a mount base — `/api/v1`, `/api`, or the empty base the dispatcher sees (the hono adapter hands `dispatch()` the app prefix already stripped) — plus at most one environment scope immediately after that base (`/environments/<id>`, or ADR-0006's superseded `/projects/<id>`), because the dispatcher evaluates the gate before its scoped-URL strip. `/auth/…` at that position stays exempt; the five bootstrap reads are EXACT routes there instead of suffixes. The scope is only recognised immediately after a base, which is why `/data/environments/x/health` is not a scoped `/health`.
10+
- **This only ever removes exemptions.** Measured, not asserted: over a generated corpus of 111,152 paths, the number that are newly exempt is **0** and 25,979 stopped being exempt. The check is kept as a test, with the pre-anchoring predicate transcribed beside it, so a later widening cannot arrive quietly.
11+
- **Every genuinely-exempt shape still is**, pinned in both directions: `/auth/sign-out`, `/health`, `/ready`, `/discovery` (dispatcher shapes); `/api/auth/sign-in`, `/api/v1/auth/change-password`, `/api/v1/auth/me/permissions`, `/api/v1/health`, `/api/v1/me/apps`, `/api/v1/me/localization`; and the scoped `/api/v1/environments/<id>/auth/sign-out`.
12+
13+
**If you serve the API from a non-default mount,** an allow-listed route reached as `${basePath}/${version}/…` with `basePath`/`version` moved off `/api` and `v1` is no longer named by the allow-list. That price cannot be avoided: `/rest/v2/health` and `/data/xyz/health` are the same shape, so a rule that accepts an arbitrary base is the defect itself. It costs nothing at either live seam — the dispatcher's path arrives base-stripped, and REST registers its control-plane routes without `enforceAuth` at all — but if you gate a custom mount through this predicate, mount the remediation routes under one of the named bases.

docs/qa/platform-checklist/areas/access-security.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2599,7 +2599,7 @@
25992599
},
26002600
"source": [
26012601
"packages/plugins/plugin-hono-server/src/current-user-endpoints.ts#tabPermissions (/auth/me/permissions aggregation + most-permissive merge), (/auth/me/localization), (/me/apps requiredPermissions/tabPermissions filter), (the /api/v1 prefix)",
2602-
"packages/core/src/security/auth-gate.ts#ALLOW_SUFFIXES (ALLOW_SUFFIXES — /me/apps + /me/localization reachable to gated users)",
2602+
"packages/core/src/security/auth-gate.ts#ALLOW_ROUTES (ALLOW_ROUTES — /me/apps + /me/localization reachable to gated users. Was ALLOW_SUFFIXES, an endsWith test that also exempted any path merely ENDING in those two — /data/xyz/me/apps among them; the allow-list is anchored to a mount base now, so these are EXACT routes at a mount and a record id can no longer spell its way into the exemption)",
26032603
"#7616 (delegated permission-set resolution — the enforcement path's own answer), #2752 (/me/apps registry sourcing), #3391 (effective apiOperations annotation), #4093 (guarded degraded branch), ADR-0090 D5 (additive baseline)",
26042604
"cross-ref access-security.anonymous-deny-surfaces — the 401 floor this trio is the declared exception to",
26052605
"cross-ref access-security.fls-mask-and-strip — owns the FLS enforcement this item's clause 2 cross-checks"

0 commit comments

Comments
 (0)