Skip to content

isAuthGateAllowlisted exempts by UNANCHORED substring/suffix — an object named auth or a record with id health bypasses the ADR-0069 auth-policy gate #16839

Description

@claude

Found while sweeping path predicates for #16263. Out of that card's scope (it repairs two prefix predicates in packages/runtime; this is a substring/suffix predicate in packages/core), filed unassigned.

The mechanism

isAuthGateAllowlisted (packages/core/src/security/auth-gate.ts) decides which paths are exempt from the ADR-0069 authentication-policy gate — the gate that blocks a session with an expired password or required MFA from protected resources. It matches with two unanchored tests:

if (path.includes('/auth/')) return true;                 // ANY position
for (const s of ALLOW_SUFFIXES) if (path.endsWith(s)) return true;
// ALLOW_SUFFIXES = ['/health', '/ready', '/discovery', '/me/apps', '/me/localization']

Neither is anchored to the start of the path, so a path that merely CONTAINS /auth/ anywhere, or ENDS WITH one of those five suffixes anywhere, is exempted.

Measured

Against the built packages/core on origin/main 7f96e1417e:

isAuthGateAllowlisted('/data/auth/123')      -> true    <- object named `auth`
isAuthGateAllowlisted('/meta/auth/objects')  -> true    <- object named `auth`
isAuthGateAllowlisted('/data/x/health')      -> true    <- record whose id is `health`
isAuthGateAllowlisted('/data/xyz/me/apps')   -> true
isAuthGateAllowlisted('/auth/me')            -> true    (control: genuinely exempt)
isAuthGateAllowlisted('/data/contacts/1')    -> false   (control: correctly gated)

The two control rows are what prove the reading is about these paths and not about the function refusing everything or accepting everything.

HttpDispatcher.enforceAuthGate passes cleanPath to it directly, so these are reachable request paths: a tenant that declares an object named auth, or holds a record whose id is health, hands a gated user an unauthenticated-policy bypass on that object's data routes.

Why this is NOT the already-known part

The /auth/ substring width is already documented at one door, and defended there: endpoint-policy.ts deliberately does NOT pass the request path to shouldDenyAnonymous, with a comment explaining that an app-declared /api/v1/apps/NAMESPACE/auth/callback would otherwise exempt itself from its own authRequired: true. That defence covers app-declared endpoints. It does not cover the dispatcher seam, which passes the path.

ALLOW_SUFFIXES is not covered by that comment at all, and the /data/OBJECT/health row above is the shape nobody has written down.

Not fixed here

#16263 repairs the two bare startsWith PREFIX predicates (the domain registry's default and the membership skip list). This is a different predicate shape in a different package, and the repair is a different question — whether these tests should be anchored, or whether the dispatcher should stop passing a data-plane path to a control-plane allowlist at all. Riding it along would have widened a PR on a surface the card had already scoped.

Refs: #16263 (the prefix members of this sweep), #15021 (a different defect in the same function).


Generated by Claude Code

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