This website uses cookies

Read our Privacy policy and Terms of use for more information.

Seven advisories landed in one release, and the instinct is to triage them like a list: patch, move on. But read the descriptions in a row and a pattern shows up. A use cache function called through unstable_cache returns another request's result. A nested use cache stores an outer entry before the inner function's dependencies are recorded. Two different binary arguments produce the same cache identity. A crafted URL pollutes a shared page shell that other URLs then serve.

That is four advisories describing one failure: the cache decided two calls were the same call when they were not. My argument in this issue is that this is a class of bug, not four coincidences, and that it will keep appearing wherever a framework infers cache keys for you. If you only patch, you fix the four instances. If you understand the class, you know what to audit in your own code before the next release.

What shipped, and what the post says versus what the pre-announcement said

On October 27 the Next.js team announced a scheduled release for Tuesday, November 3, and said it would address "seven medium vulnerabilities." On November 3 the release post delivered the fixes in v16.4.5 (Active LTS) and v15.5.29 (Maintenance LTS).

Two small things in that post are worth knowing before you copy commands from it. The upgrade snippet's comment reads npm install [email protected] # for 16.5, which is a typo for the 16.4 line, so trust the version number, not the comment. And the cross-tenant unstable_cache issue, GHSA-mfr8-8qww-v76h, is labeled High severity in the release post, while the pre-announcement and Netlify's changelog both describe the whole batch as Medium. I can't tell you which label your risk process should follow. I can tell you the High label sits on the bug with the clearest multi-tenant data exposure, which is the one to read first.

Netlify's changelog adds CVE identifiers the Next.js post does not list, for example CVE-2026-119790 for the unstable_cache issue and CVE-2026-119349 for the binary-argument one. Netlify also records affected ranges: the nested and binary-argument issues start at 16.2+ and 16.x, the unstable_cache one at 16.3+, and the shell poisoning at 16.4+. The pattern is that every cache bug needs Cache Components, which is the newer caching model, so if you have not enabled it, most of this does not touch you.

The four, in the order they get worse to reason about

The first is the unstable_cache leak. Per the advisory, applications with Cache Components can disclose cached content across root-parameter values when the same public use cache function is called both directly and from inside unstable_cache, and the function reads a root parameter such as a tenant identifier. During a cold fill, a concurrent call through unstable_cache "can receive another request's result without checking its root parameters," and that result is then stored and served to later callers until revalidation or eviction.

Read that as a cache-key statement. The entry's identity depended on the root parameter. One of the two call paths did not include it in the lookup. So tenant A's value became the answer for tenant B.

The second is the nested use cache case. An inner function reads a root parameter and returns an early value alongside a slower async value. The outer function finishes without waiting for the slower value. In the advisory's words, the outer entry "can be stored before the inner function's root-parameter dependencies are recorded." Here the key is not computed wrong; it is computed too early. The dependency that should have been part of the identity had not been observed yet when the entry was written.

The third is binary arguments. "Distinct binary arguments passed to a use cache function can produce the same cache identity." If your application uses those arguments to distinguish users or accounts, and the other identity inputs match, one caller can receive another's result or plant their own.

I don't know how Next.js serializes arguments internally and the advisory does not say, so I won't claim the mechanism. But the shape is familiar enough to demonstrate with a toy. This is not Next.js code. It is the standard way a lossy key goes wrong, and I ran it:

const cache = new Map();
const keyOf = (args) => JSON.stringify(args);
async function cached(fn, ...args) {
  const k = keyOf(args);
  if (!cache.has(k)) cache.set(k, await fn(...args));
  return cache.get(k);
}
const loadInvoice = async (blob) => `invoice for ${await blob.text()}`;

console.log(keyOf([new Blob(["alice"])]), keyOf([new Blob(["bob"])]));
console.log(await cached(loadInvoice, new Blob(["alice"])));
console.log(await cached(loadInvoice, new Blob(["bob"])));

On Node, the output is [{}] [{}], then invoice for alice twice. JSON.stringify flattens every Blob to {}, so Bob is served Alice's entry. Hashing the actual bytes gives 616c696365 and 626f62, which are distinct. The lesson is not about Blob. It is that the moment a key is derived from a representation of the input instead of the input, you have a place for two different inputs to share a key.

