CVE-2026-64648
fetch(Request, init) cache-key confusion leaks POST responses across users
Next.js derived the cache identity from a different request than the one sent upstream.
- Product
- Next.js
- Class
- Cache confusion
- Component
- packages/next/src/server/lib/patch-fetch.ts
- Affected versions
- Next.js 13.0.0–15.5.20 and 16.0.0–16.2.10
- Fixed versions
- Next.js 15.5.21 and 16.2.11
Summary
CVE-2026-64648 allowed a server-side Next.js fetch to return a cached response produced by a different POST request to the same URL. If the body selected an account, organization, or report, one user's data could be returned to another.
The issue appeared in this valid Web Fetch API form:
fetch(new Request(url), init)
The second argument can override the original Request method, headers, body, and cache options. Next.js used those values when deciding whether the operation could be cached, but generated the cache key from the base Request alone. The network request still applied the overrides.
That created two identities for one operation:
Cache key: GET /internal/report, no body
Upstream: POST /internal/report, body=org=victim
Requests with different bodies could therefore share one cache entry.
Exploitation conditions
The vulnerability did not affect every fetch call. An application needed to:
- run
fetchon the server through App Router; - pass a
Requestobject as the first argument; - override relevant properties in the second argument;
- allow the operation to be cached;
- use the body or headers to select different data without changing the URL.
A realistic example is a backend-for-frontend that authenticates a user, selects the allowed tenant, then calls an internal service with a shared credential:
const session = await authenticateUser()
const response = await fetch(
new Request("https://internal.example/reports"),
{
method: "POST",
cache: "force-cache",
headers: {
authorization: "Bearer internal-service-token",
"content-type": "text/plain",
},
body: `org=${session.org}`,
},
)
Application authorization can remain correct. The leak happens later, when the cache maps responses selected by different bodies to the same key.
Root cause
Before the fix, Next.js correctly resolved effective values such as method, headers, and cache:
const getRequestMeta = (field: string) => {
const value = (init as any)?.[field]
return value || (isRequestInput ? (input as any)[field] : null)
}
Cache-key generation later chose between input and init:
cacheKey = await incrementalCache.generateCacheKey(
fetchUrl,
isRequestInput ? (input as RequestInit) : init,
)
When input was a Request, the effective init was discarded at this step. The key generator already included method, headers, cache mode, and body; it was given the wrong object.
The network request remained correct:
return originFetch(input, clonedInit)
The cache described the base Request, while the upstream received the base request combined with the overrides.
Leak sequence
Consider two valid sessions mapped to different organizations:
const sessions = {
"victim-session": { org: "victim" },
"attacker-session": { org: "attacker" },
}
- The victim reaches the route.
- The application authorizes
org=victim. - Next.js looks up a key based on the base
Requestand gets a miss. - The upstream receives
POST body=org=victim. - The private response is stored under the incomplete key.
- The attacker reaches the same route with a separate session.
- The application authorizes
org=attacker, but the lookup finds the victim's entry. - Next.js returns the cached response without calling the upstream again.
The attacker does not need to know or control the victim's body. They only need to reach the same path after a relevant response has been cached.
Reproduction and controls
The PoC used a public route that mapped two sessions to different POST bodies. The victim first warmed the cache:
curl -H "x-session: victim-session" \
"http://127.0.0.1:3000/api/report?mode=request-init&cache=force-cache"
The attacker then requested the same route:
curl -H "x-session: attacker-session" \
"http://127.0.0.1:3000/api/report?mode=request-init&cache=force-cache"
The attacker response contained the report selected by org=victim, even though the application identified the attacker session and intended to query org=attacker.
Two negative controls isolated the vulnerable form:
// Safe: init is used to generate the key
await fetch(url, init)
// Safe: effective values already belong to the Request
await fetch(new Request(url, init))
Both kept victim and attacker responses separate. The bug required a base Request plus different overrides in the second argument.
Patch
The fix normalizes the request at the start of the flow. When a Request and separate init are present, Next.js now builds one effective object:
if (isRequestInput && init) {
const { next, ...overrides } = init
input = new Request(input as Request, overrides)
init = next ? { next } : undefined
}
The next options remain separate because they belong to the framework rather than the Web API's RequestInit.
After normalization, cache policy, cache-key generation, and the network request observe the same identity. The patch does more than special-case POST: it restores the rule that a cache key must describe the operation that is actually executed.
Affected versions and mitigation
The official advisory lists these vulnerable ranges:
Next.js >= 13.0.0 and < 15.5.21
Next.js >= 16.0.0 and < 16.2.11
The fixed versions are 15.5.21 and 16.2.11. Applications using Pages Router only are not affected.
Upgrade to a fixed release:
npm install next@15.5.21
or:
npm install next@16.2.11
The advisory provides no general workaround other than upgrading. Avoiding the affected call form or using cache: "no-store" can reduce exposure at a known call site, but does not replace the patch.
Detection
Look for calls where the first argument is a Request and the second overrides relevant properties:
fetch(requestObject, {
method: "POST",
headers: dynamicHeaders,
body: dynamicBody,
cache: "force-cache",
})
The pattern alone does not prove a leak. Review whether:
- the body or headers change the returned resource;
- the URL remains the same across users;
- the response contains account- or tenant-specific data;
- the operation can be cached and reused;
- another user can reach the same loader or route.
Timeline and credit
- May 18, 2026: vulnerability reported to Vercel.
- June 16, 2026: report moved to validation.
- July 15, 2026: confirmed and awarded at CVSS 6.0.
- July 21, 2026: advisory and fixed releases published.
- July 23, 2026: report closed after the fix shipped.
The vulnerability was discovered and reported by @rafabd1 of Vyntra Research. The reporter credit appears in the official Next.js advisory.