해당 CVE를 이해하기 위해서는 Next.js의 middleware에 대해 알아야 합니다.
middleware에 알기 전 Next.js에 대해 간단히 알아보겠습니다.
먼저 Next.js란 프론트엔드부터 백엔드까지(풀스택) 웹 애플리케이션을 구축하기 위해 사용하는 React기반 프레임워크입니다.
여기서 React란? 오픈소스 자바스크립트 라이브러리로, UI 구축에 사용됩니다.
여기서 라이브러리란? 재사용 가능한 코드의 집합으로, 특정 기능을 수행하는 함수, 클래스, 모듈 등으로 구성되며 개발자가 필요한 기능을 호출하여 사용할 수 있습니다.
라이브러리를 사용하면 같은 코드를 여러 번 작성할 필요가 없어 편리합니다.
따라서, Next.js는 개발자가 최소한의 노력으로 최대한의 결과를 내도록 도와줍니다.
이제 middleware에 대해 알아보겠습니다.
middleware는 요청이 완료되기 전에 실행되는 함수입니다.
이를 이용해 수신되는 요청에 따라 요청 또는 응답 헤더를 재작성, 리디렉션, 수정하거나 직접 응답하는 등 응답을 수정할 수 있습니다
middleware는 캐시된 내용과 경로가 일치하기 전에 실행됩니다.
middleware를 사용하는 이유는 이 미들웨어를 어플리케이션에 통합시키면 성능, 보안, 사용자 경험을 향상시킬 수 있기 때문입니다.
인증 및 권한 부여, 서버 측 리디렉션, 경로 재작성, 봇 감지 등의 기능을 제공합니다.
보통 middleware에서 중요한 기능은 인증 및 권한 부여 기능입니다.
예를 들어 사용자가 중요한 페이지(/dashboard/admin)에 접속하려 하면 요청은 먼저 미들웨어를 거쳐 세션 쿠키가 유효한지 확인합니다. 유효하다면 요청을 전달하고 그렇지 않다면 사용자를 로그인 페이지 등으로 리디렉션합니다.
이 취약점은 공격자가 미들웨어 기반 인증 검사를 우회할 수 있게 하는 취약점입니다.
// v12.0.7
const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
const allHeaders = new Headers()
let result: FetchEventResult | null = null
for (const middleware of this.middleware || []) {
if (middleware.match(params.parsedUrl.pathname)) {
if (!(await this.hasMiddleware(middleware.page, middleware.ssr))) {
console.warn(`The Edge Function for ${middleware.page} was not found`)
continue
}
await this.ensureMiddleware(middleware.page, middleware.ssr)
const middlewareInfo = getMiddlewareInfo({
dev: this.renderOpts.dev,
distDir: this.distDir,
page: middleware.page,
serverless: this._isLikeServerless,
})
if (subrequests.includes(middlewareInfo.name)) {
result = {
response: NextResponse.next(),
waitUntil: Promise.resolve(),
}
continue
}
}
미들웨어는 주요 기능 외에도 x-middleware-subrequest 헤더의 값을 가져와 미들웨어를 적용해야 하는지 여부를 확인하는 데 사용됩니다.
클라이언트에서 보낸 x-middleware-subrequest 헤더를 읽어, : 를 기준으로 분리된 값들을 이미 실행된 미들웨어 목록으로 간주합니다. (미들웨어 체인에서 무한 루프 방지)
const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
그리고 현재 실행될 미들웨어의 경로 정보(middlewareInfo.name)가 이 미들웨어 목록에 포함되어 있다면, 해당 미들웨어는 실행되지 않고 다음 단계로 넘어갑니다.
if (subrequests.includes(middlewareInfo.name)) {
result = {
response: NextResponse.next(),
waitUntil: Promise.resolve(),
}
continue
}
즉 x-middleware-subrequest, 요청에 올바른 값을 가진 헤더를 추가하면 미들웨어는 그 목적이 무엇이든 완전히 무시되고, 요청은 미들웨어의 영향 없이 전달되어 원래 목적지까지 전송됩니다.
next.js 12.2 버전 이전에는 규칙상 미들웨어 파일 이름을 _middleware.ts로 설정해야 했습니다.
이 정보를 통해 미들웨어의 정확한 경로를 추론하고, 따라서 x-middleware-subrequest 헤더의 값을 추측할 수 있습니다. 여기서 헤더의 값은 단순 디렉토리 이름과 파일 이름으로 구성되어 있습니다.
따라서 예를 들자면 전역 미들웨어는 pages/_middleware.ts에 위치할 가능성이 높으므로, 아래와 같은 헤더를 요청에 포함시키는 것 만으로도 해당 미들웨어를 우회할 수 있습니다.
x-middleware-subrequest: pages/_middleware
// v15.1.7
export const run = withTaggedErrors(async function runWithTaggedErrors(params) {
const runtime = await getRuntimeContext(params)
const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
const MAX_RECURSION_DEPTH = 5
const depth = subrequests.reduce(
(acc, curr) => (curr === params.name ? acc + 1 : acc),
0
)
if (depth >= MAX_RECURSION_DEPTH) {
return {
waitUntil: Promise.resolve(),
response: new runtime.context.Response(null, {
headers: {
'x-middleware-next': '1',
},
}),
}
이전과 마찬가지로 x-middleware-subrequest 헤더값을 : 기호를 기준으로 분리해 배열로 처리합니다. 하지만 이번에는 미들웨어 규칙을 우회하고 요청을 직접 전달하는 조건이 달라졌습니다. 12.2 버전 이후로는 미들웨어 파일 이름이 middleware.ts 로 바뀌었습니다.
MAX_RECURSION_DEPTH라는 상수(5)는, 재귀적으로 미들웨어가 실행되는 최대 깊이를 제한합니다. 요청에 포함된 x-middleware-subrequest 헤더의 각 항목 중, 현재 실행 중인 미들웨어의 경로(params.name)와 일치하는 값이 있을 때마다 depth 값이 1씩 증가합니다.
depth >= MAX_RECURSION_DEPTH 조건을 만족하면, 해당 미들웨어는 실행되지 않고, 요청이 바로 다음 단계로 전달됩니다. 예를 들어 middleware.ts가 미들웨어 경로라면, 다음과 같은 헤더를 삽입함으로써 이를 우회할 수 있습니다.
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
만약, src 디렉토리에 애플리케이션 코드를 배치한다면?
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
을 사용하여 우회할 수 있습니다.
이제 실제로 가능한지 테스트해보겠습니다.
깃허브에서 테스트용 서버를 받아와 로컬에서 실행시켰고, protected page로 들어가려고 하면 인증되지 않아 미들웨어에서 다시 위 페이지로 리다이렉션 시키는 형태입니다.
따라서 미들웨어를 우회하면 protected page에 접속할 수 있을 것입니다.
burp suite를 이용해 우회를 시도해보겠습니다.
protected page로 가는 요청을 보냈고, 위에서 배운대로
x-middleware-subrequest: middleware
를 사용하여 우회를 시도해보겠습니다.
우회에 성공하였습니다.
https://zhero-web-sec.github.io/research-and-things/nextjs-and-the-corrupt-middleware
https://hackyboiz.github.io/2025/03/27/bekim/2025-03-27/
https://github.com/aydinnyunus/CVE-2025-29927
이 취약점은 CVE-2025-29927을 수정하는 과정에서 발견되었습니다.
// packages/next/server/next-server.ts 코드 중 일부
랜덤 16진수 16자리 문자열을 생성해 전역 심볼에 저장합니다.
CVE-2025-29927을 완화하기 위해 Next.js는 x-middleware-subrequest-id라는 헤더를 검증하는 메커니즘을 도입했습니다. 이 미들웨어 순환 참조 방지를 위한 고유 ID는 여러 수신 요청을 받는 동안 지속되도록 설계되었습니다.
// packages/next/server/web/sandbox/context.ts 코드 중 일부
전역 심볼에서 이전에 저장한 ID를 가져와 헤더 값으로 설정합니다.
하지만 이 subrequest ID는 Next.js 애플리케이션과 동일한 호스트가 아닌 목적지라도 모든 요청에 전송됩니다. 미들웨어 내에서 서드파티로 fetch 요청을 시작하면 x-middleware-subrequest-id가 해당 서드파티로 전송됩니다.
이 취약점은 subrequest ID 검증 메커니즘의 구현에서 발생합니다.
crypto.getRandomValues()를 사용하여 랜덤 ID가 생성되고 전역적으로 저장되지만, 이 subrequest ID가 x-middleware-subrequest-id 헤더를 통해 제 3자를 포함한 모든 요청에 전송됩니다.
그러나 공격자가 서드파티를 제어해야 하므로 실질적인 위협 가능성은 낮습니다.
하지만 결국 내부 정보가 유출되는 것이기 때문에, 패치가 필요합니다.
https://vercel.com/changelog/cve-2025-30218
https://nvd.nist.gov/vuln/detail/CVE-2025-30218
https://github.com/vercel/next.js/commit/63c7985774ff11e8d674ea110e378080db5a3f6d
https://github.com/vercel/next.js/blob/v15.2.3/packages/next/src/server/web/sandbox/sandbox.ts