
Some Proof-of-Concept (POCs) for CVE-2025-29927, CVE-2026-27978, and CVE-2026-29057 in Next.js.
This repository contains reproducible proof-of-concept environments for three Next.js vulnerabilities. Each PoC includes a vulnerable target, a fixed target, and a script that demonstrates the behavioral difference between the two.
The goal of this project is to make the root cause and practical impact of each issue easy to observe in a minimal environment.
| CVE | Advisory | Impact | Vulnerable Version | Fixed Version |
|---|---|---|---|---|
CVE-2025-29927 | GHSA-f82v-jwr5-mffw | authorization bypass when access control relies only on middleware | 15.2.2 | 15.2.3 |
CVE-2026-27978 | GHSA-mq59-m269-xvcx | Origin: null bypass of Server Actions CSRF checks | 16.1.6 | 16.1.7 |
CVE-2026-29057 | GHSA-ggv3-7p47-pfv8 | HTTP request smuggling through rewrites to an external backend | 15.5.12 | 15.5.13 |
NEXT-16.2.4-IMAGE-REDIRECT | local source audit finding | image optimizer remote allowlist bypass through redirects | 16.2.4 | not verified |
NEXT-16.2.4-IMAGE-LOCAL-REWRITE | local source audit finding | image optimizer local URL can reach private upstream through external rewrites | 16.2.4 | not verified |
The hashes below are taken from upstream vercel/next.js.
| CVE | Vulnerable Release Commit | Fixed Release Commit | Relevant Patch Commit |
|---|---|---|---|
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 is available.docker compose up --build
Exposed ports:
3001 -> CVE-2025-29927 vulnerable3002 -> CVE-2025-29927 fixed3003 -> CVE-2026-27978 vulnerable3004 -> CVE-2026-27978 fixed3005 -> CVE-2026-29057 vulnerable3006 -> CVE-2026-29057 fixed3007 -> NEXT-16.2.4-IMAGE-REDIRECT3008 -> NEXT-16.2.4-IMAGE-LOCAL-REWRITEIn this PoC, /dashboard is protected only by 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()
}
The authorization check itself is not incorrect. The issue is that the route depends entirely on the assumption that middleware execution cannot be skipped.
In affected Next.js versions, external requests could still supply the internal header x-middleware-subrequest, and the runtime treated that value as trusted middleware metadata. The relevant vulnerable logic was:
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 was not filtered there, so attacker-controlled input could reach the middleware runtime. That value was then used to derive recursion depth:
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',
},
}),
}
}
If an attacker sends middleware:middleware:middleware:middleware:middleware, the runtime may conclude that recursion depth has already been reached and forward the request without executing the application middleware.
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
Expected behavior:
3001 returns a redirect without the exploit header, but returns 200 OK with x-middleware-subrequest.3002 continues to redirect to /login because the internal header is no longer trusted from external input.This PoC exposes a normal Server Action that changes server-side state:
'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('/')
}
The issue is not in transferFunds() itself. The vulnerable behavior was in Next.js CSRF validation for Server Actions. In affected versions, Origin: null was treated like a missing origin instead of an explicit opaque origin:
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.')
// ...
}
}
Because 'null' became undefined, requests from opaque origins such as sandboxed iframes could avoid the host/origin comparison path and still be processed with victim cookies attached.
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
Expected behavior:
Origin: nullThis PoC rewrites /rewrites/:path* to an external backend: