Vulnerability DatabaseGHSA-c5hj-jm58-pxh9

GHSA-c5hj-jm58-pxh9: 
JavaScript vulnerability analysis and mitigation

NOTE

Fixed in Logto 1.46.0 (https://github.com/logto-io/logto/pull/9855). The third-party token endpoints of the Account API now reject access tokens issued to third-party applications, as the other Account API routes already do.

GHSA-mg64-x3xv-96rc and GHSA-cvjg-j6fr-vcjv reported the same issue and were consolidated here; their reporters are credited.


Summary

Third-party OAuth app can steal a user's live upstream social/SSO provider access tokens via the Account API (missing assertFirstPartyClient on the third-party-token endpoints)

Affected code

packages/core/src/routes/account/third-party-tokens.ts:118 third-party-tokens.ts (GET social, no first-party/step-up check): L112 async (ctx, next) => { L113 const { id: userId, scopes } = ctx.auth; L114 const { target } = ctx.guard.params; L118 assertThat(scopes.has(UserScope.Identities), 'auth.forbidden', 403); L120 const tokenSetSecret = await secretsQueries.findSocialTokenSetSecretByUserIdAndTarget(userId, target); L132 ctx.body = await getAccessToken(libraries, queries, tokenSetSecret); // returns decrypted UPSTREAM token (no assertFirstPartyClient anywhere in this file) getAccessToken returns the plaintext upstream token (third-party-tokens.ts:55-74): L55 const { access_token, refresh_token } = decryptTokens({ iv, encryptedDek, ciphertext, authTag }); L72 if (!expiresAt || now < expiresAt) { return formatTokenResponse(access_token, metadata, now); } The boundary these endpoints skip (assert-first-party-client.ts:19-23): L19 export const assertFirstPartyClient = async (queries, clientId?) => { L20 if (clientId !== undefined && (await isThirdPartyApplication(queries, clientId))) { L21 throw new RequestError({ code: 'auth.third_party_application_forbidden', status: 403 }); Auth chain imposes no first-party restriction: koa-oidc-auth.ts:91 assertThat(scopes.has('openid'), ...403); // any app has openid koa-account-center.ts:23 assertThat(accountCenter.enabled, 'account_center.not_enabled'); // tenant toggle only Every sibling account route DOES call it, e.g. identities.ts:126,161,194 ; email-and-phone.ts:52,98,139,181 ; mfa-verifications.ts:109,280,424,468 ; sessions.ts:93 ; grants.ts:90 ; logto-config.ts:79 ; index.ts:104,167,192,275 — third-party-tokens.ts has zero occurrences. The route's own test confirms the only gate is the scope (third-party-tokens.test.ts:86-95): a token with just {openid, identities} and no clientId 'passes the scope gate for GET social access-token' (proceeds to reading the stored secret).

Attacker model

Operator of a third-party (non-first-party) OAuth application registered in the Logto tenant, which the victim end-user has signed into and consented the identities user scope to. The attacker holds only a normal opaque OIDC access token issued to that app (scopes openid identities). No admin, no Management API scope, no server access.

Details

Every account-mutating/identity endpoint on the userRouter enforces the triad scopes.has(...) + (often) identityVerified + assertFirstPartyClient(queries, clientId). assertFirstPartyClient is the documented boundary that keeps third-party OAuth apps (which always carry openid) out of account-management data: its own docstring says enabling a claim scope like email for a third-party app only tells the app it may read that claim, not act on the account. The two GET handlers (and the PUT) in third-party-tokens.ts break this pattern: they gate ONLY on scopes.has(UserScope.Identities) and never call assertFirstPartyClient, nor do they require identityVerified. The handlers then decrypt and return the upstream provider's live access token (the Google/Microsoft/SSO IdP token Logto stored during social/SSO login, refreshing it via the connector if expired). The identities scope's semantic grant is 'your social and SSO identity details' (userinfo claims identities/sso_identities), not the live upstream bearer credential — so returning that token exceeds what the consent conveyed. koaOidcAuth (koa-oidc-auth.ts:91) only requires openid; koaAccountCenter (koa-account-center.ts:23) only requires the tenant toggle enabled; neither restricts to first-party clients. That the sibling GET /api/account profile read (index.ts:52-64) also omits the first-party check confirms third-party apps genuinely reach account GET routes — so these token GETs are reachable by them. Note: the same missing assertFirstPartyClient guard affects every handler in packages/core/src/routes/account/third-party-tokens.ts (both the GET handlers at lines 118 and 132 and the PUT variant), not just a single route — the guard is absent from the router registration, so it applies file-wide.

Proof of concept

Entrypoint: GET /api/account/identities/:target/access-token, GET /api/account/sso-identities/:connectorId/access-token, and the corresponding PUT /api/account/identities/:target/access-token variant (registered on the userRouter via accountRoutes; reachable with a third-party OAuth app's access token)

  1. Tenant admin enables the identities user scope on the attacker's third-party app (routine for apps that want social identity info) and has a social/SSO connector with token storage enabled; the victim previously linked that identity, so Logto has an encrypted upstream token set for the user.
  2. Victim signs into the attacker's third-party app and consents to 'Your social and SSO identity details'.
  3. The app receives an opaque access token with scopes openid identities.
  4. Attacker calls GET https://{tenant}.logto.app/api/account/identities/google/access-token with Authorization: Bearer <that token> (or /api/account/sso-identities/{connectorId}/access-token).
  5. koaOidcAuth passes (openid present) -> koaAccountCenter passes (enabled) -> handler passes the identities scope gate -> NO first-party check, NO step-up check -> getAccessToken() decrypts the stored token set and returns the upstream provider access_token (refreshed if expired).
  6. Attacker now holds the victim's live Google/Microsoft/SSO access token and can call those provider APIs as the victim — a capability the identities consent never granted.

Impact

A consented third-party application escalates a narrow 'read identity details' consent into possession of the user's live upstream IdP/social provider access tokens (with connector-side refresh), enabling impersonation of the victim at the external provider (reading mail/contacts/drive, calling downstream APIs, etc.). This is exactly the trust boundary assertFirstPartyClient exists to protect, and it is the only thing missing on these endpoints.

Verification notes

Cited line third-party-tokens.ts:118 (scope gate on GET social) is accurate, as are the getAccessToken decrypt/return lines and the assert-first-party-client.ts / koa-oidc-auth.ts / koa-account-center.ts citations. Nuance: the test at third-party-tokens.test.ts:86-95 uses a token with NO clientId, so it demonstrates the scope-only gate but does not itself prove a third-party clientId would slip through — the actual proof is the total absence of assertFirstPartyClient in the file combined with the middleware chain that never restricts to first-party. The PUT endpoint additionally requires a verified Social verification record whose identity target and userInfo.id match the current user, but still performs no first-party client check. The two GET endpoints have no step-up (identityVerified) requirement at all. Note the feature also requires the non-default connector 'store token' setting to be enabled for any token to exist to steal.


Source: NVD

Related JavaScript vulnerabilities:

CVE ID

Severity

Score

Technologies

Component name

CISA KEV exploit

Has fix

Published date

GHSA-cr4j-qhrf-249wHIGH7.5
  • JavaScript logoJavaScript
  • @logto/tunnel
NoYesNov 09, 2026
GHSA-2pq4-jqvf-f7qjMEDIUM6.5
  • JavaScript logoJavaScript
  • @logto/core
NoYesNov 09, 2026
GHSA-c5hj-jm58-pxh9MEDIUM6.5
  • JavaScript logoJavaScript
  • @logto/core
NoYesNov 09, 2026
GHSA-3xj6-982x-2r3gMEDIUM6.5
  • JavaScript logoJavaScript
  • @logto/core
NoYesNov 09, 2026
GHSA-ffj9-42qx-f38hMEDIUM6.3
  • JavaScript logoJavaScript
  • @angular/upgrade
NoYesNov 09, 2026

Free Vulnerability Assessment

Benchmark your Cloud Security Posture

Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.

Request assessment

Additional Wiz resources

Get a personalized demo

Ready to see Wiz in action?

"Best User Experience I have ever seen, provides full visibility to cloud workloads."
David EstlickCISO
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
Adam FletcherChief Security Officer
"We know that if Wiz identifies something as critical, it actually is."
Greg PoniatowskiHead of Threat and Vulnerability Management