CVE-2026-64648
fetch(Request, init) cache-key confusion vaza respostas POST entre usuários
O Next.js calculava a identidade do cache a partir de uma requisição diferente daquela enviada ao upstream.
- Produto
- Next.js
- Classe
- Cache confusion
- Componente
- packages/next/src/server/lib/patch-fetch.ts
- Versões afetadas
- Next.js 13.0.0–15.5.20 e 16.0.0–16.2.10
- Versões corrigidas
- Next.js 15.5.21 e 16.2.11
Resumo
A CVE-2026-64648 permitia que um fetch server-side do Next.js retornasse a resposta armazenada de outra requisição POST feita para a mesma URL. Se o body escolhesse uma conta, organização ou relatório, dados de um usuário poderiam ser entregues a outro.
A falha aparecia neste formato válido da Web Fetch API:
fetch(new Request(url), init)
O init pode sobrescrever método, headers, body e opções de cache do Request original. O Next.js usava esses valores para decidir se a operação poderia ser armazenada, mas calculava a cache key somente a partir do Request base. A requisição de rede, por sua vez, aplicava os overrides.
Esse cache confusion criava duas identidades para a mesma operação:
Cache key: GET /internal/report, sem body
Upstream: POST /internal/report, body=org=victim
Requisições com bodies diferentes podiam então compartilhar a mesma entrada.
Condições para exploração
A vulnerabilidade não afetava todo uso de fetch. A aplicação precisava reunir estas condições:
- executar
fetchno servidor pelo App Router; - passar um objeto
Requestcomo primeiro argumento; - sobrescrever propriedades relevantes no segundo argumento;
- permitir cache para a operação;
- usar body ou headers para selecionar dados diferentes sem alterar a URL.
Um caso plausível é um backend-for-frontend que autentica o usuário, escolhe o tenant permitido e consulta um serviço interno com uma credencial compartilhada:
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}`,
},
)
A autorização da aplicação pode continuar correta. O vazamento ocorre depois, quando o cache associa respostas selecionadas por bodies distintos à mesma chave.
Causa raiz
Antes da correção, o Next.js identificava corretamente os valores efetivos de method, headers e cache:
const getRequestMeta = (field: string) => {
const value = (init as any)?.[field]
return value || (isRequestInput ? (input as any)[field] : null)
}
Na geração da chave, porém, a implementação escolhia entre input e init:
cacheKey = await incrementalCache.generateCacheKey(
fetchUrl,
isRequestInput ? (input as RequestInit) : init,
)
Quando input era um Request, o init efetivo era descartado nessa etapa. O gerador da chave já considerava método, headers, modo de cache e body; ele apenas recebia o objeto errado.
O request de rede continuava correto:
return originFetch(input, clonedInit)
Assim, o cache descrevia o Request base enquanto o upstream recebia o Request base combinado com os overrides.
Fluxo do vazamento
Considere duas sessões válidas mapeadas para organizações diferentes:
const sessions = {
"victim-session": { org: "victim" },
"attacker-session": { org: "attacker" },
}
- A vítima acessa a rota.
- A aplicação autoriza
org=victim. - O Next.js consulta uma chave baseada no
Requestbase e encontra um miss. - O upstream recebe
POST body=org=victim. - A resposta privada é armazenada sob a chave incompleta.
- O atacante acessa a mesma rota com sua própria sessão.
- A aplicação autoriza
org=attacker, mas a consulta encontra a entrada da vítima. - O Next.js devolve a resposta armazenada sem consultar o upstream novamente.
O atacante não precisa conhecer nem controlar o body da vítima. Ele precisa alcançar o mesmo caminho depois que uma resposta relevante estiver no cache.
Reprodução e controles
A PoC usou uma rota pública que convertia duas sessões em bodies distintos. Primeiro, a vítima aquecia o cache:
curl -H "x-session: victim-session" \
"http://127.0.0.1:3000/api/report?mode=request-init&cache=force-cache"
Depois, o atacante chamava a mesma rota:
curl -H "x-session: attacker-session" \
"http://127.0.0.1:3000/api/report?mode=request-init&cache=force-cache"
A resposta do atacante continha o relatório selecionado por org=victim, embora a aplicação tivesse identificado a sessão e pretendesse consultar org=attacker.
Dois controles negativos isolaram o bug:
// Seguro: init participa da geração da chave
await fetch(url, init)
// Seguro: os valores efetivos já pertencem ao Request
await fetch(new Request(url, init))
Nos dois casos, vítima e atacante receberam respostas separadas. O problema exigia a combinação entre um Request base e overrides diferentes no segundo argumento.
Correção
O patch normaliza a requisição no início do fluxo. Quando existe um Request e um init separado, o Next.js constrói um único objeto efetivo:
if (isRequestInput && init) {
const { next, ...overrides } = init
input = new Request(input as Request, overrides)
init = next ? { next } : undefined
}
As opções next ficam separadas porque pertencem ao framework, não ao RequestInit da Web API.
Depois da normalização, a decisão de cache, a cache key e a requisição de rede observam a mesma identidade. O patch não trata apenas POST: ele restaura a regra de que o cache deve representar exatamente a operação executada.
Versões afetadas e mitigação
O advisory oficial lista como vulneráveis:
Next.js >= 13.0.0 e < 15.5.21
Next.js >= 16.0.0 e < 16.2.11
As versões corrigidas são 15.5.21 e 16.2.11. Aplicações que usam apenas Pages Router não são afetadas.
A correção recomendada é atualizar:
npm install next@15.5.21
ou:
npm install next@16.2.11
O advisory não oferece workaround geral além da atualização. Evitar o formato vulnerável ou usar cache: "no-store" reduz a exposição de um call site conhecido, mas não substitui o patch.
Como detectar
Procure chamadas em que o primeiro argumento é um Request e o segundo sobrescreve propriedades relevantes:
fetch(requestObject, {
method: "POST",
headers: dynamicHeaders,
body: dynamicBody,
cache: "force-cache",
})
A presença desse formato não prova vazamento. A revisão precisa confirmar se:
- body ou headers alteram o recurso retornado;
- a URL permanece igual entre usuários;
- a resposta contém dados específicos de conta ou tenant;
- a operação pode ser armazenada e reutilizada;
- outro usuário pode alcançar o mesmo loader ou rota.
Timeline e crédito
- 18 de maio de 2026: vulnerabilidade reportada à Vercel.
- 16 de junho de 2026: encaminhada para validação.
- 15 de julho de 2026: confirmada e premiada com CVSS 6.0.
- 21 de julho de 2026: advisory e versões corrigidas publicados.
- 23 de julho de 2026: report encerrado após o lançamento da correção.
A vulnerabilidade foi descoberta e reportada por @rafabd1, da Vyntra Research. O crédito consta no advisory oficial do Next.js.