Skip to content

Cross-session access token disclosure via in-flight request coalescing #2382

@MegaManSec

Description

@MegaManSec

Description

When two different authenticated users concurrently call getAccessToken() with the same (audience, scope), the second call may reuse the first call's in-flight promise and receive the first user's access token. This leaks tokens across sessions and can break authz/isolation guarantees. With DPoP, misuse is limited but the token value still leaks.

The problem stems from the fact that TokenRequestCache computes the cache key from audience + scope only, ignoring the caller's session/user (and DPoP key). The cache is shared per Auth0Client instance; the official guidance encourages a process-wide singleton, so concurrent requests from different users hit the same in-memory cache.

Conditions to exploit:

  • A singleton Auth0Client instance
  • Two+ users request a token with the same (audience, scope) at the same time on the same runtime/worker.

Reproduction

Log in as User A and User B (separate browsers or sessions).

From both, fire parallel requests to a route that calls auth0.getAccessToken(req, res, { audience: X, scope: '....', refresh: true }).

Observe cases where both responses return identical token, and only User A's session gets persisted changes (if any).

Additional context

Here's an unfinished patch which gives a rough idea of the issue + a path towards a true fix:

diff --git a/src/server/token-request-cache.ts b/src/server/token-request-cache.ts
index dab3a387..ee960abd 100644
--- a/src/server/token-request-cache.ts
+++ b/src/server/token-request-cache.ts
@@ -53,6 +53,7 @@ export abstract class GenericRequestCache<TOptions, TResponse> {
 export type TokenRequestCacheOptions = {
   options: GetAccessTokenOptions;
   authorizationParameters?: AuthorizationParameters;
+  sessionKey?: string;
 };

 export type TokenRequestCacheResponse = {
@@ -76,10 +77,12 @@ export class TokenRequestCache extends GenericRequestCache<
    */
   protected getTokenCacheKey({
     options,
-    authorizationParameters
+    authorizationParameters,
+    sessionKey
   }: {
     options: GetAccessTokenOptions;
     authorizationParameters?: AuthorizationParameters;
+    sessionKey?: string;
   }): string {
     const audience =
       options.audience ?? authorizationParameters?.audience ?? "";
@@ -88,6 +91,7 @@ export class TokenRequestCache extends GenericRequestCache<
       (authorizationParameters?.scope &&
         getScopeForAudience(authorizationParameters?.scope, audience)) ??
       "";
-    return `${audience}:${scope}`;
+    const session = sessionKey ?? "anon";
+    return `${session}:${audience}:${scope}`;
   }
 }

This bug was found with ZeroPath.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions