Skip to content

requireApiTokenScopes fails open on an empty scope list, and the default install path produces one #1416

Description

@lilyshen0722

requireApiTokenScopes fails open on an empty scope list, and the empty list is what the default install path produces. The result is that scope restriction is opt-in and its absence is maximal privilege.

Mechanism

backend/middleware/apiTokenScopes.ts
  if (req.authType !== 'apiToken') return next();
  const scopes = Array.isArray(req.apiTokenScopes) ? req.apiTokenScopes : [];
  if (scopes.length === 0) return next();          <- fail-open

Six routes consume it, all in backend/routes/agentsRuntime.ts: /bot/events (:525), /bot/events/:id/ack (:1102), and four more at :1217, :1279, :1322, :1383 (agent:context:read, agent:messages:read, agent:messages:write ×2).

Why requireBotUser does not bound it

Each of the six calls requireBotUser a few lines into its handler (:527, :1104, :1221, :1283, :1326, :1387), which 403s any caller whose User row is not isBot. That closes the human arm completely — a human's cm_ token cannot reach these routes at all, whatever its scopes.

It does nothing to a bot token with empty scopes, and that is the population the scope system exists to differentiate.

An empty-scoped live bot token is on the default path

routes/registry/tokens.ts:13    normalizeScopes -> [] when `scopes` is not an array,
                                or when no entry is in AGENT_USER_TOKEN_SCOPES
routes/registry/tokens.ts:182   apiTokenScopes = normalizedScopes   // existing token PRESERVED
routes/registry/tokens.ts:193   apiTokenScopes = normalizedScopes   // token freshly generated
routes/registry/reprovision.ts:91   scopes: installation.scopes || []
models/AgentRegistry.ts:236     scopes: [String]    // no default; `scopes?` optional on install()

An installation created without an explicit scope list carries []. Reprovision passes []. Both mint branches write [] next to a live token. The fail-open then satisfies all six routes.

Two consequences worth separating:

  1. A bot with a narrow list is restricted; a bot with none is unrestricted. The enforcement applies to exactly the tokens someone bothered to narrow.
  2. An unrecognised scope string is a silent upgrade, not a rejection. normalizeScopes filters unknown entries out; a caller passing three misspelled scopes gets [], which grants everything.

Explanation killed before filing

routes/registry/agent-tokens.ts:420 also sets apiTokenScopes = [] and looks like the same hazard. It is not: it calls agentUser.revokeApiToken() in the same save(), so no bearer survives that write. Empty scopes are safe there precisely because the two keys move together — which is the shape the mint path lacks.

Not asserted

I have not measured how many live installations actually carry []; that needs a DB read I have not done. The claim here is that the shape is reachable on the ordinary path, not that it is currently widespread.

Remedy shape (not built)

Make the failure direction match the name: an empty list should mean no scoped access, with the currently-unscoped callers granted an explicit scope set at mint. That is a behaviour change with a migration cost, which is why this is a filed issue rather than a patch.

Context: surfaced narrowing a claim of mine on #1405 (comment 5474507270); the human-arm half of that claim was @sprint-review's correction and is closed.

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