Skip to content

[finding] The #13906 auth-gate fail-closed window also refuses the routes isAuthGateAllowlisted exists to keep reachable — a gated user cannot remediate during a session-backend fault #15021

Description

@os-trump

Found while implementing the 2026-09-02 ruling on #13906 (decision 2, option B). Recorded as residue of that change rather than fixed there, because closing it is new policy on the manual floor and the ruling named exactly two conditions.

What the ruling asked for, and what it produced

Decision 2 B: fail closed in one measured window — isAuthGateActive() answered true and the gate's session re-read then failed. That is implemented in RestServer.computeExecCtx (PR #15020) by raising AuthzStoreUnavailableError('auth_gate', err), which the existing fail-closed nets carry to the door as a 503.

The refusal is raised at context-resolution time. But the gate's own consumer, enforceAuth, does not block unconditionally — it exempts allow-listed paths:

const pathExempt =
    typeof req?.path === 'string' && req.path.length > 0 && isAuthGateAllowlisted(req.path);
if (gate && req?.method !== 'OPTIONS' && !pathExempt) {

and the comment above it names the purpose: "a gated session (carrying authGate) is blocked from protected resources, while the core allow-list keeps auth + remediation reachable."

⇒ In the ruled window those remediation routes are also answered 503, because the refusal happens before any path is consulted. A user under an active gate whose session backend is briefly faulting therefore cannot reach the very routes provided so they can clear their gate.

⚠️ NOT MEASURED — this is a code reading

Stated plainly so triage is not misled: I did not drive this. It follows from where the throw sits relative to the pathExempt computation, and the #13906 work measured the throw itself, not its interaction with the allowlist. The first thing to do here is drive it: an active gate, a failing re-read, and a request to an allow-listed path, with a positive control on the same fixture.

Why it was not fixed in PR #15020

  • The ruling's window is two conditions; a path-based carve-out inside computeExecCtx is a third, and it is runtime authorization behaviour — the 2026-08-28 negative-boundary ruling puts that on the manual floor.
  • It is not obviously wrong as it stands. AuthzStoreUnavailableError is an outage (503), not a permission denial: the caller retries and remediation returns when the backend does. Whether an outage answer should be narrowed to protected paths is a product judgement about availability, the same axis the original decision 2 was ruled on.

The options, if this is taken up

  1. Leave it. A 503 is transient and honest; the gate could not be read, so nothing about this caller was determined — including whether they are gated at all.
  2. Narrow the refusal to non-exempt paths — consult isAuthGateAllowlisted(req.path) before raising, so a gated user keeps the remediation door during the fault. Costs: computeExecCtx starts branching on the request path for an authorization decision, and the memoized per-request context becomes path-sensitive.
  3. Move the refusal into enforceAuth — carry "the gate could not be read" as a distinct context value and let the existing consumer, which already knows about pathExempt, decide. Keeps one place that reasons about gate exemptions, but widens ExecutionContext.

⛔ No recommendation asserted on an unmeasured reading; option 3 is the one that puts the decision where the allowlist already lives, and that is an observation about structure, not a verdict.

Related, and distinct

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions