Cross-Site WebSocket Hijacking (CSWSH)
The relatively new HTML5 WebSocket technique to enable full-duplex communication channels between browsers and servers is retrieving more and more attention from developers as well as security analysts. Using WebSockets developers can exchange text and binary messages pushed from the server to the browser as well as vice versa.
During some experiments and pentests with WebSocket backed applications in the last few months I came across a scenario where developers might use WebSockets in a way to open up their applications to a vulnerability I call Cross-Site WebSocket Hijacking (CSWSH), which I will present in this short blog post.
The protocol upgrade
In order to create the full-duplex communication channel the WebSocket protocol requires a handshake (carried out over
https://) to switch towards
a WebSocket protocol. This handshake effectively upgrades the communication protocol to
wss:// for SSL protected channels).
But this upgrade phase is also a potential target to attack and Achilles' heel of using
WebSockets inside an application that deals with non-public data, because it kind of bridges/transfers the HTTP(S)-based communication towards the WS(S)-based WebSocket protocol.
The typical lifecycle of a WebSocket interaction between client & server goes as follows:
- Client initiates a connection by sending an HTTP(S) WebSocket handshake request.
- Server replies with a handshake response (all handled by the web application server transparently to the application) and sends a response status code of 101 Switching Protocols.
Let's take a closer look at this handshake request and inspect the request headers of such a handshake to upgrade the protocol to WebSockets (of an imaginary stock portfolio management application, which uses WebSockets to quickly push new stock quotes to logged-in users as well as retrieve stock orders from them):
GET /trading/ws/stockPortfolio HTTP/1.1Host: www.some-trading-application.com User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:23.0) Firefox/23.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Sec-WebSocket-Version: 13 Origin: https://www.some-trading-application.com Sec-WebSocket-Key: x7nPlaiHMGDBuJeD6l7y/Q== Cookie: JSESSIONID=1A9431CF043F851E0356F5837845B2EC Connection: keep-alive, Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket
As you can see from the request headers of the HTTP(S) handshake request, the authentication data (in this example the Cookie header) is sent along with the upgrade handshake. The same would be true for HTTP-Authentication data. Both is correct behaviour of the browsers according to the aforementioned specification and RFC.
WebSocket handshake the server replies with the 101 Switching Protocols status code and from then on the
wss:// based connection is
established between browser and server. The header
Sec-WebSocket-Key is part of the browser/server handshake internals (to verify that the server has read and
understood the request) and is automatically created by and taken care of the browser
initiating the WebSocket request.
Regarding client authentication during this handshake/upgrade phase the RFC 6455 reads as follows:
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.
RFC 6455 "The WebSocket Protocol", chapter 10.5 WebSocket Client Authentication
This means to developers that they can use for example cookies or HTTP-Authentication to authenticate the WebSocket handshake request, as if it was a regular HTTP(S) web application request.
Hijacking it cross-site
Now let's consider what happens when a developer follows this well-known style of using session cookies to authenticate the WebSocket handshake/upgrade request within a logged-in (sensitive) part of a web application:
Because WebSockets are not restrained by the same-origin policy, an attacker can easily initiate a WebSocket
request (i.e. the handshake/upgrade process) from a malicious webpage targeting the
wss:// endpoint URL of the attacked application (the
stock service in our example). Due to the fact that this request is a regular HTTP(S) request, browsers send the cookies and HTTP-Authentication headers along, even cross-site.
Take a look at the WebSocket handshake/upgrade request when issued from a malicious webpage cross-site (visited by the victim while logged-in with our stock trading application).
Here the WebSocket endpoint
wss://www.some-trading-application.com/trading/ws/stockPortfolio is accessed from a malicious webpage at
GET /trading/ws/stockPortfolio HTTP/1.1Host: www.some-trading-application.com User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:23.0) Firefox/23.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Sec-WebSocket-Version: 13 Origin: https://www.some-evil-attacker-application.com Sec-WebSocket-Key: hP+ghc+KuZT2wQgRRikjBw== Cookie: JSESSIONID=1A9431CF043F851E0356F5837845B2EC Connection: keep-alive, Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket
As you can see, the browser sends the authentication information (in this example the session cookie) along with the WebSocket handshake/upgrade request. This is very similar to a Cross-Site Request Forgery (CSRF) attack scenario. But in the WebSocket scenario this attack can be extended from a write-only CSRF attack to a full read/write communication with a WebSocket service by physically establishing a new WebSocket connection with the service under the same authentication data as the victim. Therefore I call this attack vector Cross-Site WebSocket Hijacking (CSWSH).
Effectively this allows the attacker in our scenario to read the victim's stock portfolio updates pushed via the WebSocket connection and update the protfolio by issuing write requests via the WebSocket connection. This is possible due to the fact that the server's WebSocket code relies on the session authentication data (cookies or HTTP-Authentication) sent along from the browser during the WebSocket handshake/upgrade phase.
Another interesting observation is the
Origin header that is sent along the WebSocket handshake/upgrade request. This is like in a regular CORS request utilizing
when the server does not explicitly allow it (via a matching
Access-Control-Allow-Origin response header). But when it comes to WebSockets this "fail close"
style of defaulting to "restrict response access" when the server does not explicitly allow cross-origin requests is inverted: In our example the server did not send
any CORS response headers along, but the cross-site WebSocket request's response is still handled by the browser by properly establishing the full-duplex WebSocket connection.
This demonstrates that WebSockets are not protected by the same-origin policy (SOP), so developers must not rely on SOP protection when it comes to developing WebSocket based applications.
Clearly the CORS stuff has nothing to do with the WebSockets stuff, but they both utilize the same request header (
Origin) and the server-side code should check that header!
As you've already noticed, securing an application against Cross-Site WebSocket Hijacking attacks can be performed using two countermeasures:
- Check the
Originheader of the WebSocket handshake request on the server, since that header was designed to protect the server against attacker-initiated cross-site connections of victim browsers!
- Use session-individual random tokens (like CSRF-Tokens) on the handshake request and verify them on the server.
If you don't need to access the web session from the server-side WebSocket counterpart, just separately handle authentication and/or authorization using custom tokens or similar techniques within your WebSocket protocol and avoid relating to the web session via cookies or HTTP-Authentication during the handshake request.