Journey into WebSockets Authentication/Authorization
One subject that is often mentioned in talks about WebSockets security, is how WebSockets does not implement authentication/authorization in the protocol.
This might not be as familiar because when the original research was done, there were not many applications using WebSockets. I wanted to demonstrate what this pattern looks like with an application that was using WebSockets for a critical application function.
"It is a common misconception that a user who is authenticated in the hosting web application, is also authenticated in the socket stream. These are two completely different channels." - José F. Romaniello, https://auth0.com/blog/2014/01/15/auth-with-socket-io/
Without repeating all the research about WebSockets, if you are new to WebSocket hacking, this is a great Black Hat talk which will help catch you up. talk | slides
tl;dr - Many of the same vulnerability classes exist in applications that are using WebSockets that would exist in web applications using HTTP polling, except that the way that these issues are exploited are WebSockets specific. Such as:
- Transmitting sensitive data in cleartext (WS:// instead of WSS://)
- User input validation issues
- Authentication/Authorization issues
- Origin Header Verification / Cross-site Request Forgery (CSRF)
Because authentication and authorization is not inherently handled in the protocol, it is the developers responsibility to implement this at the application level in WebSockets.
This is what the WebSockets RFC has to say about WebSocket client authentication.
This protocol doesn't prescribe any particular way that servers can
authenticate clients during the WebSocket handshake. The WebSocket
server can use any client authentication mechanism available to a
generic HTTP server, such as cookies, HTTP authentication, or TLS
authentication. RFC6455
WebSocket Opening Handshake Sec-WebSocket-Key Header
In the WebSocket opening handshake the Sec-WebSocket-Key header is used to ensure that the server does not accept connections from non-WebSocket clients. This is not used for authentication.
GET /socket/
Sec-WebSocket-Key: 01GkxdiA9js4QKT1PdZrQw==
Upgrade: websocket
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: iyRn17RxQADfC/y254mArm4wRyI=
The Sec-WebSocket-Key header is just a base64 encoded 16-byte nonce value, and the Sec-WebSocket-Accept response is the Sec-WebSocket-Key value concatenated with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", SHA1 hashed, then base64 encoded.
Chrome Developer Tools WebSockets Testing
When reviewing WebSocket applications for security issues, ZAP or Burp may be able to read, or even modify some WebSockets frames. However, I have found that in some applications attempting to modify/replay messages may break the socket connection, or otherwise go wrong. You may have better results by calling the WebSockets API directly, or using the API of the implementation used by the application. Eg., Socket.io, SockJS, WS, etc.
Chrome Developer Tools provides an easy way to view WebSockets messages, correctly unmasks data frames, and will allow you to test applications that are using WebSockets. To view WebSocket frames, go to Developer Tools, Network, WS tab:
WebSocket API Basic Usage
Using the WebSocket API to send and recieve messages.
//connect to the socket interface
var socket = new WebSocket('wss://host.com');
//on open event
socket.onopen = function(event) { console.log("Connected"); };
//on message event. return messages received from the server
socket.onmessage = function(event) { console.log(event.data); }
//send a message to the server
socket.send('simple message');
//send message syntax used in socket.io.
socket.send('42/namespace,["mymessage","hi"]');
//JSON.stringify message to the server
socket.send(JSON.stringify({"vId":null,"type":"UPDATE_USER","data":{"name":"admin","pass":"mypassword","priv":true}}));
Socket.io (WebSockets Realtime Framework) Basic Usage
If the application is using Socket.io, the server will serve the path /socket.io by default. This is where engine.io and socket.io.js are served from.
//jQuery load socket.io.js. (if it is not already loaded)
$.getScript('http://host/socket.io/socket.io.js');
//connect to the socket interface, and the defined namespace
var socket = io.connect('http://host/console');
//returning custom 'data' socket messages from the server
socket.on('data', function (data) { console.log(data); });
//emitting a message (equivalent to socket.send)
socket.emit('Simple message');
//emitting a custom socket message
socket.emit('command', 'cat /etc/passwd');
Server Console Application
This is one example of an application which required authentication for the web application, but not for the WebSocket connection.
There was server console functionality included in the application stack, that used Socket.io to communicate system commands in realtime.
In reviewing the socket frames when authenticated to the console, it was evident that WebSocket messages containing system commands were passed without authorization tokens, or authentication required before the socket connection was established.
So from this point, it was just a matter of connecting to the WebSocket endpoint directly which did not require any authentication:
var socket = io.connect('http://host/console);
Returning custom 'data' socket messages from the server (so we get responses to our commands):
socket.on('data', function (data) { console.log(data); });
Emitting a custom socket message:
socket.emit('command', 'cat /etc/passwd');
Round.io (Demo chat application)
Someone created an interesting concept for a chat application that uses WebSockets to allow you to chat with people around the world, and displays their location on a map. The UI will use the browser geolocation to show where you are chatting from; if this is not supplied, the UI will not allow the user to chat. For demo purposes, and to play with the concept of application authorization, deny the browser access to your geolocation when it is requested. https://round.io/chat/
Connect using the WebSocket API:
var socket = new WebSocket('wss://round.io/socket.io/?EIO=3&transport=websocket');
Send a chat message with coordinates, and nickname. eg.,
socket.send('42["outgoing message",{"msgtext":"Nobody exists on purpose Summer","lat":53.06,"lng":6.57,"nickname":"Morty"}]');
Even if an application does not provide any visible user inputs, communication sent to the WebSocket can still be manipulated, allow attacks against users connected to the socket, or allow attacks against the server.
Auth0 has a nice post on how to require authentication in Socket.io with cookie-based or token-based authentication: https://auth0.com/blog/2014/01/15/auth-with-socket-io/
Authentication can also be passed in the WebSocket URI when connecting. The issue with this method is that authorization will be passed in a GET request which will remain latent in proxy logs, so that issue will need to be mitigated: http://dev.datasift.com/docs/api/streaming-api/websockets-streaming
The messaging service Slack takes this approach in authenticating to their real time messaging (RTM) API. Their API describes a single-use WebSocket URI that is only valid for 30 seconds. https://api.slack.com/rtm
If you have any comments about this, you can find me here.
Craig is a security consultant at Stratum Security. Stratum is a boutique security consulting company specializing in application security, data exfiltration and network security.
References: