Next.js cache poisoning due to omission of Vary header
Description
Next.js is a React framework for building full-stack web applications. In Next.js App Router from 15.3.0 to before 15.3.3 and Vercel CLI from 41.4.1 to 42.2.0, a cache poisoning vulnerability was found. The issue allowed page requests for HTML content to return a React Server Component (RSC) payload instead under certain conditions. When deployed to Vercel, this would only impact the browser cache, and would not lead to the CDN being poisoned. When self-hosted and deployed externally, this could lead to cache poisoning if the CDN does not properly distinguish between RSC / HTML in the cache keys. This issue has been resolved in Next.js 15.3.3.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
nextnpm | >= 15.3.0, < 15.3.3 | 15.3.3 |
Affected products
1Patches
1ec202eccf058Revert "[next-server] skip setting vary header for basic routes" (#79426)
3 files changed · +44 −5
packages/next/src/server/base-server.ts+9 −3 modified@@ -104,6 +104,7 @@ import { NEXT_ROUTER_SEGMENT_PREFETCH_HEADER, NEXT_DID_POSTPONE_HEADER, NEXT_URL, + NEXT_ROUTER_STATE_TREE_HEADER, NEXT_IS_PRERENDER_HEADER, } from '../client/components/app-router-headers' import type { @@ -1981,16 +1982,21 @@ export default abstract class Server< isAppPath: boolean, resolvedPathname: string ): void { + const baseVaryHeader = `${RSC_HEADER}, ${NEXT_ROUTER_STATE_TREE_HEADER}, ${NEXT_ROUTER_PREFETCH_HEADER}, ${NEXT_ROUTER_SEGMENT_PREFETCH_HEADER}` + const isRSCRequest = getRequestMeta(req, 'isRSCRequest') ?? false + let addedNextUrlToVary = false if (isAppPath && this.pathCouldBeIntercepted(resolvedPathname)) { // Interception route responses can vary based on the `Next-URL` header. // We use the Vary header to signal this behavior to the client to properly cache the response. - res.appendHeader('vary', `${NEXT_URL}`) + res.appendHeader('vary', `${baseVaryHeader}, ${NEXT_URL}`) addedNextUrlToVary = true + } else if (isAppPath || isRSCRequest) { + // We don't need to include `Next-URL` in the Vary header for non-interception routes since it won't affect the response. + // We also set this header for pages to avoid caching issues when navigating between pages and app. + res.appendHeader('vary', baseVaryHeader) } - // For other cases such as App Router requests or RSC requests we don't need to set vary header since we already - // have the _rsc query with the unique hash value. if (!addedNextUrlToVary) { // Remove `Next-URL` from the request headers we determined it wasn't necessary to include in the Vary header.
test/e2e/app-dir/app/index.test.ts+23 −0 modified@@ -323,6 +323,29 @@ describe('app dir - basic', () => { expect(res.headers.get('Content-Type')).toBe('text/x-component') }) + it('should return the `vary` header from edge runtime', async () => { + const res = await next.fetch('/dashboard') + expect(res.headers.get('x-edge-runtime')).toBe('1') + expect(res.headers.get('vary')).toBe( + 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch' + ) + }) + + if (!isNextDeploy) { + it('should return the `vary` header from pages for flight requests', async () => { + const res = await next.fetch('/', { + headers: { + ['RSC'.toString()]: '1', + }, + }) + expect(res.headers.get('vary')).toBe( + isNextDeploy + ? 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch' + : 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding' + ) + }) + } + it('should pass props from getServerSideProps in root layout', async () => { const $ = await next.render$('/dashboard') expect($('title').first().text()).toBe('hello world')
test/e2e/vary-header/test/index.test.ts+12 −2 modified@@ -12,16 +12,21 @@ describe('Vary Header Tests', () => { expect(res.headers.get('vary')).toContain('Custom-Header') }) - it('should preserve custom vary header', async () => { + it('should preserve custom vary header and append RSC headers in app route handlers', async () => { const res = await next.fetch('/normal') const varyHeader = res.headers.get('vary') // Custom header is preserved expect(varyHeader).toContain('User-Agent') expect(res.headers.get('cache-control')).toBe('s-maxage=3600') + + // Next.js internal headers are appended + expect(varyHeader).toContain('RSC') + expect(varyHeader).toContain('Next-Router-State-Tree') + expect(varyHeader).toContain('Next-Router-Prefetch') }) - it('should preserve middleware vary header', async () => { + it('should preserve middleware vary header in combination with route handlers', async () => { const res = await next.fetch('/normal') const varyHeader = res.headers.get('vary') const customHeader = res.headers.get('my-custom-header') @@ -32,5 +37,10 @@ describe('Vary Header Tests', () => { // Both middleware and route handler vary headers are preserved expect(varyHeader).toContain('my-custom-header') expect(varyHeader).toContain('User-Agent') + + // Next.js internal headers are still present + expect(varyHeader).toContain('RSC') + expect(varyHeader).toContain('Next-Router-State-Tree') + expect(varyHeader).toContain('Next-Router-Prefetch') }) })
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-r2fc-ccr8-96c4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-49005ghsaADVISORY
- github.com/vercel/next.js/commit/ec202eccf05820b60c6126d6411fe16766ecc066ghsax_refsource_MISCWEB
- github.com/vercel/next.js/issues/79346ghsax_refsource_MISCWEB
- github.com/vercel/next.js/pull/79939ghsaWEB
- github.com/vercel/next.js/releases/tag/v15.3.3ghsax_refsource_MISCWEB
- github.com/vercel/next.js/security/advisories/GHSA-r2fc-ccr8-96c4ghsax_refsource_CONFIRMWEB
- vercel.com/changelog/cve-2025-49005ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.