Introduction to SignalR Security (SignalR 1.x)
by Patrick Fletcher, Tom FitzMacken
Warning
This documentation isn't for the latest version of SignalR. Take a look at ASP.NET Core SignalR.
This article describes the security issues you must consider when developing a SignalR application.
Overview
This document contains the following sections:
SignalR Security Concepts
Authentication and authorization
SignalR is designed to be integrated into the existing authentication structure for an application. It does not provide any features for authenticating users. Instead, you authenticate users as you would normally in your application, and then work with the results of the authentication in your SignalR code. For example, you might authenticate your users with ASP.NET forms authentication, and then in your hub, enforce which users or roles are authorized to call a method. In your hub, you can also pass authentication information, such as user name or whether a user belongs to a role, to the client.
SignalR provides the Authorize attribute to specify which users have access to a hub or method. You apply the Authorize attribute to either a hub or particular methods in a hub. Without the Authorize attribute, all public methods on the hub are available to a client that is connected to the hub. For more information about hubs, see Authentication and Authorization for SignalR Hubs.
The Authorize
attribute is used only with hubs. To enforce authorization rules when using a PersistentConnection
you must override the AuthorizeRequest
method. For more information about persistent connections, see Authentication and Authorization for SignalR Persistent Connections.
Connection token
SignalR mitigates the risk of executing malicious commands by validating the identity of the sender. A connection token, containing the connection id and username for authenticated users, is passed between the client and server for each request. The connection id is a unique identifier that is randomly generated by the server when a new connection is created, and is persisted for the duration of the connection. The username is provided by the authentication mechanism for the web application. The connection token is protected with encryption and a digital signature.
For each request, the server validates the contents of the token to ensure that the request is coming from the specified user. The username must correspond to the connection id. By validating both the connection id and the username, SignalR prevents a malicious user from easily impersonating another user. If the server cannot validate the connection token, the request fails.
Because the connection id is part of the verification process, you should not reveal one user's connection id to other users or store the value on the client, such as in a cookie.
Rejoining groups when reconnecting
By default, the SignalR application will automatically re-assign a user to the appropriate groups when reconnecting from a temporary disruption, such as when a connection is dropped and re-established before the connection times out. When reconnecting, the client passes a group token that includes the connection id and the assigned groups. The group token is digitally signed and encrypted. The client retains the same connection id after a reconnection; therefore, the connection id passed from the reconnected client must match the previous connection id used by the client. This verification prevents a malicious user from passing requests to join unauthorized groups when reconnecting.
However, it is important to note, that the group token does not expire. If a user belonged to a group in the past, but was banned from that group, that user may be able to mimic a group token that includes the prohibited group. If you need to securely manage which users belong to which groups, you need to store that data on the server, such as in a database. Then, add logic to your application that verifies on the server whether a user belongs to a group. For an example of verifying group membership, see Working with groups.
Automatically rejoining groups only applies when a connection is reconnected after a temporary disruption. If a user disconnects by navigating away from the application or the application restarts, your application must handle how to add that user to the correct groups. For more information, see Working with groups.
How SignalR prevents Cross-Site Request Forgery
Cross-Site Request Forgery (CSRF) is an attack where a malicious site sends a request to a vulnerable site where the user is currently logged in. SignalR prevents CSRF by making it extremely unlikely for a malicious site to create a valid request for your SignalR application.
Description of CSRF attack
Here is an example of a CSRF attack:
A user logs into
www.example.com
, using forms authentication.The server authenticates the user. The response from the server includes an authentication cookie.
Without logging out, the user visits a malicious web site. This malicious site contains the following HTML form:
<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>
Notice that the form action posts to the vulnerable site, not to the malicious site. This is the "cross-site" part of CSRF.
The user clicks the submit button. The browser includes the authentication cookie with the request.
The request runs on the example.com server with the user's authentication context, and can do anything that an authenticated user is allowed to do.
Although this example requires the user to click the form button, the malicious page could just as easily run a script that sends an AJAX request to your SignalR application. Moreover, using SSL does not prevent a CSRF attack, because the malicious site can send an "https://" request.
Typically, CSRF attacks are possible against web sites that use cookies for authentication, because browsers send all relevant cookies to the destination web site. However, CSRF attacks are not limited to exploiting cookies. For example, Basic and Digest authentication are also vulnerable. After a user logs in with Basic or Digest authentication, the browser automatically sends the credentials until the session ends.
CSRF mitigations taken by SignalR
SignalR takes the following steps to prevent a malicious site from creating valid requests to your SignalR application. These steps are taken by default and do not require any action in your code.
- Disable cross domain requests
By default, cross domain requests are disabled in a SignalR application to prevent users from calling a SignalR endpoint from an external domain. Any request that comes from an external domain is automatically considered invalid and is blocked. It is recommended that you keep this default behavior; otherwise, a malicious site could trick users into sending commands to your site. If you need to use cross domain requests, see How to establish a cross-domain connection . - Pass connection token in query string, not cookie
SignalR passes the connection token as a query string value, instead of as a cookie. By not storing the connection token as a cookie, the connection token is not inadvertently forwarded by the browser when malicious code is encountered. Also, the connection token is not persisted beyond the current connection. Therefore, a malicious user cannot make a request under another user's authentication credentials. - Verify connection token
As described in the Connection token section, the server knows which connection id is associated with each authenticated user. The server does not process any request from a connection id that does not match the user name. It is unlikely a malicious user could guess a valid request because the malicious user would have to know the user name and the current randomly-generated connection id. That connection id becomes invalid as soon as the connection is ended. Anonymous users should not have access to any sensitive information.
SignalR Security Recommendations
Secure Socket Layers (SSL) protocol
The SSL protocol uses encryption to secure the transport of data between a client and server. If your SignalR application transmits sensitive information between the client and server, use SSL for the transport. For more information about setting up SSL, see How to set up SSL on IIS 7.
Do not use groups as a security mechanism
Groups are a convenient way of collecting related users, but they are not a secure mechanism for limiting access to sensitive information. This is especially true when users can automatically rejoin groups during a reconnect. Instead, consider adding privileged users to a role and limiting access to a hub method to only members of that role. For an example of restricting access based on a role, see Authentication and Authorization for SignalR Hubs. For an example of checking user access to groups when reconnecting, see Working with groups.
Safely handling input from clients
All input from clients that is intended for broadcast to other clients must be encoded to ensure that a malicious user does not send script to other users. It is best to encode messages on the receiving clients rather than the server, because your SignalR application may have many different types of clients. Therefore, HTML-encoding works for a web client, but not for other types of clients. For example, a web client method to display a chat message would safely handle the user name and message by calling the html()
function.
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>');
};
Reconciling a change in user status with an active connection
If a user's authentication status changes while an active connection exists, the user will receive an error that states, "The user identity cannot change during an active SignalR connection." In that case, your application should re-connect to the server to make sure the connection id and username are coordinated. For example, if your application allows the user to log out while an active connection exists, the username for the connection will no longer match the name that is passed in for the next request. You will want to stop the connection before the user logs out, and then restart it.
However, it is important to note that most applications will not need to manually stop and start the connection. If your application redirects users to a separate page after logging out, such as the default behavior in a Web Forms application or MVC application, or refreshes the current page after logging out, the active connection is automatically disconnected and does not require any additional action.
The following example shows how to stop and start a connection when the user status has changed.
<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>
Or, the user's authentication status may change if your site uses sliding expiration with Forms Authentication, and there is no activity to keep the authentication cookie valid. In that case, the user will be logged out and the user name will no longer match the user name in the connection token. You can fix this problem by adding some script that periodically requests a resource on the web server to keep the authentication cookie valid. The following example shows how to request a resource every 30 minutes.
$(function () {
setInterval(function() {
$.ajax({
url: "Ping.aspx",
cache: false
});
}, 1800000);
});
Automatically generated JavaScript proxy files
If you do not want to include all of the hubs and methods in the JavaScript proxy file for each user, you can disable the automatic generation of the file. You might choose this option if you have multiple hubs and methods, but do not want every user to be aware of all of the methods. You disable automatic generation by setting EnableJavaScriptProxies to false.
var hubConfiguration = new HubConfiguration();
hubConfiguration.EnableJavaScriptProxies = false;
RouteTable.Routes.MapHubs("/signalr", hubConfiguration);
For more information about the JavaScript proxy files, see The generated proxy and what it does for you.
Exceptions
You should avoid passing exception objects to clients because the objects may expose sensitive information to the clients. Instead, call a method on the client that displays the relevant error message.
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.");
}
}