WebSockets not Bound by SOP and CORS? Does this mean…
Browsers allow attackers to inject and exfiltrate WebSocket data from foreign domains.

Since there is currently a lack of resources on the web explaining how to test WebSocket (WS) implementations, I decided to give an internal presentation on WebSockets, what they are, security considerations surrounding them, and of course, a methodology for testing. During that session, an interesting question was raised,
“Since WebSocket connections are not bound by the SOP and CORS, could we cause the browser to establish a cross-origin connection from a client on origin a, to a WS server on origin b, then access response data successfully?”
Short answer, Yes. The cross-origin resource standard is used ONLY to enable browsers to issue cross-origin XHR, media, script, stylesheet, and WebGL texture HTTP requests then grant access to that data to other domains. While the client uses an HTTP handshake to establish a WebSocket communication channel, client-server communication occurs over the WS protocol (i.e., ws:// or wss://). Simply put, cross-origin data access restrictions imposed by the SOP as well as custom CORS policies would not apply to data transmitted via WebSockets as CORS only places such restrictions on HTTP responses.
Continued reading of this blog will provide a deeper understanding of CORS, and why it is not applicable to WebSocket data. Further, I will discuss what type of WS data can be accessed cross-origin, and demonstrate how an attacker could successfully establish and transmit data between a WS client and WS server on different domains.
A Deeper Look into the SOP and CORS Standard
By default, modern browsers adhere to the SOP which is a security mechanism that places limitations on how the requesting origin can interact with resources retrieved from another domain if the origins differ. To limit the security ramifications of cross-origin requests, browsers restrict access to a cross-origin response in accordance with the SOP.
CORS is a mechanism that enables browsers to retrieve then grant access to resources via requests originating from a different domain than that of what is currently being browsed. Developers utilize CORS to relax or disable the SOP entirely to allow front-end applications to access cross-origin resources.
Cross-origin HTTP requests occur when a client issues a request from an entirely different domain, port, or using a different protocol than the domain currently browsed. An example of a cross-origin request would be if a request from https://example.com
is issued to http://example.com:3000
.This would be a cross-origin request due to the different protocols (HTTPS ->HTTP) and ports (443 -> 3000).
CORS policies inform the browser which domains are allowed to access the response object of a request, if cookies should be sent within requests, and which HTTP methods are allowed via HTTP response headers (just to mention a few restrictions that could be imposed by CORS policies). The cross-origin resource standard includes several HTTP headers; however, I will mainly focus on the Access-Control-Allow-Origin
header as it permits browsers to grant cross-origin access to response objects.
As mentioned, the Access-Control-Allow-Origin
header informs the browser whether or not to grant the requesting domain access to the response object. The value of this header specifies the origin(s) that is permitted to access the response data. Examples of this will be:
Access-Control-Allow-Origin: http://example.com
and
Access-Control-Allow-Origin: *
where the first header informs the browser to limit resource access to http://example.com, and the second informs the browser to grant resource access to all domains. Let us take a look at a cross-site request and response to paint a clearer picture.
GET /info.png HTTP/1.1
Host: http://example.com
Referer: http://evil.com
Origin: http://evil.com
The request headers above shows an HTTP GET request originating from http://evil.com
to retrieve info.png
from http://example.com
.
HTTP/1.1 200 OK
Connection: close
Access-Control-Allow-Origin: http://example.com
The response to the cross-site request (above) informs the browser that info.png
should only be accessible to thehttp://example.com
domain. The browser will issue the GET request and receive the content; however, it will not grant access to the response object to http://evil.com
due to the origin restriction set by the Access-Control-Allow-Origin
header.
It should be understood that CORS enables cross-origin access to response objects. Considering that, even without the presence of the custom CORS policy, browsers will prevent access to response objects of cross-origin requests, so access to the response will not be granted.
Why and What WebSocket Data is at Risk?
At first, it may seem a bit confusing as to why access to WebSocket data cannot be restricted by the SOP or a CORS policy when it is required that a WS client issues an HTTP GET request to initiate the handshake with the WS server. There are two aspects of the WebSocket protocol that renders the SOP and CORS restrictions ineffective: (1) no HTTP response data is required to complete the WS handshake workflow, and (2) data transfer occurs over the WebSocket protocol (ws or wss).
The WebSocket handshake between a client and a server occurs via HTTP Upgrade request and response headers. Since the SOP can only control domain access to HTTP response objects, a malicious user could cause the browser to disclose the WS server’s response headers to an attacker-controlled client. Although attackers could gain access to this data, WS response headers will most likely not to contain any sensitive data. The code snippets below include an example of request and response headers exchanged during a typical WS handshake initiation.
GET /socket.io/?EIO=3&transport=websocket&sid=EzQJ3IZYHSWNCOehAAAA HTTP/1.1
Host: 192.168.1.160:3000
Sec-WebSocket-Version: 13
Origin: http://192.168.1.160:3000
Sec-WebSocket-Key: mNp0XrJcxA7qJH/JuG5qFQ==
Connection: keep-alive, Upgrade
Upgrade: websocket
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 7tggGstqSGs0spvWVfMX5BvA4Yo=
The WebSocket protocol only uses the HTTP protocol to establish a connection between the client and the server. WebSocket channel data transmission commences over ws://
or wss://
, WebSocket and WebSocket Secure respectively. As I alluded to previously, the SOP prevents, and CORS enables browsers to access cross-origin XHR, media, script, stylesheet, and WebGL texture HTTP response data. Therefore, since the SOP only pertains to HTTP response data, the SOP cannot instruct the browser to restrict access to data transmitted via WebSockets.
With SOP’s lack of ability to enforce access controls on WebSocket data, attackers could establish a cross-origin WS connection channel, to send malicious data to the WS server as well as expose any data transmitted to the “subscribed” WS channel. The subsequent section outlines how an adversary may go about achieving this.
Establishing a Cross-Origin WebSocket Connection
An attacker could create a WS client to initiate a WS handshake with a web server that supports WebSockets (denoted by an HTTP 101 Switching Protocols response). From there, the malicious entity could potentially gain unauthorized access to cross-origin WS data.
To demonstrate this, I will use a sample proof-of-concept that one of our Junior Analysts, Paul Yun, and myself put together. You can find it here. This Node.js PoC contains a Support Live Chat app (hosted on localhost:3000), and a Cross-Origin WS Client (hosted on localhost:9000) — which utilize the Socket.io JavaScript framework to integrate WebSocket support.
Socket.io framework includes a client API that makes developing WS clients straightforward. Below is a code snippet of the Cross-Origin WS Client’s front-end JS code within index.html
(external scripts and HTML omitted).
$(function() {
$('#connect').click(function() {
var target = $('#target').val();
const socket = io(target); $('#send').click(function() {
var message = $('#m').val();
var username = 'spoofed_admin'; socket.emit('chat message', {
username: username,
message: message
});
}); socket.on('chat message', function(d) {
$('#messages').append($('<li>').text(d.username + ' : ' + d.message))
}); socket.on('user joined', function(data) {
$('#messages').append($('<li>').text('A new user has joined, there are currently ' + data.numOfUsers + ' in this lobby.'));
});
});
});
The front-end Cross-Origin WS Client has the ability to initiate a WS handshake with a cross-origin WS server, and send messages to and retrieve broadcast messages from that WS server.
The WS client’s code for the Support Live Chat app is similar to the client for the Cross-Origin WS Client. Instead of establishing a cross-origin WS connection, this client connects to the WS server on its own domain, creating a same-origin connection. To achieve this, we used const socket = io();
instead of const socket = io(target);
to initiate the WS connection to our Support Live Chat’s WS server.
The Support Live Chat’s WS server listens for WS handshakes originating from any domain, then establishes a WS communication channel with the requesting WS client. The purpose of this WS server is to emit (broadcast) messages received from WS clients to subscribers of the WS server’s channel. Our WS server’s Socket.io JS code is shown in the snippet below.
var io = require('socket.io')(server);io.on('connection', function (socket) {
++numOfUsers;
socket.on('disconnect', function () {
--numOfUsers;
}); socket.on('chat message', function (data) {
io.emit('chat message', {
username: data.username,
message: data.message
});
}); socket.broadcast.emit('user joined', {
numOfUsers: numOfUsers
});
});
Now that we have a cross-origin WS client let us attempt to establish a WS connection with a server that does not support WebSocket connections. We can test this by using our cross-origin WS client to initiate a WS handshake with the support chat’s server while the Socket.io implementation commented out.

As expected, the browser does indeed block the cross-origin request to a server that does not support WS as the WS handshake occurs over HTTP, and the browser receives an HTTP 404 response without the Access-Control-Allow-Origin
header set to http://192.168.1.160:9000
or *
. If the Access-Control-Allow-Origin
were properly set, the browser would have exposed the HTTP response to the front-end application, in this case, Cannot GET /socket.io/
.
Let us observe how the brower handles a cross-origin WS HTTP Upgrade request with a server that supports WS but without the Access-Control-Allow-Origin
set.

As shown in Figure 2, the WS server receives the cross-origin HTTP Upgrade request, then establishes a cross-origin connection without interferece from the browser due to the SOP. As mentioned above, a WS HTTP Upgrade response has an empty body the browser does not have any data to place restrictions on hence the lack of console log error messages.
As the SOP fails to enforce the browser to prevent cross-origin WS connections, we do not expect CORS to be effective as it relaxes or disables the SOP. Further, since WS do not support custom headers within WS handshake requests and responses, a CORS policy cannot be inserted into an HTTP Upgrade response per the WS RFC.
Considering we have the ability to establish cross-origin WS connections, and WS communication occurs over the WS protocol, the Cross-Origin WS Client will be able to send malicious data to the WS server as well as expose any data transmitted to the “subscribed” WS channel (at the time of writing). Figures 3 and 4 depict cross-origin WS communication between a legitimate user of the Support Live Chat application and a cross-origin attacker.


Remediation
Due to the lack of effectiveness of the SOP and CORS, every WebSocket server implementation should verify the origin of HTTP Upgrade request to prevent cross-origin WS connections. Doing so will further improve the security posture of a WebSocket implementation by building upon security controls that are paramount and should be in place (e.g., authentication and authorization checks).
Socket.io recognizes the potential ramifications of the WS protocol’s overly permissive manner and provides such functionality by providing an additional of server option key-pair. Other widely used WS frameworks should offer similar features as well.
Key Takeaways
- Browsers prevent access to response objects when a cross-origin request is made by default.
- CORS policies enable browsers to grant access to response data when a cross-origin request is made.
- SOP and CORS apply only to the HTTP URI scheme.
- Client-server WebSocket handshakes occur over the HTTP protocol.
- Attackers could maliciously connect to WS servers without authentication/authorization.
- Restrict access to the WebSocket server via origin validation.
- Validating the origin is not a replacement for authentication or authorization verification.
- Use the WS framework’s built-in security controls
Drew Branch, Security Analyst @ Independent Security Evaluators
Twitters: @ISESecurity