CVE-2025-29927
명칭: Next.js Middleware 인증/권한 우회
Next.js에서 미들웨어(Middleware)에만 의존해서 인증/권한 체크를 해둔 경우, 공격자가 특정 내부용 헤더를 흉내 내서 미들웨어 실행을 ‘건너뛰게’ 만들고 보호된 페이지에 접근할 수 있는 취약점입니다.
미들웨어(Middleware)가 뭐지?
Next.js 문서 기준 Middleware는 요청이 라우트(페이지/핸들러)에 도착하기 전에 실행되는 코드라고 정의가 되어있습니다.
무슨 일을 하는데?
- 로그인했는지 확인 → 안 했으면 /login으로 보내기
- 인증 쿠키/토큰이 없으면 NextResponse.redirect(new URL('/login', req.url)) 같은 식으로 로그인으로 보냄
- 특정 권한(관리자)인지 확인 → 아니면 403
- 로그인은 했어도 role이 admin이 아니면 return new Response('Forbidden', {status:403}) 등으로 차단
- “접근 제어(Authorization)”를 미들웨어에 넣는 대표 사례입니다.
- 국가/언어별 리다이렉트 처리
- 예: Accept-Language, GeoIP 기반으로 /en, /ko로 보내거나, 특정 국가에서만 페이지 노출
- 보안 헤더 추가
- CSP, X-Frame-Options, HSTS 같은 헤더를 응답에 추가해 전역 보안 정책을 적용
서브리퀘스트(subrequest)
미들웨어는 렌더링(페이지 처리)과 분리된 별도 프로세스/단계에서 동작합니다.
Next.js는 무한 재귀(루프)를 막기 위해 내부적으로 x-middleware-subrequest 헤더를 사용합니다.
쉽게 말해,
- 사용자가 /admin 요청
- 미들웨어가 뭔가 확인하다가 내부적으로 다시 앱의 다른 URL을 한번 더 요청(혹은 rewite)
- 이때 미들웨어가 또 그 요청을 잡아 내부 요청을 만들고.. 와 같은 루프가 생길 수 있음
- 그래서 '이건 내부에서 만든 서브리퀘스트다(미들웨어 이미 한번 실행했다)'라고 표시하려고 만든 내부용 헤더가
x-middleware-subrequest
x-middleware-subrequest
이 헤더는 개발자가 이 헤더를 "직접 작성해서 클라이언트에 보내라고" 만든 것이 아닙니다.
원래 의도는 Next.js 내부에서 자동으로 붙이는 내부 신호입니다.
정상 설계 의도
- 라우터/서버는 요청을 받을 때
- "이 요청이 사용자가 처음 보낸 것인가?"
- "이 요청이 미들웨어가 내부에서 만든 서브리퀘스트인가?"
를 구분해야 합니다.
그런데 여기서 Next.js가 그 구분을 x-middleware-subrequest 헤더 하나에 의존을 해서 문제가 발생합니다.
즉, 외부 사용자가 그 헤더를 마음대로 붙여도 "내부 요청"으로 오해하고 미들웨어 실행을 스킵합니다.
따라서 미들웨어에 넣어둔 인증/권한 체크가 무력화될 수 있습니다.
공격 방식
Background
예를 들어, 요청이 이렇게 들어온다고 가정해 봅시다.
x-middleware-subreqeust:middleware:middeware:middleware
HTTP 관점에서는 이 값은 문자열 한 덩어리입니다.
그런데 Next.js 내부 코드에서 이 문자열을 : 로 split 해서 리스트로 만들었습니다.
- 헤더 문자열:"middleware:middleware:middleware"
- split 결과: ["middleware", "middleware", "middeware"]
Next.js 내부에서는 재귀 제한값(기본 5) 같은 로직이 존재했었습니다.
Next.js는 x-middleware-subreqeust를 split 한 리스트에서 "현재 미들웨어 이름이 몇 번 등장하는 세어 재귀를 판단" 했습니다.
그래서 공격 흐름은 이렇습니다
- 헤더 값을 가져온다
- : 로 split 한다
- 리스트에서 미들웨어 이름이 같은 값이 몇 번 있는지 카운트
- 그 횟수가 MAX_RECURSION_DEPTH(기본 5) 이상이면 루프 방지 목적으로 미들웨어 실행을 중단하고 다음 단계로 넘어감
x-middleware-subreqeust:middleware:middeware:middleware:middleware:middleware
##그럼 x-middleware-subrequest : 5는 안 되나요?
먼저, Next.js는 이 헤더를 숫자로 해석하지 않습니다.
문자열을 split 해서 리스트로 만들고, 그 안에 미들웨어 이름이 몇 번 등장했는지를 세기 때문에
x-middleware-subrequest: 5를 해도 ["5"]가 되기에 카운트가 되지 않습니다.
해결 방안
외부 요청 헤더 차단
: 외부 요청에서 x-middleware-subrequest 가 들어오는 것을 차단/제거
이중 검증
: 중요한 데이터/API는 라우터/핸들러/백엔드 등에서도 세션/권한을 다시 확인하는 방식 적용
실제 패치?
1) 프로세스(세션)마다 랜덤 ID 생성
Next.js 서버가 뜰 때, 랜덤 바이트로 만든 hex 문자열을 하나 생성해 전역 심볼로 저장
- crypto.getRandomValues(...)
- Buffer.from(...).toString('hex')
- 전역에 Symbol.for('@next/middleware-subrequest-id')로 보관
2) 새 내부 헤더 x-miideware-subrequest-id로 "정상 내부 요청"을 증명
- 요청에 x-middleware-subrequest가 있는데
- x-middleware-subrequest-id가 전역에 저장된 랜덤 ID와 일치하지 않으면
- x-middleware-subrequest를 삭제(delete)
즉, 외부 공격자가 증명용 ID 헤더를 맞출 수 없으니 x-middleware-subrequest가 제거되어 미들웨어 스킵 조건이 성립 x
참고 문헌
- NVD CVE-2025-29927
- GitHub Security Advisory (vercel/next.js)
- Vercel Postmortem
- SK쉴더스 EQST Insight
- 꼰머의 보안공부 정리
- GitHub PoC 저장소(동작 개념 참고)
- Cloudflare WAF 업데이트/권고 버전
- Next.js 공식 Middleware 문서
'Web Study' 카테고리의 다른 글
| CVE-2022-29078 (0) | 2026.02.19 |
|---|---|
| Prototype Pollution (0) | 2026.02.14 |
| XS - Search (1) | 2026.01.07 |
| XXE Injection (0) | 2026.01.04 |
| HTTP request Smuggling (0) | 2026.01.03 |