
Alcuni Proof-of-Concept (POCs) per CVE-2025-29927, CVE-2026-27978 e CVE-2026-29057 in Next.js.
Questo repository contiene ambienti proof-of-concept riproducibili per tre vulnerabilità di Next.js. Ogni PoC include un target vulnerabile, un target corretto e uno script che dimostra la differenza comportamentale tra i due.
L'obiettivo di questo progetto è rendere facile osservare la causa principale e l'impatto pratico di ogni problema in un ambiente minimale.
| CVE | Advisory | Impatto | Versione vulnerabile | Versione corretta |
|---|---|---|---|---|
CVE-2025-29927 | GHSA-f82v-jwr5-mffw | bypass dell'autorizzazione quando il controllo degli accessi si basa solo sul middleware | 15.2.2 | 15.2.3 |
CVE-2026-27978 | GHSA-mq59-m269-xvcx | bypass di Origin: null dei controlli CSRF delle Server Actions | 16.1.6 | 16.1.7 |
CVE-2026-29057 | GHSA-ggv3-7p47-pfv8 | contrabbando di richieste HTTP tramite riscritture verso un backend esterno | 15.5.12 | 15.5.13 |
NEXT-16.2.4-IMAGE-REDIRECT | reperto di audit sorgente locale | bypass della whitelist remota dell'Image Optimizer tramite reindirizzamenti | 16.2.4 | non verificato |
NEXT-16.2.4-IMAGE-LOCAL-REWRITE | reperto di audit sorgente locale | l'URL locale dell'Image Optimizer può raggiungere un upstream privato tramite riscritture esterne | 16.2.4 | non verificato |
Gli hash sotto sono presi dall'upstream vercel/next.js.
| CVE | Commit di rilascio vulnerabile | Commit di rilascio corretto | Commit di correzione rilevante |
|---|---|---|---|
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 sia disponibile.docker compose up --build
Porte esposte:
3001 -> CVE-2025-29927 vulnerabile3002 -> CVE-2025-29927 corretto3003 -> CVE-2026-27978 vulnerabile3004 -> CVE-2026-27978 corretto3005 -> CVE-2026-29057 vulnerabile3006 -> CVE-2026-29057 corretto3007 -> NEXT-16.2.4-IMAGE-REDIRECT3008 -> NEXT-16.2.4-IMAGE-LOCAL-REWRITEIn questo PoC, /dashboard è protetto solo dal 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()
}
Il controllo di autorizzazione di per sé non è errato. Il problema è che la route dipende interamente dal presupposto che l'esecuzione del middleware non possa essere saltata.
Nelle versioni interessate di Next.js, le richieste esterne potevano comunque fornire l'header interno x-middleware-subrequest, e il runtime trattava quel valore come metadati del middleware fidati. La logica vulnerabile rilevante 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 non veniva filtrato lì, quindi l'input controllato dall'attaccante poteva raggiungere il runtime del middleware. Quel valore veniva poi utilizzato per derivare la profondità di ricorsione:
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 un attaccante invia middleware:middleware:middleware:middleware:middleware, il runtime potrebbe concludere che la profondità di ricorsione è già stata raggiunta e inoltrare la richiesta senza eseguire il middleware dell'applicazione.
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 previsto:
3001 restituisce un reindirizzamento senza l'header di exploit, ma restituisce 200 OK con x-middleware-subrequest.3002 continua a reindirizzare a /login perché l'header interno non è più considerato attendibile dall'input esterno.Questo PoC espone una normale Server Action che modifica lo stato lato server:
'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('/')
}
Il problema non è in transferFunds() stesso. Il comportamento vulnerabile risiedeva nella validazione CSRF di Next.js per le Server Actions. Nelle versioni interessate, Origin: null veniva trattato come un'origine mancante invece che come un'origine opaca esplicita:
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)) {
// Ignora
} else {
const error = new Error('Invalid Server Actions request.')
// ...
}
}
Poiché 'null' diventava undefined, le richieste da origini opache come iframe sandbox potevano evitare il confronto host/origine ed essere comunque elaborate con i cookie della vittima allegati.
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 previsto: