昨日(2024 年 3 月 12 日),笔者发现互联网上披露了 CVE-2023-49785 漏洞,其中指出 ChatGPT-Next-Web 在 2.11.2(本文写作时的最新版本)及以下版本存在 SSRF 漏洞。本文对该漏洞的风险、验证方式与可能的利用途径进行描述,并指出一些可缓解该漏洞的临时解决方法。
2024/3/14 更新:ChatGPT-Next-Web 的 Issues 中已经有对该 CVE 的相关讨论 ChatGPT-Next-Web#4283。项目的开发者在得知该漏洞后,已经着手对存在 SSRF 漏洞的 API 增加限制以缓解该漏洞,并发布了新版本 v2.11.3,建议私有部署 ChatGPT-Next-Web 的用户尽快升级至该最新版本。
漏洞风险
CVE-2023-49785 指出 ChatGPT-Next-Web 的 API Endpoint /api/cors
上存在 SSRF 漏洞,可被用于从服务器端发起任意网络请求并取得响应内容。
这一 SSRF 漏洞支持以 GET、POST 方法从部署 ChatGPT-Next-Web 部署的机器(或 docker 容器)对内网或外网的任意 HTTP URL 发起请求,进而造成内网服务暴露在公网的风险。
验证
阅读 ChatGPT-Next-Web 的源码可知,/api/cors
接口仅被用于云同步(跨设备同步对话记录)功能。该功能允许用户指定任意 WebDAV 服务器或 Upstash Redis 的 URL 进行数据的上传、下载,以同步对话记录。为了解决跨域问题,ChatGPT-Next-Web 提供了 /api/cors
接口对 WebDAV 操作进行转发并添加 Access-Control-*
响应 header。
要验证当前部署的 ChatGPT-Next-Web 是否受此漏洞影响,可以构造以下 URL 向 https://httpbin.org/get
发起请求,观察是否能通过该接口收到 httpbin 的响应,若是,则表明当前部署的 ChatGPT-Next-Web 受此漏洞影响。
https://next-chat.example.com/api/cors/https/httpbin.org/get
SSRF -> XSS
进一步地,由于这个 SSRF 漏洞是通过 Node.js 的 fetch
方法发起请求,因此它除了可以请求 HTTP URL 以外,也支持 Data URI。假如我们通过 Data URI 请求一段包含 <script>
标签的 HTML,那么它会被浏览器执行。也就是说,这个漏洞还可以是一个 XSS 漏洞。它允许在 ChatGPT-Next-Web 所在的域名下执行任意 JS 脚本,只需要访问特定的 URL。这使得攻击者也可以窃取 ChatGPT-Next-Web 在浏览器中保存的 API Key、聊天记录等信息。
以下 URL 会加载一段包含 JS 的 HTML,它会读取 ChatGPT-Next-Web 保存在 localStorage 中的对话历史数据。
https://next-chat.example.com/api/cors/data:text%2fhtml,%3Cscript%3Edocument.write(localStorage.getItem('chat-next-web-store'))%3C%2Fscript%3E
<script>document.write(localStorage.getItem('chat-next-web-store'))</script>
缓解
当前 ChatGPT-Next-Web 最新版本 2.11.2 并未修复该问题。如果将 ChatGPT-Next-Web 暴露到公网,有较大的安全风险。建议考虑以下操作以缓解该漏洞造成的风险:
- 若 ChatGPT-Next-Web 是通过 Nginx 或 Caddy 等 HTTP Server 的反向代理暴露到公网,可以在反向代理的配置中禁止访问 ChatGPT-Next-Web 的
/api/cors
endpoint(注意这会影响 ChatGPT-Next-Web 的云同步功能) - 限制 ChatGPT-Next-Web 仅能通过可信来源(例如内网 IP 及部分可信的外网 IP 地址)访问,避免将其完全暴露在公网
在 Caddy 中禁止访问 /api/cors
next-chat.example.com {
# ...
@nextChatCors path /api/cors*
respond @nextChatCors 403
}
在 Nginx 中禁止访问 /api/cors
server {
server_name next-chat.example.com;
# ...
location ~ ^/api/cors {
return 403;
}
}