authkit-nextjs may let session cookies be cached in CDNs
Description
The AuthKit library for Next.js provides convenient helpers for authentication and session management using WorkOS & AuthKit with Next.js. In authkit-nextjs version 2.11.0 and below, authenticated responses do not defensively apply anti-caching headers. In environments where CDN caching is enabled, this can result in session tokens being included in cached responses and subsequently served to multiple users. Next.js applications deployed on Vercel are unaffected unless they manually enable CDN caching by setting cache headers on authenticated paths. Patched in authkit-nextjs 2.11.1, which applies anti-caching headers to all responses behind authentication.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
AuthKit for Next.js versions ≤2.11.0 lack anti-caching headers on authenticated responses, risking session token exposure via CDN caches.
The AuthKit library for Next.js (versions 2.11.0 and below) fails to include anti-caching HTTP headers, such as Cache-Control: no-store, on responses for authenticated endpoints [1][2]. This omission means that when a CDN or intermediate cache is enabled, the response—which may contain session tokens or other sensitive data—can be stored and subsequently served to other users [2].
Exploitation requires that the Next.js application is deployed behind a CDN that performs caching, such as when cache headers are manually set on authenticated routes [2]. On Vercel deployments, caching is not enabled by default, so they are unaffected unless explicitly configured [2]. An attacker can simply request the same authenticated URL and, if the response is cached, receive the session token of the legitimate user [2].
The impact is that an attacker could gain access to another user's session, potentially leading to full account compromise. The severity is reflected in a CVSS v4.0 vector string not yet fully published [2].
The issue is fixed in authkit-nextjs version 2.11.1, which applies anti-caching headers to all authenticated responses [3][4]. The patch introduces a function setCachePreventionHeaders that ensures proper Cache-Control and Vary headers are set [3]. Users should upgrade to 2.11.1 immediately. If upgrade is not possible, ensure CDN caching is disabled for authenticated routes.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@workos-inc/authkit-nextjsnpm | < 2.11.1 | 2.11.1 |
Affected products
2- Range: <=2.11.0
- workos/authkit-nextjsv5Range: < 2.11.1
Patches
194cf43812499Merge commit from fork
5 files changed · +143 −30
README.md+40 −18 modified@@ -603,25 +603,21 @@ export default async function middleware(request: NextRequest) { // Control of what to do when there's no session on a protected route is left to the developer if (pathname.startsWith('/account') && !session.user) { console.log('No session on protected path'); - - // Preserve AuthKit headers on redirects (e.g., cookies) - const response = NextResponse.redirect(authorizationUrl); - for (const [key, value] of authkitHeaders) { - if (key.toLowerCase() === 'set-cookie') { - response.headers.append(key, value); - } else { - response.headers.set(key, value); - } - } - return response; + return NextResponse.redirect(authorizationUrl); } - // Forward the incoming request headers (mitigation) and then add AuthKit's headers + // Forward the incoming request headers (mitigation) and pass AuthKit headers as request headers const response = NextResponse.next({ - request: { headers: new Headers(request.headers) }, + request: { headers: authkitHeaders }, }); + // Copy Set-Cookie and cache control headers to the response, but exclude the internal + // x-workos-session header which contains encrypted session data and should never appear + // in HTTP responses (it's only used to pass session data between middleware and page handlers) for (const [key, value] of authkitHeaders) { + if (key.toLowerCase() === 'x-workos-session') { + continue; // Internal header - must not leak to response + } if (key.toLowerCase() === 'set-cookie') { response.headers.append(key, value); } else { @@ -707,17 +703,17 @@ export default authkitMiddleware({ Use the `validateApiKey` function in your application's public API endpoints to parse a [Bearer Authentication](https://swagger.io/docs/specification/v3_0/authentication/bearer-authentication/) header and validate the [API key](https://workos.com/docs/authkit/api-keys) with WorkOS. ```ts -import { NextResponse } from 'next/server' -import { validateApiKey } from '@workos-inc/authkit-nextjs' +import { NextResponse } from 'next/server'; +import { validateApiKey } from '@workos-inc/authkit-nextjs'; export async function GET() { - const { apiKey } = await validateApiKey() + const { apiKey } = await validateApiKey(); if (!apiKey) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - return NextResponse.json({ success: true }) + return NextResponse.json({ success: true }); } ``` @@ -785,6 +781,32 @@ await saveSession(session, req); await saveSession(session, 'https://example.com/callback'); ``` +### CDN Deployments and Caching + +AuthKit automatically implements cache security measures to protect against session leakage in CDN environments. This is particularly important when deploying to AWS with SST/OpenNext, Cloudflare, or other CDN configurations. + +#### How It Works + +The library automatically sets appropriate cache headers on all authenticated requests: + +- `Cache-Control: private, no-cache, no-store, must-revalidate, max-age=0` - Aggressive cache prevention with multiple directives +- `Pragma: no-cache` - HTTP/1.0 compatibility +- `Expires: 0` - HTTP/1.0 cache expiration +- `Vary: Cookie` - Ensures CDNs differentiate between different users (defense-in-depth) +- `x-middleware-cache: no-cache` - Prevents Next.js middleware result caching + +These headers are applied automatically when: + +- A session cookie is present in the request +- An Authorization header is detected +- An active authenticated session exists + +#### Performance Considerations + +**Authenticated pages:** Will not be cached at the CDN level and will always hit your origin server. This is the correct and secure behavior for session-based authentication. + +**Public pages:** Unaffected by these security measures. Public routes without authentication context can still be cached normally. + ### Debugging To enable debug logs, initialize the middleware with the debug flag enabled.
src/authkit-callback-route.ts+17 −6 modified@@ -2,9 +2,14 @@ import { NextRequest } from 'next/server'; import { WORKOS_CLIENT_ID } from './env-variables.js'; import { HandleAuthOptions } from './interfaces.js'; import { saveSession } from './session.js'; -import { errorResponseWithFallback, redirectWithFallback } from './utils.js'; +import { errorResponseWithFallback, redirectWithFallback, setCachePreventionHeaders } from './utils.js'; import { getWorkOS } from './workos.js'; +function preventCaching(headers: Headers): void { + headers.set('Vary', 'Cookie'); + setCachePreventionHeaders(headers); +} + function handleState(state: string | null) { let returnPathname: string | undefined = undefined; let userState: string | undefined; @@ -90,6 +95,7 @@ export function handleAuth(options: HandleAuthOptions = {}) { // Fall back to standard Response if NextResponse is not available. // This is to support Next.js 13. const response = redirectWithFallback(url.toString()); + preventCaching(response.headers); if (!accessToken || !refreshToken) throw new Error('response is missing tokens'); @@ -116,23 +122,28 @@ export function handleAuth(options: HandleAuthOptions = {}) { console.error(errorRes); - return errorResponse(request, error); + return await errorResponse(request, error); } } - return errorResponse(request); + return await errorResponse(request); }; - function errorResponse(request: NextRequest, error?: unknown) { + async function errorResponse(request: NextRequest, error?: unknown) { if (onError) { - return onError({ error, request }); + const response = await onError({ error, request }); + preventCaching(response.headers); + return response; } - return errorResponseWithFallback({ + const response = errorResponseWithFallback({ error: { message: 'Something went wrong', description: "Couldn't sign in. If you are not sure what happened, please contact your organization admin.", }, }); + + preventCaching(response.headers); + return response; } }
src/session.spec.ts+2 −4 modified@@ -116,7 +116,7 @@ describe('session.ts', () => { await expect(async () => { await withAuth(); }).rejects.toThrow( - "You are calling 'withAuth' on https://example.com/ that isn’t covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.", + /You are calling 'withAuth' on https:\/\/example\.com\/ that isn't covered by the AuthKit middleware/, ); }); @@ -126,9 +126,7 @@ describe('session.ts', () => { await expect(async () => { await withAuth({ ensureSignedIn: true }); - }).rejects.toThrow( - "You are calling 'withAuth' on a route that isn’t covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.", - ); + }).rejects.toThrow(/You are calling 'withAuth' on a route that isn't covered by the AuthKit middleware/); }); it('should throw an error if the URL is not found in the headers', async () => {
src/session.ts+73 −2 modified@@ -21,7 +21,7 @@ import { getWorkOS } from './workos.js'; import type { AuthenticationResponse } from '@workos-inc/node'; import { parse, tokensToRegexp } from 'path-to-regexp'; -import { lazy, redirectWithFallback } from './utils.js'; +import { lazy, redirectWithFallback, setCachePreventionHeaders } from './utils.js'; const sessionHeaderName = 'x-workos-session'; const middlewareHeaderName = 'x-workos-middleware'; @@ -30,6 +30,49 @@ const jwtCookieName = 'workos-access-token'; const JWKS = lazy(() => createRemoteJWKSet(new URL(getWorkOS().userManagement.getJwksUrl(WORKOS_CLIENT_ID)))); +/** + * Applies cache security headers with Vary header deduplication. + * Only applies headers if the request is authenticated (has session, cookie, or Authorization header). + * Used in middleware where existing Vary headers may already be present. + * @param headers - The Headers object to set the cache security headers on. + * @param request - The NextRequest object to check for authentication. + * @param sessionData - Optional session data to check for authentication. + */ +function applyCacheSecurityHeaders( + headers: Headers, + request: NextRequest, + sessionData?: { accessToken?: string } | Session, +): void { + const cookieName = WORKOS_COOKIE_NAME || 'wos-session'; + + // Only apply cache headers for authenticated requests + if (!sessionData?.accessToken && !request.cookies.has(cookieName) && !request.headers.has('authorization')) { + return; + } + + const varyValues = new Set<string>(['cookie']); + if (request.headers.has('authorization')) { + varyValues.add('authorization'); + } + + const currentVary = headers.get('Vary'); + if (currentVary) { + currentVary.split(',').forEach((v) => { + const trimmed = v.trim().toLowerCase(); + if (trimmed) varyValues.add(trimmed); + }); + } + + headers.set( + 'Vary', + Array.from(varyValues) + .map((v) => v.charAt(0).toUpperCase() + v.slice(1)) + .join(', '), + ); + + setCachePreventionHeaders(headers); +} + /** * Determines if a request is for an initial document load (not API/RSC/prefetch) */ @@ -120,7 +163,33 @@ async function updateSessionMiddleware( headers.set(signUpPathsHeaderName, signUpPaths.join(',')); } + applyCacheSecurityHeaders(headers, request, session); + + // Create a new request with modified headers (for page handlers) + const requestHeaders = new Headers(request.headers); + requestHeaders.set(middlewareHeaderName, headers.get(middlewareHeaderName)!); + requestHeaders.set('x-url', headers.get('x-url')!); + if (headers.has('x-redirect-uri')) { + requestHeaders.set('x-redirect-uri', headers.get('x-redirect-uri')!); + } + if (headers.has(signUpPathsHeaderName)) { + requestHeaders.set(signUpPathsHeaderName, headers.get(signUpPathsHeaderName)!); + } + + // Pass session to page handlers via request header + // This ensures handlers see refreshed sessions immediately (before Set-Cookie reaches browser) + const sessionHeader = headers.get(sessionHeaderName); + if (sessionHeader) { + requestHeaders.set(sessionHeaderName, sessionHeader); + } + + // Remove session header from response headers to prevent leakage + headers.delete(sessionHeaderName); + return NextResponse.next({ + request: { + headers: requestHeaders, + }, headers, }); } @@ -172,6 +241,8 @@ async function updateSession( const cookieName = WORKOS_COOKIE_NAME || 'wos-session'; + applyCacheSecurityHeaders(newRequestHeaders, request, session); + if (hasValidSession) { newRequestHeaders.set(sessionHeaderName, request.cookies.get(cookieName)!.value); @@ -488,7 +559,7 @@ async function getSessionFromHeader(): Promise<Session | undefined> { if (!hasMiddleware) { const url = headersList.get('x-url'); throw new Error( - `You are calling 'withAuth' on ${url ?? 'a route'} that isn’t covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.`, + `You are calling 'withAuth' on ${url ?? 'a route'} that isn't covered by the AuthKit middleware. Make sure it is running on all paths you are calling 'withAuth' from by updating your middleware config in 'middleware.(js|ts)'.`, ); }
src/utils.ts+11 −0 modified@@ -1,5 +1,16 @@ import { NextResponse } from 'next/server'; +/** + * Sets cache prevention headers to prevent CDN/proxy caching. + * @param headers - The Headers object to set the cache prevention headers on. + */ +export function setCachePreventionHeaders(headers: Headers): void { + headers.set('Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0'); + headers.set('Pragma', 'no-cache'); + headers.set('Expires', '0'); + headers.set('x-middleware-cache', 'no-cache'); +} + export function redirectWithFallback(redirectUri: string, headers?: Headers) { const newHeaders = headers ? new Headers(headers) : new Headers(); newHeaders.set('Location', redirectUri);
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-p8pf-44ff-93gfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-64762ghsaADVISORY
- github.com/workos/authkit-nextjs/commit/94cf438124993abb0e7c19dac64c3cb5724a15eaghsax_refsource_MISCWEB
- github.com/workos/authkit-nextjs/releases/tag/v2.11.1ghsax_refsource_MISCWEB
- github.com/workos/authkit-nextjs/security/advisories/GHSA-p8pf-44ff-93gfghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.