Skip to content

Commit 12a62ca

Browse files
Merge commit from fork
fix(security): prevent CDN caching of session responses by validating ` __session` and `Set-Cookie`
2 parents 0bfbe1f + d5a6162 commit 12a62ca

File tree

3 files changed

+44
-3
lines changed

3 files changed

+44
-3
lines changed

src/server/auth-client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
removeTrailingSlash
3333
} from "../utils/pathUtils";
3434
import { toSafeRedirect } from "../utils/url-helpers";
35+
import { addCacheControlHeadersForSession } from "./cookies";
3536
import { AbstractSessionStore } from "./session/abstract-session-store";
3637
import { TransactionState, TransactionStore } from "./transaction-store";
3738
import { filterClaims } from "./user";
@@ -296,6 +297,7 @@ export class AuthClient {
296297
await this.sessionStore.set(req.cookies, res.cookies, {
297298
...session
298299
});
300+
addCacheControlHeadersForSession(res);
299301
}
300302

301303
return res;
@@ -441,6 +443,7 @@ export class AuthClient {
441443

442444
const res = NextResponse.redirect(url);
443445
await this.sessionStore.delete(req.cookies, res.cookies);
446+
addCacheControlHeadersForSession(res);
444447

445448
// Clear any orphaned transaction cookies
446449
await this.transactionStore.deleteAll(req.cookies, res.cookies);
@@ -567,6 +570,7 @@ export class AuthClient {
567570
}
568571

569572
await this.sessionStore.set(req.cookies, res.cookies, session, true);
573+
addCacheControlHeadersForSession(res);
570574
await this.transactionStore.delete(res.cookies, state);
571575

572576
return res;
@@ -580,8 +584,9 @@ export class AuthClient {
580584
status: 401
581585
});
582586
}
583-
584-
return NextResponse.json(session?.user);
587+
const res = NextResponse.json(session?.user);
588+
addCacheControlHeadersForSession(res);
589+
return res;
585590
}
586591

587592
async handleAccessToken(req: NextRequest): Promise<NextResponse> {
@@ -631,6 +636,7 @@ export class AuthClient {
631636
...session,
632637
tokenSet: updatedTokenSet
633638
});
639+
addCacheControlHeadersForSession(res);
634640
}
635641

636642
return res;

src/server/cookies.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { NextResponse } from "next/server";
12
import { describe, expect, it } from "vitest";
23

34
import { generateSecret } from "../test/utils";
4-
import { decrypt, encrypt } from "./cookies";
5+
import { addCacheControlHeadersForSession, decrypt, encrypt } from "./cookies";
56

67
describe("encrypt/decrypt", async () => {
78
const secret = await generateSecret(32);
@@ -53,3 +54,17 @@ describe("encrypt/decrypt", async () => {
5354
await expect(() => decrypt(encrypted, "")).rejects.toThrowError();
5455
});
5556
});
57+
58+
describe("addCacheControlHeadersForSession", () => {
59+
it("unconditionally adds strict cache headers", () => {
60+
const res = NextResponse.next();
61+
62+
addCacheControlHeadersForSession(res);
63+
64+
expect(res.headers.get("Cache-Control")).toBe(
65+
"private, no-cache, no-store, must-revalidate, max-age=0"
66+
);
67+
expect(res.headers.get("Pragma")).toBe("no-cache");
68+
expect(res.headers.get("Expires")).toBe("0");
69+
});
70+
});

src/server/cookies.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { NextResponse } from "next/server";
12
import {
23
RequestCookie,
34
RequestCookies,
@@ -329,3 +330,22 @@ export function deleteChunkedCookie(
329330
resCookies.delete(cookie.name); // Delete each filtered cookie
330331
});
331332
}
333+
334+
/**
335+
* Unconditionally adds strict cache-control headers to the response.
336+
*
337+
* This ensures the response is not cached by CDNs or other shared caches.
338+
* It is now the caller's responsibility to decide when to call this function.
339+
*
340+
* Usage:
341+
* Call this function whenever a `Set-Cookie` header is being written
342+
* for session management or any other sensitive data that must not be cached.
343+
*/
344+
export function addCacheControlHeadersForSession(res: NextResponse): void {
345+
res.headers.set(
346+
"Cache-Control",
347+
"private, no-cache, no-store, must-revalidate, max-age=0"
348+
);
349+
res.headers.set("Pragma", "no-cache");
350+
res.headers.set("Expires", "0");
351+
}

0 commit comments

Comments
 (0)