介绍
场景
近年,不论是正在快速增长的直播,远程教育以及IM聊天场景,还是在常规企业级系统中用到的系统提醒,对websocket的需求越来越大,对websocket的要求也越来越高。从早期对websocket的应用仅限于少部分功能和IM等特殊场景,逐步发展为追求支持高并发,百万、千万级每秒通讯的高可用websocket服务。
相比于 Http 的单项通信方式,WebSocket 可以从服务器向浏览器主动推送消息,这一特性可以帮助我们完成诸如:订单消息推送、IM实时聊天 等一些特定业务。
数据通信
安全问题
WebSocket 本身对 身份认证 并没有提供直接的支持,对客户端的连接默认是 来者不拒。
WebSocket作为一种通信协议引入到Web应用中,并不会解决Web应用中存在的安全问题,因此WebSocket应用的安全实现是由开发者或服务端负责。这就要求开发者了解WebSocket应用潜在的安全风险,以及如何做到安全开发规避这些安全问题。
认证
WebSocket 协议没有规定服务器在握手阶段应该如何认证客户端身份。服务器可以采用任何 HTTP 服务器的客户端身份认证机制,如 cookie认证,HTTP 基础认证,TLS 身份认证等。在WebSocket应用认证实现上面临的安全问题和传统的Web应用认证是相同的,如:CVE-2015-0201, Spring框架的Java SockJS客户端生成可预测的会话ID,攻击者可利用该漏洞向其他会话发送消息,CVE-2015-1482, Ansible Tower未对用户身份进行认证,远程攻击者通过websocket连接获取敏感信息。
授权
同认证一样,WebSocket协议没有指定任何授权方式,应用程序中用户资源访问等的授权策略由服务端或开发者实现。WebSocket应用也会存在和传统Web应用相同的安全风险,如:垂直权限提升和水平权限提升。
跨域请求
WebSocket使用基于源的安全模型,在发起WebSocket握手请求时,浏览器会在请求中添加一个名为Origin的HTTP头,Oringin字段表示发起请求的源,以此来防止未经授权的跨站点访问请求。WebSocket 的客户端不仅仅局限于浏览器,因此 WebSocket 规范没有强制规定握手阶段的 Origin 头是必需的,并且WebSocket不受浏览器同源策略的限制。
如果服务端没有针对Origin头部进行验证可能会导致跨站点WebSocket劫持攻击。该漏洞最早在 2013 年被Christian Schneider 发现并公开,Christian 将之命名为跨站点 WebSocket 劫持 (Cross Site WebSocket Hijacking)(CSWSH)。跨站点 WebSocket 劫持危害大,但容易被开发人员忽视。
图片来源:腾讯安全应急响应中心(Tencent Security Response Center)
上图展示了跨站WebSocket劫持的过程,某个用户已经登录了WebSocket应用程序,如果他被诱骗访问了某个恶意网页,而恶意网页中植入了一段js代码,自动发起 WebSocket 握手请求跟目标应用建立 WebSocket 连接。注意到,Origin 和 Sec-WebSocket-Key 都是由浏览器自动生成的,浏览器再次发起请求访问目标服务器会自动带上Cookie 等身份认证参数。
如果服务器端没有检查Origin头,则该请求会成功握手切换到 WebSocket 协议,恶意网页就可以成功绕过身份认证连接到 WebSocket 服务器,进而窃取到服务器端发来的信息,或者发送伪造信息到服务器端篡改服务器端数据。与传统跨站请求伪造(CSRF)攻击相比,CSRF 主要是通过恶意网页悄悄发起数据修改请求,而跨站 WebSocket 伪造攻击不仅可以修改服务器数据,还可以控制整个双向通信通道。也正是因为这个原因,Christian 将这个漏洞命名为劫持(Hijacking),而不是请求伪造(Request Forgery)。
理解了跨站WebSocket劫持攻击的原理和过程,那么如何防范这种攻击呢?处理也比较简单,在服务器端的代码中增加 对Origin头的检查,如果客户端发来的 Origin 信息来自不同域,服务器端可以拒绝该请求。但是仅仅检查 Origin 仍然是不够安全的,恶意网页可以伪造Origin头信息,绕过服务端对Origin头的检查,更完善的解决方案可以借鉴CSRF的解决方案-令牌机制。
授权实现
Origin头的检查
修改配置文件
configpluginwebmangateway-workerprocess.php
进程配置文件,修改网关gateway
配置onConnect
链接配置回调函数。
...
'gateway' => [
'handler' => Gateway::class,
'listen' => 'websocket://0.0.0.0:8783',
'count' => cpu_count(),
'reloadable' => false,
'constructor' => ['config' => [
'lanIp' => '127.0.0.1',
'startPort' => 2300,
'pingInterval' => 25,
'pingData' => '{"type":"ping"}',
'registerAddress' => '127.0.0.1:12306',
'onConnect' => function ($connection) {
$connection->onWebSocketConnect = function ($connection, $header) {
/** 1. HTTP_ORIGIN 请求头合法性校验 */
// var_dump($_SERVER);
// 判断连接来源是否合法,不合法就关掉连接(Jmeter压测暂时注释掉)
if (!isset($_SERVER['HTTP_ORIGIN'])) {
echo ' [x] [ORIGIN合法检测] 未定义HTTP_ORIGIN ', "n";
return $connection->close();
}
// 判断连接来源是满足条件,不合法就关掉连接
// if ($_SERVER['HTTP_ORIGIN'] != 'http://127.0.0.1:8783') {
if ($_SERVER['HTTP_ORIGIN'] != 'https://tinywan.com') {
echo ' [x] [ORIGIN合法检测] HTTP_ORIGIN不满足条件', "n";
return $connection->close();
}
echo ' [x] [ORIGIN合法检测] HTTP_ORIGIN验证通过啦!!!', "n";
/** 2. 认证签名校验 */
if (!isset($_GET['sign']) || !isset($_GET['ts'])) {
echo ' [x] [签名认证] 未携带签名或时间戳参数', "n";
return $connection->close();
}
// 秘钥可以通过配置文件或者Redis读取
$secret = 'Tinywan2024';
$serverSign = sha1($_GET['ts'].'|'.$secret);
if ($_GET['sign'] != $serverSign) {
echo ' [x] [签名认证] 签名认证失败', "n";
return $connection->close();
}
echo ' [x] [签名认证] 验证通过啦!!!!!!!!', "n";
return true;
};
},
]]
],
...
非法HTTP_ORIGIN
测试代码
var ws = new WebSocket("ws://127.0.0.1:8783");
ws.onopen = function(evt) {
ws.send("认证授权和实现思路");认证授权和实现思路
};
客户端
服务端
以上截图可以看出HTTP_ORIGIN
请求源不合法,链接被断开链接了
合法 HTTP_ORIGIN
服务端
客户端
签名认证
签名函数get_wss_sign()
/**
* @desc: 获取websocket连接签名
* @return array
* @author Tinywan(ShaoBo Wan)
*/
function get_wss_sign(): array
{
// ts = 生成链接的时间+有效时间
$ts = time() + 360;
$secret = 'Tinywan2024';
return [
'sign' => sha1($ts.'|'.$secret),
'ts' => $ts
];
}
客户端连接代码
var ws = new WebSocket("ws://127.0.0.1:8783/?ts=1701697325&sign=3c99ce96521602cf54df53f65cc07b977e33a27c");
ws.onopen = function(evt) {
console.log("Connection open ...");
let $_content = {
"mode": 1,
"from_username": "Tinywan",
"to_user_id": "10000",
"content": "Hi, 开源技术小栈",
};
ws.send(JSON.stringify($_content));
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
携带签名连接
客户端
服务端
不携带签名或者签名错误
客户端
服务端
源码
文章相关源码地址:https://github.com/Tinywan/webman-admin
原文始发于微信公众号(开源技术小栈):「IM系列」WebSocket教程:安全授权认证详解和简单实现思路
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/247982.html