
Quelques preuves de concept (POCs) pour CVE-2025-29927, CVE-2026-27978 et CVE-2026-29057 dans Next.js.
Ce dépôt contient des environnements reproductibles de preuve de concept pour trois vulnérabilités Next.js. Chaque PoC inclut une cible vulnérable, une cible corrigée et un script qui montre la différence de comportement entre les deux.
L'objectif de ce projet est de rendre la cause racine et l'impact pratique de chaque problème faciles à observer dans un environnement minimal.
| CVE | Avis | Impact | Version vulnérable | Version corrigée |
|---|---|---|---|---|
CVE-2025-29927 | GHSA-f82v-jwr5-mffw | contournement d'autorisation lorsque le contrôle d'accès repose uniquement sur un middleware | 15.2.2 | 15.2.3 |
CVE-2026-27978 | GHSA-mq59-m269-xvcx | contournement par Origin: null des vérifications CSRF des Server Actions | 16.1.6 | 16.1.7 |
CVE-2026-29057 | GHSA-ggv3-7p47-pfv8 | injection de requêtes HTTP via des réécritures vers un backend externe | 15.5.12 | 15.5.13 |
NEXT-16.2.4-IMAGE-REDIRECT | constatation d'audit local | contournement de la liste blanche distante de l'optimiseur d'images via des redirections | 16.2.4 | non vérifié |
NEXT-16.2.4-IMAGE-LOCAL-REWRITE | constatation d'audit local | l'URL locale de l'optimiseur d'images peut atteindre un amont privé via des réécritures externes | 16.2.4 | non vérifié |
Les hash ci-dessous proviennent du dépôt amont vercel/next.js.
| CVE | Commit de version vulnérable | Commit de version corrigée | Commit de correctif pertinent |
|---|---|---|---|
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
Ports exposés :
3001 -> CVE-2025-29927 vulnérable3002 -> CVE-2025-29927 corrigé3003 -> CVE-2026-27978 vulnérable3004 -> CVE-2026-27978 corrigé3005 -> CVE-2026-29057 vulnérable3006 -> CVE-2026-29057 corrigé3007 -> NEXT-16.2.4-IMAGE-REDIRECT3008 -> NEXT-16.2.4-IMAGE-LOCAL-REWRITEDans ce PoC, /dashboard n'est protégé que par un 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()
}
Le contrôle d'autorisation lui-même n'est pas incorrect. Le problème est que la route dépend entièrement de l'hypothèse que l'exécution du middleware ne peut pas être contournée.
Dans les versions affectées de Next.js, les requêtes externes pouvaient toujours fournir l'en-tête interne x-middleware-subrequest, et le runtime traitait cette valeur comme des métadonnées de middleware de confiance. La logique vulnérable pertinente était :
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'y était pas filtré, donc une entrée contrôlée par l'attaquant pouvait atteindre le runtime du middleware. Cette valeur était ensuite utilisée pour dériver la profondeur de récursion :
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 attaquant envoie middleware:middleware:middleware:middleware:middleware, le runtime peut conclure que la profondeur de récursion a déjà été atteinte et transmettre la requête sans exécuter le middleware de l'application.
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
Comportement attendu :
3001 renvoie une redirection sans l'en-tête d'exploitation, mais renvoie 200 OK avec x-middleware-subrequest.3002 continue de rediriger vers /login car l'en-tête interne n'est plus considéré comme fiable depuis une entrée externe.Ce PoC expose une Server Action normale qui modifie l'état côté serveur :
'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('/')
}
Le problème ne vient pas de transferFunds() elle-même. Le comportement vulnérable se situait dans la validation CSRF des Server Actions de Next.js. Dans les versions affectées, Origin: null était traité comme une origine manquante au lieu d'une origine opaque explicite :
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.')
// ...
}
}
Parce que 'null' devenait undefined, les requêtes provenant d'origines opaques telles que des iframes sandboxed pouvaient éviter le chemin de comparaison hôte/origine et être tout de même traitées avec les cookies de la victime.
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
Comportement attendu :