How a Cached __session Cookie Leaked Across Users on Our Next.js Site
A static page served a cached Set-Cookie header, leaking Auth0 sessions between users. Here is the middleware fix we shipped and the lessons we learned.
Author
The Incident: How a Cached __session Cookie Leaked Across Users on the Academy Website
Last month our monitoring alert fired at 03:17 IST. The academy website, a statically generated Next.js marketing site, was serving authenticated content to users who had never logged in. One of our interns, Arjun, noticed it first: he opened an incognito window, landed on the homepage, and saw a personalized dashboard greeting with a name he did not recognize.
We traced it to a cached Set-Cookie: __session=... header on a statically rendered page. The Auth0 v4 SDK was attaching the session cookie to the response automatically, and Vercel's edge CDN was caching that response for a full year by default. When the next visitor hit the same URL, the CDN served the cached HTML, including the previous user's __session cookie.
This was not a theoretical risk. It was a live, production account takeover vector, and it was happening on our own site.
The Setup: Auth0 v4 SDK + Next.js Middleware on a Statically Generated Marketing Site
Our stack was straightforward on paper:
- Next.js 14, App Router, static export for marketing pages
@auth0/nextjs-auth0v4.5.0 for authenticationauth0.middlewareregistered inmiddleware.tsas the SDK docs required- Deployed on Vercel with default caching behavior
The marketing pages were statically generated at build time. They had no dynamic data, no user-specific content, and no reason to carry a session cookie. But the Auth0 middleware, running on every request, was injecting a Set-Cookie header whenever it detected a session. And because Next.js sets Cache-Control: s-maxage=31536000 on static pages by default, the CDN cached those responses, cookie and all.
What We Tried First: Trusting the Default Auth0 Middleware Behavior
Our initial assumption was that the Auth0 SDK handled this. After all, it is a widely used library, and the v4 migration guide did not mention cache control as a manual step. We wrote the middleware exactly as the docs showed:
// middleware.ts
import { auth0 } from './lib/auth0';
export default auth0.middleware;
We deployed it, tested locally (where caching is disabled), and moved on. Local dev never lies, right?
Wrong. In production, the bug was already live. The SDK does not set Cache-Control headers on responses that include the __session cookie. The GitHub issue Cache-Control headers not set by v4 SDK causing auth0's __session cookie leakage between clients confirms this: the SDK relies on the developer to handle cache control, and the default Next.js behavior is to cache static pages aggressively.
What Actually Failed: CDN Caching of Set-Cookie Headers on Static Pages
The failure chain was clean and brutal:
- User A visits
/aboutand is authenticated. The Auth0 middleware addsSet-Cookie: __session=eyJhbGciOi...to the response. - Vercel's edge CDN caches the response, including the
Set-Cookieheader, becauseCache-Control: s-maxage=31536000says to. - User B, unauthenticated, visits
/about. The CDN serves the cached response, including User A's__sessioncookie. - User B's browser stores the cookie. Now User B is effectively User A.
The Auth0 security policy notes this explicitly: statically generated pages with session cookies must not be cached. But the SDK does not enforce it. It is a footnote, and we missed it.
The Working Fix: Manually Setting Cache-Control Headers in middleware.ts
We needed to ensure that any response carrying a __session cookie was marked as non-cacheable. The fix was to intercept the Auth0 middleware response and set the correct headers before it reached the CDN.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth0 } from './lib/auth0';
export async function middleware(request: NextRequest) {
const authRes = await auth0.middleware(request);
// If the Auth0 middleware returned a response with a Set-Cookie header,
// we must prevent the CDN from caching it.
if (authRes.headers.get('Set-Cookie')) {
authRes.headers.set(
'Cache-Control',
'private, no-cache, no-store, must-revalidate, max-age=0'
);
}
return authRes;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
],
};
This ensured that whenever the __session cookie was present in the response, the CDN would not cache it. Static pages without sessions continued to be cached normally.
Real Commands and File Paths: The Exact Code We Shipped
Here is the full middleware.ts we deployed to production, along with the verification steps we ran:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth0 } from './lib/auth0';
export async function middleware(request: NextRequest) {
const authRes = await auth0.middleware(request);
if (authRes.headers.get('Set-Cookie')) {
authRes.headers.set(
'Cache-Control',
'private, no-cache, no-store, must-revalidate, max-age=0'
);
}
return authRes;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
],
};
We verified the fix with curl:
curl -I -H "Cookie: __session=eyJhbGciOi..." https://academy.agenticlabs.in/about
Before the fix, the response included Cache-Control: s-maxage=31536000 and a Set-Cookie header. After the fix, it returned Cache-Control: private, no-cache, no-store, must-revalidate, max-age=0 with the same Set-Cookie header. The CDN would no longer cache it.
We also added a test in our CI pipeline that checks for Set-Cookie headers on static page responses and fails the build if Cache-Control is not set to no-store.
Pitfalls We Would Warn an Intern About: Why Local Dev Hides This Bug
Local development hides this bug in three ways:
- No CDN caching.
next devdoes not cache responses. The bug only appears when a CDN sits between the user and the server. - No static export caching. When running locally, Next.js serves pages dynamically. In production, static pages are pre-rendered and cached at the edge.
- No multi-user testing. Local dev is single-user. You cannot reproduce cookie leakage without multiple sessions hitting the same cached response.
We also learned that the Auth0 SDK's own test suite does not cover CDN caching scenarios. The v4.6.1 patch release that fixed this issue was reactive, not proactive. It shipped after the vulnerability was reported in Issue #2100.
What We Would Do Differently Next Time: Upgrading to v4.6.1 and Adding Automated Security Checks
We upgraded to @auth0/nextjs-auth0 v4.6.1, which sets Cache-Control: no-store on responses with __session cookies by default. But we kept our manual header override as a defense-in-depth measure. The SDK fix is good, but we do not trust it alone.
We also added three automated checks:
- A curl-based integration test that verifies
Cache-Controlheaders on authenticated static page responses. - A Lighthouse CI audit that flags pages with
Set-Cookieheaders and nono-storedirective. - A weekly security scan using Nuclei with the
http/exposurestemplate set.
The incident cost us a week of engineering time and a lot of sleepless nights. But it also taught us that authentication on static sites is not a solved problem. It is a moving target, and the defaults are not safe.
We now treat every Set-Cookie header in production as a potential security incident until proven otherwise.
Sources
- Cache-Control headers not set by v4 SDK causing auth0's __session cookie leakage between clients
- Auth0 nextjs-auth0 Security Policy
- v4.6.1 Release Notes
- Production Auth Issue, Cookie Exists but Middleware Redirects
- Next.js Middleware/Proxy redirect loop at /login in Production
- Next.js 16 Middleware: auth patterns and race conditions
Sources
Related reading
Enjoyed this article?
Back to Blog


