
Algumas Provas de Conceito (POCs) para CVE-2025-29927, CVE-2026-27978 e CVE-2026-29057 no Next.js.
Este repositório contém ambientes de prova de conceito reproduzíveis para três vulnerabilidades do Next.js. Cada PoC inclui um alvo vulnerável, um alvo corrigido e um script que demonstra a diferença de comportamento entre os dois.
O objetivo deste projeto é tornar a causa raiz e o impacto prático de cada problema fáceis de observar em um ambiente mínimo.
| CVE | Aviso | Impacto | Versão vulnerável | Versão corrigida |
|---|---|---|---|---|
CVE-2025-29927 | GHSA-f82v-jwr5-mffw | bypass de autorização quando o controle de acesso depende apenas de middleware | 15.2.2 | 15.2.3 |
CVE-2026-27978 | GHSA-mq59-m269-xvcx | bypass de Origin: null nas verificações de CSRF de Server Actions | 16.1.6 | 16.1.7 |
CVE-2026-29057 | GHSA-ggv3-7p47-pfv8 | contrabando de requisições HTTP (request smuggling) por meio de reescritas para um backend externo | 15.5.12 | 15.5.13 |
NEXT-16.2.4-IMAGE-REDIRECT | achado de auditoria de código-fonte local | bypass da lista de permissões remota do otimizador de imagens por meio de redirecionamentos | 16.2.4 | não verificado |
NEXT-16.2.4-IMAGE-LOCAL-REWRITE | achado de auditoria de código-fonte local | URL local do otimizador de imagens pode alcançar upstream privado por meio de reescritas externas | 16.2.4 | não verificado |
Os hashes abaixo foram obtidos do repositório upstream vercel/next.js.
| CVE | Commit do lançamento vulnerável | Commit do lançamento corrigido | Commit de correção relevante |
|---|---|---|---|
CVE-2025-29927 | v15.2.2 -> f4552826e1ed15fbeb951be552d67c5a08ad0672 | v15.2.3 -> 535e26d3c69de49df8bd17618a424cbe65ec897b | 52a078da3884efe6501613c7834a3d02a91676d2 |
CVE-2026-27978 | v16.1.6 -> adf8c612adddd103647c90ff0f511ea35c57076e | v16.1.7 -> bdf3e3577a6d55ea186a48238d61fbd8da07a626 | a27a11d78e748a8c7ccfd14b7759ad2b9bf097d8 |
CVE-2026-29057 | v15.5.12 -> d23f41c42506005fe6978e076a1ccbf8979e4925 | v15.5.13 -> cfd5f533b08df3038476dcd54f1d6d660d85f069 | dc98c04f376c6a1df76ec3e0a2d07edf4abdabd6 |
.
|- docker-compose.yml
|- pocs/
| |- cve-2025-29927/
| |- cve-2026-27978/
| |- cve-2026-29057/
| |- next-16.2.4-image-redirect-allowlist-bypass/
| `- next-16.2.4-image-local-rewrite-ssrf/
`- scripts/
|- run-cve-2025-29927.mjs
|- run-cve-2026-27978.mjs
|- run-cve-2026-29057.mjs
|- run-next-16.2.4-image-redirect-allowlist-bypass.mjs
`- run-next-16.2.4-image-local-rewrite-ssrf.mjs
docker compose esteja disponível.docker compose up --build
Portas expostas:
3001 -> CVE-2025-29927 vulnerável3002 -> CVE-2025-29927 corrigido3003 -> CVE-2026-27978 vulnerável3004 -> CVE-2026-27978 corrigido3005 -> CVE-2026-29057 vulnerável3006 -> CVE-2026-29057 corrigido3007 -> NEXT-16.2.4-IMAGE-REDIRECT3008 -> NEXT-16.2.4-IMAGE-LOCAL-REWRITENeste PoC, /dashboard é protegido apenas por middleware:
export function middleware(request) {
const session = request.cookies.get('session')?.value
if (session !== 'admin') {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
A verificação de autorização em si não está incorreta. O problema é que a rota depende inteiramente da suposição de que a execução do middleware não pode ser ignorada.
Nas versões afetadas do Next.js, requisições externas ainda podiam fornecer o cabeçalho interno x-middleware-subrequest, e o runtime tratava esse valor como metadados confiáveis de middleware. A lógica vulnerável relevante era:
const INTERNAL_HEADERS = [
'x-middleware-rewrite',
'x-middleware-redirect',
'x-middleware-set-cookie',
'x-middleware-skip',
'x-middleware-override-headers',
'x-middleware-next',
'x-now-route-matches',
'x-matched-path',
]
export const filterInternalHeaders = (headers) => {
for (const header in headers) {
if (INTERNAL_HEADERS.includes(header)) {
delete headers[header]
}
}
}
x-middleware-subrequest não era filtrado ali, portanto entrada controlada pelo atacante podia alcançar o runtime do middleware. Esse valor era então usado para derivar a profundidade de recursão:
const subreq = params.request.headers['x-middleware-subrequest']
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
const depth = subrequests.reduce(
(acc, curr) => (curr === params.name ? acc + 1 : acc),
0
)
if (depth >= MAX_RECURSION_DEPTH) {
return {
response: new Response(null, {
headers: {
'x-middleware-next': '1',
},
}),
}
}
Se um atacante enviar middleware:middleware:middleware:middleware:middleware, o runtime pode concluir que a profundidade de recursão já foi atingida e encaminhar a requisição sem executar o middleware da aplicação.
docker compose up --build cve-2025-29927-vuln cve-2025-29927-fixed
node scripts/run-cve-2025-29927.mjs http://localhost:3001
node scripts/run-cve-2025-29927.mjs http://localhost:3002
Comportamento esperado:
3001 retorna um redirecionamento sem o cabeçalho do exploit, mas retorna 200 OK com x-middleware-subrequest.3002 continua redirecionando para /login porque o cabeçalho interno não é mais confiável a partir de entrada externa.Este PoC expõe uma Server Action normal que altera o estado no lado do servidor:
'use server'
import { cookies } from 'next/headers'
import { revalidatePath } from 'next/cache'
import { recordTransfer } from '../lib/state'
export async function transferFunds(formData) {
const cookieStore = await cookies()
const session = cookieStore.get('session')?.value
if (!session) {
throw new Error('Victim session cookie is missing.')
}
const amount = Number(formData.get('amount') || '0')
recordTransfer(session, amount)
revalidatePath('/')
}
O problema não está em transferFunds() em si. O comportamento vulnerável estava na validação de CSRF do Next.js para Server Actions. Nas versões afetadas, Origin: null era tratado como uma origem ausente em vez de uma origem opaca explícita:
const originHeader = req.headers['origin']
const originDomain =
typeof originHeader === 'string' && originHeader !== 'null'
? new URL(originHeader).host
: undefined
const host = parseHostHeader(req.headers)
if (!originDomain) {
warning = 'Missing `origin` header from a forwarded Server Actions request.'
} else if (!host || originDomain !== host.value) {
if (isCsrfOriginAllowed(originDomain, serverActions?.allowedOrigins)) {
// Ignore it
} else {
const error = new Error('Invalid Server Actions request.')
// ...
}
}
Como 'null' se tornava undefined, requisições de origens opacas, como iframes com sandbox, podiam evitar o caminho de comparação host/origin e ainda ser processadas com os cookies da vítima anexados.
docker compose up --build cve-2026-27978-vuln cve-2026-27978-fixed
node scripts/run-cve-2026-27978.mjs http://localhost:3003
node scripts/run-cve-2026-27978.mjs http://localhost:3004
Comportamento esperado: