Todos os registros
Registro técnico

CVE-2026-64648

Severidade6.0 / MédiaStatusCorrigida

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
Publicada em: 21 de jul. de 2026Leitura: 5 minAdvisory oficial

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 fetch no servidor pelo App Router;
  • passar um objeto Request como 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" },
}
  1. A vítima acessa a rota.
  2. A aplicação autoriza org=victim.
  3. O Next.js consulta uma chave baseada no Request base e encontra um miss.
  4. O upstream recebe POST body=org=victim.
  5. A resposta privada é armazenada sob a chave incompleta.
  6. O atacante acessa a mesma rota com sua própria sessão.
  7. A aplicação autoriza org=attacker, mas a consulta encontra a entrada da vítima.
  8. 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.

Pesquisa e reporte@rafabd1