SignalR 安全性简介 (SignalR 1.x)

作者 :Patrick FletcherTom FitzMacken

警告

本文档不适用于最新版本的 SignalR。 查看 ASP.NET Core SignalR

本文介绍开发 SignalR 应用程序时必须考虑的安全问题。

概述

本文档包含以下各节:

SignalR 安全概念

身份验证和授权

SignalR 旨在集成到应用程序的现有身份验证结构中。 它不提供对用户进行身份验证的任何功能。 而是像平常在应用程序中一样对用户进行身份验证,然后在 SignalR 代码中使用身份验证结果。 例如,可以使用 ASP.NET 表单身份验证对用户进行身份验证,然后在中心强制要求哪些用户或角色有权调用方法。 在中心内,还可以将身份验证信息(例如用户名或用户是否属于角色)传递给客户端。

SignalR 提供 Authorize 属性,用于指定哪些用户有权访问中心或方法。 将 Authorize 属性应用于中心或中心中的特定方法。 如果没有 Authorize 属性,中心上的所有公共方法都可供连接到中心的客户端使用。 有关中心的详细信息,请参阅 SignalR 中心的身份验证和授权

特性 Authorize 仅用于中心。 若要在使用 时强制实施授权规则,PersistentConnectionAuthorizeRequest必须重写 方法。 有关持久连接的详细信息,请参阅 SignalR 持久连接的身份验证和授权

连接令牌

SignalR 通过验证发送方的标识来降低执行恶意命令的风险。 每个请求在客户端和服务器之间传递连接令牌(包含经过身份验证的用户的连接 ID 和用户名)。 连接 ID 是服务器在创建新连接时随机生成的唯一标识符,并在连接期间保留。 用户名由 Web 应用程序的身份验证机制提供。 连接令牌受加密和数字签名的保护。

关系图连接令牌系统,显示客户端、服务器、身份验证系统和连接令牌之间的关系。

对于每个请求,服务器都会验证令牌的内容,以确保请求来自指定用户。 用户名必须与连接 ID 相对应。通过验证连接 ID 和用户名,SignalR 可防止恶意用户轻松模拟其他用户。 如果服务器无法验证连接令牌,则请求将失败。

连接令牌系统的示意图,其中显示了客户端、服务器和保存的令牌之间的关系。

由于连接 ID 是验证过程的一部分,因此不应向其他用户显示一个用户的连接 ID,也不应将该值存储在客户端上,例如在 Cookie 中。

重新连接时重新加入组

默认情况下,SignalR 应用程序会在从临时中断重新连接时自动将用户重新分配到相应的组,例如,在连接超时之前断开连接并重新建立连接。重新连接时,客户端会传递包含连接 ID 和分配的组的组令牌。 组令牌经过数字签名和加密。 客户端在重新连接后保留相同的连接 ID;因此,从重新连接客户端传递的连接 ID 必须与客户端使用的先前连接 ID 匹配。 此验证可防止恶意用户在重新连接时传递加入未经授权的组的请求。

但是,请务必注意,组令牌不会过期。 如果用户过去属于某个组,但被禁止进入该组,该用户可能能够模拟包含禁止组的组令牌。 如果需要安全地管理哪些用户属于哪些组,则需要将这些数据存储在服务器上,例如存储在数据库中。 然后,向应用程序添加逻辑,以在服务器上验证用户是否属于某个组。 有关验证组成员身份的示例,请参阅 使用组

仅当连接在暂时中断后重新连接时,自动重新加入组才适用。 如果用户通过导航离开应用程序或应用程序重启来断开连接,则应用程序必须处理如何将该用户添加到正确的组。 有关详细信息,请参阅 使用组

SignalR 如何防止跨站点请求伪造

跨站点请求伪造 (CSRF) 是恶意站点将请求发送到用户当前登录的易受攻击的站点的攻击。 SignalR 通过使恶意站点极不可能为 SignalR 应用程序创建有效请求来阻止 CSRF。

CSRF 攻击的说明

下面是 CSRF 攻击的示例:

  1. 用户使用表单身份验证登录到 www.example.com

  2. 服务器对用户进行身份验证。 服务器的响应包括身份验证 Cookie。

  3. 如果不注销,用户将访问恶意网站。 此恶意网站包含以下 HTML 表单:

    <h1>You Are a Winner!</h1>
    <form action="http://example.com/api/account" method="post">
        <input type="hidden" name="Transaction" value="withdraw" />
        <input type="hidden" name="Amount" value="1000000" />
        <input type="submit" value="Click Me"/>
    </form>
    

    请注意,表单操作发布到易受攻击的网站,而不是恶意网站。 这是 CSRF 的“跨网站”部分。

  4. 用户单击“提交”按钮。 浏览器包含包含请求的身份验证 Cookie。

  5. 请求在具有用户身份验证上下文的 example.com 服务器上运行,并且可以执行经过身份验证的用户允许执行的任何操作。

尽管此示例要求用户单击表单按钮,但恶意页面同样可以轻松运行脚本,以便将 AJAX 请求发送到 SignalR 应用程序。 此外,使用 SSL 不会阻止 CSRF 攻击,因为恶意站点可能会发送“https://”请求。

通常,使用 Cookie 进行身份验证的网站可能会受到 CSRF 攻击,因为浏览器会将所有相关 Cookie 发送到目标网站。 但是,CSRF 攻击不仅限于利用 Cookie。 例如,基本身份验证和摘要式身份验证也容易受到攻击。 用户使用基本或摘要式身份验证登录后,浏览器会自动发送凭据,直到会话结束。

SignalR 采取的 CSRF 缓解措施

