2025 WEB 정규팀 1주차

유원상·2025년 9월 10일
0

SF Web Regular

목록 보기
1/3

CVE-2025-29927

The Next.js Middleware Authorization Bypass Vulnerability

해당 CVE를 이해하기 위해서는 Next.js의 middleware에 대해 알아야 합니다.

middleware에 알기 전 Next.js에 대해 간단히 알아보겠습니다.

Next.js

먼저 Next.js란 프론트엔드부터 백엔드까지(풀스택) 웹 애플리케이션을 구축하기 위해 사용하는 React기반 프레임워크입니다.

React? 오픈소스 자바스크립트 라이브러리로, UI 구축에 사용됩니다.

라이브러리? 재사용 가능한 코드의 집합으로, 특정 기능을 수행하는 함수, 클래스, 모듈 등으로 구성되며 개발자가 필요한 기능을 호출하여 사용할 수 있습니다.

라이브러리를 사용하면 같은 코드를 여러 번 작성할 필요가 없어 편리합니다.

따라서, Next.js는 개발자가 최소한의 노력으로 최대한의 결과를 내도록 도와줍니다.

이제 middleware에 대해 알아보겠습니다.

middleware

middleware는 요청이 완료되기 전에 실행되는 함수입니다.
이를 이용해 들어오는 요청에 따라 응답을 재작성, 리디렉션, 요청 또는 응답 헤더 수정, 또는 직접 응답할 수 있습니다.

middleware는 캐시된 내용과 경로가 일치하기 전에 실행됩니다.

middleware를 사용하는 이유는 이 미들웨어를 어플리케이션에 통합시키면 성능, 보안, 사용자 경험을 향상시킬 수 있기 때문입니다.
인증 및 권한 부여, 서버 측 리디렉션, 경로 재작성, 봇 감지 등의 기능을 제공합니다.

보통 middleware에서 중요한 기능은 인증 및 권한 부여 기능입니다.

인증 및 권한 부여 : 사용자 신원을 확인하고 특정 페이지나 API 경로에 접근하기 전에 세션 쿠키를 확인합니다.

예를 들어 사용자가 중요한 페이지(/dashboard/admin)에 접속하려 하면 요청은 먼저 미들웨어를 거쳐 세션 쿠키가 유효한지 확인합니다. 유효하다면 요청을 전달하고 그렇지 않다면 사용자를 로그인 페이지 등으로 리디렉션합니다.

Explain

이 취약점은 공격자가 미들웨어 기반 인증 검사를 우회할 수 있게 하는 취약점입니다.

// 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(':') : []

middleware 파일 경로를 가져와 middlewareInfo 에 추가합니다.

const middlewareInfo = getMiddlewareInfo({
			page: middleware.page,
		})

그리고 현재 실행될 미들웨어의 경로 정보(middlewareInfo.name)가 이 미들웨어 목록에 포함되어 있다면, 해당 미들웨어는 실행되지 않고 다음 단계로 넘어갑니다.

if (subrequests.includes(middlewareInfo.name)) {
			result = {
				response: NextResponse.next(),
				waitUntil: Promise.resolve(),
			}
			continue
		}

예를 들어,

x-middleware-subrequest: middleware
1차 서브요청 x-middleware-subrequest: middleware:auth
2차 서브요청 x-middleware-subrequest: middleware:auth:logging

이렇게 3가지의 요청이 차례대로 온다고 했을때, 첫 번째 미들웨어 실행을 완료하고 배열에 middleware 를 저장합니다. 1차 서브요청에서는 middleware를 건너뛰고 auth 를 실행한 후 배열에 auth를 저장합니다. 그렇게 되면 2차 서브요청에서는 middlewar와 auth를 실행할 필요 없이 logging만 실행하면 되는 것입니다.

즉 x-middleware-subrequest, 요청에 미들웨어 파일 경로를 가진 헤더를 추가하면 이미 실행된 것으로 간주하여 미들웨어는 그 목적이 무엇이든 완전히 무시되고, 요청은 미들웨어의 영향 없이 전달되어 원래 목적지까지 전송됩니다.

next.js 12.2 버전 이전에는 미들웨어 파일의 이름과 위치가 고정되어 있었습니다.
미들웨어 파일 이름을 _middleware.ts로, 미들웨어가 pages/ 디렉토리 하위에만 존재하도록 해야만 하였습니다.

이 정보를 통해 미들웨어의 정확한 경로를 추론하고, 따라서 x-middleware-subrequest 헤더의 값을 추측할 수 있습니다. 여기서 헤더의 값은 단순 디렉토리 이름과 파일 이름으로 구성되어 있습니다.

따라서 전역 미들웨어는 pages/_middleware.ts에 위치할 가능성이 높으므로, 아래와 같은 헤더를 요청에 포함시키는 것 만으로도 해당 미들웨어를 우회할 수 있습니다.

x-middleware-subrequest: pages/_middleware

why? Next.js는 모든 파일의 파일 경로를 프로젝트 루트 기준 상대 경로 + 확장자 제거로 처리하기 때문입니다.

// 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 로 바뀌었습니다.

따라서 12.2 버전 이후로는 x-middleware-subrequest: middleware를 사용하여 우회할 수 있습니다.

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-30218

Next.js may leak x-middleware-subrequest-id to external hosts

이 취약점은 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 검증 메커니즘의 구현에서 발생합니다.

랜덤 ID가 생성되고 전역적으로 저장되지만, 이 subrequest ID가 x-middleware-subrequest-id 헤더를 통해 제 3자를 포함한 모든 요청에 전송됩니다.

만약 미들웨어가 신뢰할 수 없는 외부 서버로 이 ID를 보낸다면 서버의 관리자는 ID를 수집할 수 있을 것입니다. 공격자는 이 ID의 패턴을 분석해서 내부 흐름이나 이용자 패턴을 추론해볼 수 있지만, 공격자가 미들웨어에게 신뢰받는 외부 서버를 장악하고 있어야 하기 때문에 실질적 위협도는 낮습니다.

그러나 내부 정보가 유출되는 것은 보안 원칙상 불가하기 때문에 패치가 필요합니다.

주요 참조

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

profile
이것저것 적는 블로그입니다.

0개의 댓글