「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

介绍

场景

近年,不论是正在快速增长的直播,远程教育以及IM聊天场景,还是在常规企业级系统中用到的系统提醒,对websocket的需求越来越大,对websocket的要求也越来越高。从早期对websocket的应用仅限于少部分功能和IM等特殊场景,逐步发展为追求支持高并发,百万、千万级每秒通讯的高可用websocket服务。

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

相比于 Http 的单项通信方式,WebSocket 可以从服务器向浏览器主动推送消息,这一特性可以帮助我们完成诸如:订单消息推送、IM实时聊天 等一些特定业务。

数据通信

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

安全问题

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 劫持危害大,但容易被开发人员忽视。

「IM系列」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("认证授权和实现思路");认证授权和实现思路
};

客户端

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

服务端

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

以上截图可以看出HTTP_ORIGIN请求源不合法,链接被断开链接了

合法 HTTP_ORIGIN

服务端

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

客户端

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

签名认证

签名函数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.");
};

携带签名连接

客户端

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

服务端

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

不携带签名或者签名错误

客户端

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

服务端

「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

上一章节:「IM系列」WebSocket教程:响应格式规范与异常处理

源码

文章相关源码地址:https://github.com/Tinywan/webman-admin


原文始发于微信公众号(开源技术小栈):「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/247982.html

(0)
李, 若俞的头像李, 若俞

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!