Preventing Cross-Site WebSocket Hijacking in ASP.NET Core

In my previous post I've written about subprotocols in WebSocket protocol. This time I wanted to focus on Cross-Site WebSocket Hijacking vulnerability.

Cross-Site WebSocket Hijacking

The WebSocket protocol is not a subject to same-origin policy. The specification states that "Servers that are not intended to process input from any web page but only for certain sites SHOULD verify the |Origin| field is an origin they expect.". This means that browser will allow any page to open a WebSocket connection.

Let's imagine a scenario in which the application is sending sensitive data over a WebSocket and authentication is based on cookies which are being send as a part of the initial handshake. In such case if user visits a malicious page while being logged to the application that page can open an authenticated WebSocket connection because browser will automatically send all the cookies. This is quite common and (if not protected) dangerous scenario. There are also more "interesting" scenarios possible like this case of remote code execution.

Protecting against CSWSH

Protection against CSWSH is easy to implement. As the Origin header is required part of initial handshake the application should check its value against list of acceptable origins and if it's not there respond with 403 Forbidden status code. The sample from my previous post was using WebSocketConnectionsMiddleware for handling the connections which makes it a perfect place to add this check.

public class WebSocketConnectionsOptions
{
    public HashSet<string> AllowedOrigins { get; set; }
}

public class WebSocketConnectionsMiddleware
{
    private WebSocketConnectionsOptions _options;
    ...

    public WebSocketConnectionsMiddleware(RequestDelegate next, WebSocketConnectionsOptions options, ...)
    {
        _options = options ?? throw new ArgumentNullException(nameof(options));
        ...
    }

    public async Task Invoke(HttpContext context)
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            if (ValidateOrigin(context))
            {
                ...
            }
            else
            {
                context.Response.StatusCode = StatusCodes.Status403Forbidden;
            }
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

    private bool ValidateOrigin(HttpContext context)
    {
        return (_options.AllowedOrigins == null)
            || (_options.AllowedOrigins.Count == 0)
            || (_options.AllowedOrigins.Contains(context.Request.Headers["Origin"].ToString()));
    }

    ...
}

Now the list of acceptable origins can be passed during the middleware registration.

public class Startup
{
    ...

    public void Configure(IApplicationBuilder app)
    {
        ...

        WebSocketConnectionsOptions webSocketConnectionsOptions = new WebSocketConnectionsOptions
        {
            AllowedOrigins = new HashSet<string> { "http://localhost:63290" }
        };

        ...
        app.UseWebSockets();
        app.Map("/socket", branchedApp =>
        {
            branchedApp.UseMiddleware<WebSocketConnectionsMiddleware>(webSocketConnectionsOptions);
        });
        ...
    }
}

I've extended the demo available at GitHub with this functionality.