security(core): stop scoped tokens from escalating via token create and login - #7957
Closed
ar2rsawseen wants to merge 2 commits into
Closed
security(core): stop scoped tokens from escalating via token create and login#7957ar2rsawseen wants to merge 2 commits into
ar2rsawseen wants to merge 2 commits into
Conversation
…ential A token's app/endpoint restriction scopes which app data it may read or write through data endpoints. It does not gate /i/token/create, which carries no app_id, so verify_token never compares the restriction there (and it is skipped entirely when no app_id is supplied). A token "restricted to App A" therefore had unrestricted access to token creation. validateUser resolves any bearer token to its owner and loads the full member, and the create handler then saved caller-supplied app/endpoint/purpose with no relationship to the authenticating credential. So a restricted token could mint an unrestricted child, or a LoggedInAuth token redeemable at /login/token/:token for a full dashboard session, escalating from an app-scoped integration token to the owner's entire account (global admin included, if the owner is one). Gate the create handler: when authenticated via a token, allow creation only if that token has no app restriction and no endpoint restriction. api_key callers (the member itself) and unrestricted tokens - which is what a dashboard session token is (multi, app "", endpoint "") - are unaffected, so the token manager and the create-a-login-token-and-redirect flow keep working. A restricted token is refused with 403. This is deliberately the conservative end of the planned "child permissions are a subset of the creating credential" model: reject rather than intersect, since the app/endpoint model does not sensibly authorize management endpoints. Tests: 7 cases in test/2.api/16.token.manager.js covering the restricted-token refusals (unrestricted child, LoggedInAuth child, refused even when it supplies its own app_id) and the api_key / full-permission-token allow cases. Reported through the security bug bounty programme (received 2026-08-18). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Defence in depth for the token scope-escalation chain. /login/token/:token gated only on the token's purpose, then granted the owner's full session regardless of the token's app/endpoint scope (and it carries no app_id, so verify_token's app check is skipped there too). So any scoped token that carried a login purpose could still be redeemed for a full session. Require the token to be unrestricted (no app and no endpoint scope) before establishing a session, in addition to the purpose allowlist. Legitimate session tokens are always created unrestricted (setLoggedInVariables, the renderer's LoginAuthToken, the ban-warning mail), so only scoped tokens are rejected. Together with the create-side gate, this enforces the invariant from both ends: a session grants the owner's full identity, so it may only come from a full-permission token. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Member
Author
|
Closing in favor of a broader token-model rework that addresses this at the root rather than with the two containment checks here. The branch is retained as a fallback and can be reopened if needed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A token deliberately scoped to one app could escalate to the owner's full account. Two independent gaps, closed from both ends.
The chain (verified on master):
app/endpointrestriction is a data-scoping control —verify_tokencompares it to the request'sapp_id, and only when one is present./i/token/createcarries noapp_idand isn't app-specific, so the restriction never gates it (supplying the permitted app passes; omittingapp_idskips the check entirely).validateUserresolves any bearer token to its owner and loads the full member, and the create handler then saved caller-suppliedapps/endpoint/purposewith no relationship to the authenticating credential (requestProcessor.js). A restricted token could mint an unrestricted child (→ reads any of the owner's apps) or aLoggedInAuthchild./login/token/:tokengated only onpurposeand then granted the owner's full session regardless of the token's scope (members.js). So aLoggedInAuthchild — or any scoped token carrying a login purpose — redeemed into a complete dashboard session as the owner. If the owner is a global admin, that's a global-admin session from a scoped integration token.The
loginWithTokencomment even asserted arbitrary purposes are "settable via /i/token/create by a global admin" — but that endpoint usesvalidateUser, notvalidateGlobalAdmin, so any authenticated principal could setpurpose: "LoggedInAuth". The premise the allowlist rested on was false.Fix — enforce "a session comes only from a full-permission token" at both ends
/i/token/createauthenticated via a token is allowed only if that token has no app and no endpoint restriction. api_key callers (the member) and unrestricted tokens — which is what a dashboard session token is (multi,app:"",endpoint:"") — are unaffected, so the token manager and the mint-a-login-token-and-redirect flow keep working. A scoped token is refused with 403./login/token/:tokenrequires the token to be unrestricted, in addition to the purpose allowlist. Legitimate session tokens are always created unrestricted, so only scoped tokens are rejected.Either gate alone closes the reported chain; both together enforce the invariant regardless of how a scoped login-capable token might arise (pre-existing token, a future regression, or another code path).
This is deliberately the conservative end of the planned "child permissions ⊆ the creating credential" model: refuse rather than intersect, because the app/endpoint model does not sensibly authorize management endpoints. When token/login permissions become explicit in the CRUD migration, these two checks become "you cannot grant a permission you do not hold, and login is one such permission."
Tests
7 cases in
test/2.api/16.token.manager.js(run bytest-api-core): restricted-token refusals for an unrestricted child, aLoggedInAuthchild, and even a child scoped to its own app; plus the api_key and full-permission-token allow cases.Reported through the security bug bounty program (received 2026-08-18).
🤖 Generated with Claude Code