SignalR 执行以下步骤,以防止恶意站点创建对 SignalR 应用程序的有效请求。 这些步骤默认执行,不需要在代码中执行任何操作。

  • 禁用跨域请求
    默认情况下,在 SignalR 应用程序中禁用跨域请求,以防止用户从外部域调用 SignalR 终结点。 来自外部域的任何请求将自动被视为无效并被阻止。 建议保留此默认行为;否则,恶意站点可能会诱使用户向站点发送命令。 如果需要使用跨域请求,请参阅 如何建立跨域连接
  • 在查询字符串中传递连接令牌,而不是在 Cookie 中传递
    SignalR 将连接令牌作为查询字符串值传递,而不是作为 Cookie 传递。 如果不将连接令牌存储为 Cookie,则遇到恶意代码时,浏览器不会无意中转发连接令牌。 此外,连接令牌不会保留到当前连接之后。 因此,恶意用户不能使用其他用户的身份验证凭据发出请求。
  • 验证连接令牌
    连接令牌 部分所述,服务器知道哪个连接 ID 与每个经过身份验证的用户相关联。 服务器不会处理来自与用户名不匹配的连接 ID 的任何请求。 恶意用户不太可能猜到有效的请求,因为恶意用户必须知道用户名和当前随机生成的连接 ID。一旦连接结束,该连接 ID 就会变为无效。 匿名用户不应有权访问任何敏感信息。

SignalR 安全建议

SSL) 协议 (安全套接字层

SSL 协议使用加密来保护客户端和服务器之间的数据传输。 如果 SignalR 应用程序在客户端和服务器之间传输敏感信息,请使用 SSL 进行传输。 有关设置 SSL 的详细信息,请参阅 如何在 IIS 7 上设置 SSL

不要将组用作安全机制

组是收集相关用户的便捷方式,但它们不是限制对敏感信息访问的安全机制。 当用户可以在重新连接期间自动重新加入组时尤其如此。 相反,请考虑将特权用户添加到角色,并将中心方法的访问权限限制为仅该角色的成员。 有关基于角色限制访问的示例,请参阅 SignalR 中心的身份验证和授权。 有关重新连接时检查用户对组的访问权限的示例,请参阅 使用组

安全处理来自客户端的输入

必须对来自要广播到其他客户端的客户端的所有输入进行编码,以确保恶意用户不会向其他用户发送脚本。 最好在接收客户端而不是服务器上对消息进行编码,因为 SignalR 应用程序可能有许多不同类型的客户端。 因此,HTML 编码适用于 Web 客户端,但不适用于其他类型的客户端。 例如,用于显示聊天消息的 Web 客户端方法可以通过调用 html() 函数安全地处理用户名和消息。

chat.client.addMessageToPage = function (name, message) {
    // Html encode display name and message. 
    var encodedName = $('<div />').text(name).html();
    var encodedMsg = $('<div />').text(message).html();
    // Add the message to the page. 
    $('#discussion').append('<li><strong>' + encodedName
        + '</strong>:  ' + encodedMsg + '</li>');
};

通过活动连接协调用户状态的更改

如果用户的身份验证状态在活动连接存在时发生更改,用户将收到一条错误,指出“用户标识在活动的 SignalR 连接期间无法更改”。在这种情况下,应用程序应重新连接到服务器,以确保连接 ID 和用户名是协调的。 例如,如果应用程序允许用户在活动连接存在时注销,则连接的用户名将不再与为下一个请求传入的名称匹配。 你需要在用户注销之前停止连接,然后重启它。

但是,请务必注意,大多数应用程序不需要手动停止和启动连接。 如果应用程序在注销后将用户重定向到单独的页面,例如Web Forms应用程序或 MVC 应用程序中的默认行为,或者在注销后刷新当前页,则活动连接将自动断开连接,不需要任何其他操作。

以下示例演示如何在用户状态发生更改时停止和启动连接。

<script type="text/javascript">
    $(function () {
        var chat = $.connection.sampleHub;
        $.connection.hub.start().done(function () {
            $('#logoutbutton').click(function () {
                chat.connection.stop();
                $.ajax({
                    url: "Services/SampleWebService.svc/LogOut",
                    type: "POST"
                }).done(function () {
                    chat.connection.start();
                });
            });
        });
    });
</script>

或者,如果你的网站使用表单身份验证的滑动过期,并且没有活动来保持身份验证 Cookie 有效,则用户的身份验证状态可能会更改。 在这种情况下,用户将注销,用户名将不再与连接令牌中的用户名匹配。 可以通过添加一些脚本来解决此问题,这些脚本会定期请求 Web 服务器上的资源,以保持身份验证 Cookie 有效。 以下示例演示如何每 30 分钟请求一次资源。

$(function () {
    setInterval(function() {
        $.ajax({
            url: "Ping.aspx",
            cache: false
        });
    }, 1800000);
});

自动生成的 JavaScript 代理文件

如果不希望在每个用户的 JavaScript 代理文件中包括所有中心和方法,可以禁用文件的自动生成。 如果有多个中心和方法,但不希望每个用户都知道所有方法,则可以选择此选项。 通过将 EnableJavaScriptProxies 设置为 false 来禁用自动生成。

var hubConfiguration = new HubConfiguration();
hubConfiguration.EnableJavaScriptProxies = false;
RouteTable.Routes.MapHubs("/signalr", hubConfiguration);

有关 JavaScript 代理文件的详细信息,请参阅 生成的代理及其功能

异常

应避免将异常对象传递给客户端,因为这些对象可能会向客户端公开敏感信息。 而是在客户端上调用显示相关错误消息的方法。

public Task SampleMethod()
{
    try
    { 
        // code that can throw an exception
    }
    catch(Exception e)
    {
        // add code to log exception and take remedial steps

        return Clients.Caller.DisplayError("Sorry, the request could not be processed.");
    }
}