The fourth is the shell poisoning, and it is the one that needs no authentication. Self-hosted applications running next start with Cache Components enabled and Partial Prefetching disabled or unset can have a shared page shell poisoned "by an unauthenticated request." A crafted URL causes request-specific values to be stored in a shell that other URLs use, producing wrong server-rendered content or persistent 404s. The route shape needed is specific: a prerenderable dynamic parameter requested with a value not generated at build time, at least two deeper parameters that are never prerendered, a blocking fallback shell, and a layout that reads the shallower deeper parameter inside a Suspense boundary. Netlify's table describes the same issue as cache poisoning of shared fallback shells via request-supplied placeholder route params, and notes that valid pages can return a cached 404.

Note what that is: a request-supplied value (the URL segment) became part of stored state that was keyed on something coarser (the shell). The input leaked into the value without making it into the key.

Why a framework that infers keys produces this class

A hand-written cache key is verbose and boring: tenant:${tenantId}:invoice:${id}. You wrote every part, so you know what is missing. The appeal of use cache is that you stop writing that string. The framework observes what your function reads (arguments, root parameters, closed-over values) and builds the identity from that.

Inference moves the failure from "I forgot to put the tenant in the key" to "the framework did not notice the function read the tenant." Those are different bugs with different detection methods. The first is visible in review, because the key is on the screen. The second is invisible in review, because there is no key on the screen, and it only shows up under concurrency (the unstable_cache race, the nested early-return) or with unusual input (binary arguments, crafted URLs).

Two of the four, the unstable_cache race and the nested early return, depend on timing: a concurrent cold fill, or an entry written before a slower value resolves. A test that sends one request at a time and checks the output cannot trigger a cold-fill race, so that kind of test passes while the bug is present.

The cases that are not this bug

Three of the seven are not cache-identity problems, and lumping them in would be sloppy. The Edge cookie issue (GHSA-f3mh-rxg3-f5g2) exposes serialized cookies in an internal x-middleware-set-cookie header that same-origin JavaScript can read, including HttpOnly cookies. It needs attacker JavaScript already running on your origin, and it does not affect Node.js Route Handlers or cookies().set(). The SVG bypass (GHSA-7qx7-33qx-rqx2) lets an image from a permitted source be misclassified as AVIF and slip past images.dangerouslyAllowSVG: false, though the advisory says it does not itself demonstrate XSS or RCE. And the compressed-response framing issue (GHSA-pc58-hwjq-p385) mixes up responses between clients behind certain HTTP/1.1 proxies. That last one is a cousin of the cache bugs, since it is also one client getting another's data, but the cause is mismatched framing headers, not a lookup.

The objection: these are medium severity with narrow preconditions

The strongest objection is that I am building a theory on bugs with narrow trigger conditions. The shell poisoning needs a very specific route shape. The binary-argument case needs binary values that distinguish users. The nested case needs an early return paired with a slower async value. Each advisory spells out how limited its reach is, and the Next.js team is explicit that the binary-argument bug "does not permit arbitrary access to unrelated cache entries."

That is true, and it is why I would not tell anyone to rip out use cache. But narrow preconditions describe how hard it is to hit one bug, not how likely it is that a key-inferring system has another one. Four independent preconditions, all in the same subsystem, in one release, is a signal about the subsystem. The honest conclusion is moderate: patch now, and treat your own tenant-scoped cached functions as the place to look, because that is where a miss costs the most.

Netlify's note is instructive here. It marked the unstable_cache leak "Not affected," reasoning that the leak needs two requests to share one server process, which its isolated serverless invocations prevent. Your hosting model changes your exposure to this class. Long-lived next start processes share memory across requests, which is the precondition for cross-request cache bleed.

What to do Monday

Upgrade to 16.4.5 or 15.5.29. If you run on a platform with a CDN or build cache, remember Netlify's reminder that a new deploy clears cache entries only within the same deploy context, so production, branch deploys and Deploy Previews need separate purges, and public previews may stay vulnerable until deleted.

Then audit three things in your own code. First, find every use cache or unstable_cache function that reads a tenant, locale or site identifier, and check that it is called one way only, not both directly and through the wrapper. Second, find every cached function that takes a binary or non-plain-object argument, and replace it with an explicit string or hash so the identity is something you can read. Third, add one concurrency test to anything tenant-scoped: fire two cold requests for different tenants at once, against a cleared cache, and assert each response contains only its own tenant's data.

Seven advisories is a batch to patch. Four of them agreeing on one cause is a design review prompt: for every cache you rely on, can you say, in one line, what is in its key and what is not?

Sources