Cross-Site WebSocket Hijacking Exploitation in 2025
Some of my favorite findings discovered during our client assessments at Include Security have exploited Cross-Site Websocket Hijacking (CSWSH) vulnerabilities. However, going back through those past findings, I realize that some of them wouldnβt work today in all browsers. This is due to improvements in baseline security in browsers around cross-origin requests. These improvements include Third Party Cookie Restrictions/Total Cookie Protection, and Private Network Access. CSWSH is a somewhat incidental casualty of these features, therefore Iβve not been able to find much discussion about the increasing limitations on CSWSH β apart from a passing mention in this excellent primer.
This blog post explores the current state of browser mitigations that make CSWSH harder to exploit. These will be explored together with three case studies of past findings, investigating which of the attacks still work today.
CSWSH Recap
A quick recap on CSWSH. Itβs a vulnerability that arises because WebSockets are not protected by the most important browser security mechanism, the Same Origin Policy (SOP). The lack of SOP protection by default enables malicious websites to open WebSocket connections to targeted websites running WebSocket servers. A typical example: a user browses to attacker.com, client-side code running on attacker.com opens a WebSocket connection to bank.com, and the browser helpfully attaches the userβs cookies authenticating to bank.com. Now attacker.com can send arbitrary WebSocket messages to bank.com while masquerading as the user.
The impact is similar to a Cross-Site Request Forgery (CSRF) attack, but more powerful since itβs two-way: the malicious site can also read the responses to malicious requests. Normally a CSRF attack canβt read the serverβs responses β unless the targeted server supports Cross-Origin Resource Sharing (CORS) requests, allows included credentials, and is misconfigured to reflect an attacker-controlled origin in the Access-Control-Allow-Origin response header.
CSWSH Mitigation
The definitive way to mitigate CSWSH is that the WebSocket server should first check the Origin of the WebSocket handshake request. If the request does not come from a trusted and expected Origin, then the WebSocket handshake should fail. βMissing Origin Validation in WebSocketsβ has its own Common Weakness Enumeration CWE-1385.
CSRF attacks are often addressed by attaching a pseudo-random CSRF token in the request HTTP header. The server can compare the header value to a CSRF token stored in a cookie (double submit cookie pattern). But this is harder to achieve with WebSocket handshakes due to an oddity with the WebSocket protocol that means you canβt set arbitrary headers. There are some workarounds such as putting a token in the Sec-WebSocket-Protocol header or authenticating in the first WebSocket message.
CSWSH Prerequisites
There are a number of prerequisites for a CSWSH attack to work:
1) The app uses cookie-based authentication
2) The authentication cookie is set to SameSite=None
3) The WebSocket server does not validate the Origin of the Websocket handshake request (and does not use another means to validate the source of requests, such as authenticating in the first WebSocket message).
This seems like a lot of things that have to line up, but CSWSH has been more common than I expected. If I had to speculate:
1) Cookies are still fairly popular compared to token auth.
2) Authentication services often operate across different origins forcing session cookies to use SameSite=None and to rely on CSRF tokens as the main mechanism to defeat CSRF, which arenβt applied to WebSocket handshakes.
3) The ws library for Nodejs and for other common webapp frameworks donβt enforce validating the Origin.
Mitigations
Now letβs discuss current browser security measures and how these make CSWSH attacks harder to achieve than they used to be. Weβll cover three different existing mitigations that can prevent CSWSH attacks, with a brief case study following each one describing how the mitigation affected the exploitability of CSWSH.
SameSite=Lax by default
SameSite=Lax by default is a longstanding and effective browser default that affects CSWSH attacks. If a SameSite setting is not explicitly configured on a cookie, then Chrome has configured it SameSite=Lax by default since 2020. SameSite=Lax means that browsers will send cookies in cross-site requests only for GET requests that resulted from a top-level navigation by the user, like clicking a link.
This has done a lot to make CSRF and CSWSH attacks harder to pull off by default. Note that not all browsers set SameSite=Lax by default. Firefox tried but too many sites broke, and so Firefox relies more on Total Cookie Protection described below instead. Safari also doesnβt use SameSite=Lax by default, but similarly to Firefox, also blocks third-party cookies. Β Microsoft Edge, being based on Chromium, follows the same behavior as Chrome.
Due to breakage of SSO logins during rollout, back in 2020 Chrome rolled out a temporary measure making default SameSite=Lax work slightly differently to a cookie that has explicitly been set to SameSite=Lax by the backend application. For implicit SameSite=Lax, there is a two minute grace period after the cookie is issued where cookies are still sent with top-level cross-site POST requests. As described by PortSwigger, thereβs scenarios where this enables a bypass of the SameSite protection. I verified that this two minute grace period still exists, in both Chrome and Firefox. However, it does not apply to CSWSH attacks, since WebSocket handshakes are not top-level POST requests. Therefore SameSite needs to explicitly be set to None for CSWSH attacks to work, no other value for SameSite will allow CSWSH to operate.
Case Study A
This was a 2021 engagement I did against a website that implemented a WebSocket API for making changes to a document. Session cookies did not set a SameSite attribute. At the time, this enabled a CSWSH attack in Firefox, but not in Chrome, due to SameSite=Lax by default. An attacker could use CSWSH to make arbitrary modifications to usersβ documents.Β The attack worked when we found it, but today the attack would no longer work in Firefox either due to Total Cookie Protection.
So what is Total Cookie Protection?
Over the past several years Firefox has been locking down their βEnhanced Tracking Protectionβ feature. Itβs unclear exactly when, but sometime in 2022-2024 βTotal Cookie Protectionβ was enabled by default for the whole userbase, and this is really effective at blocking CSWSH.
Total Cookie Protection works by isolating cookies to the site in which they are created. Essentially each site has its own cookie storage partition to prevent third parties linking a userβs browsing history together. This is designed to prevent a tracker.com script loaded on site A to set a cookie which can be read by a tracker.com script loaded on site B.
It also has the side-effect of stopping cookie-based CSWSH. A malicious site cannot perform a successful cross-site WebSocket handshake with a userβs cookie, since that cookie is outside the current cookie storage partition. This applies even if the cookie is configured as SameSite=None.
Total Cookie Protection can be disabled in Firefoxβs Browser Privacy settings, by selecting the βCustomβ mode for βEnhanced Tracking Protectionβ, then unchecking the Cookies setting or changing to the old default βCross-site tracking cookiesβ.

Note that Google has been announcing the blocking of third party cookies in Chrome by default for years but have kept delaying for various reasons. The latest target was early 2025, but they changed plans in July 2024. Itβs straightforward to configure in the βPrivacy and securityβ settings, though, and this setting is enabled by default within Incognito Mode. Some distributions, notably Chromium on Debian Linux, have the default set to βBlock third-party cookiesβ.

Case Study B
This engagement was against a large web application with a GraphQL API and SameSite=None cookies. Direct CSRF against the GraphQL API wasnβt possible due to the server enforcing the application/json Content-Type, which triggers preflighted requests from the browser. However, the GraphQL API could also be called through a WebSocket. The WebSocket was vulnerable to CSWSH, enabling arbitrary API calls to be made by a third-party attacker, including all account operations such as deleting a userβs account.
This vulnerability would now be unexploitable in default Firefox due to Total Cookie Protection, but is still currently exploitable in default Chrome/Edge.
Private Network Access
Weβve looked at two typical CSWSH scenarios; now for a more unusual one that came up in a client assessment. Letβs start with the case study, since it gives context on a situation where cookies were not used for authentication.
Case Study C
This engagement was against a network device with a camera. A design decision was that users on the same network could access the camera and perform limited configuration without further authentication. Being on the same private network was considered adequate authentication (cookies were not required). A WebSocket API which was vulnerable to CSWSH was added to the device. Now, malicious websites on the Internet could stream video from the devices and configure them, through the medium of a targeted userβs browser who was connected to the private network.
While reviewing this attack against a WebSocket server on a private IP address, I expected it to no longer work in recent versions of Chrome due to Private Network Access (apparently enforced since Chrome 130, I found it wasnβt enforced by default in Chromium 134 on Debian Linux).
The Private Network Access specification acknowledges that an increasing amount of services run on a userβs localhost and their private network, and describes a control similar to CORS to prevent public Internet resources from making unapproved requests to private resources. See for instance this incredible writeup against Tailscale.
Within the Private Network Access specification, IP address spaces are divided into three types: public, private, and local. A request (even a GET request) that is made from a more public to a more private address space triggers a preflight OPTIONS request that has the Access-Control-Request-Private-Network: true header attached by Chrome, and must receive a corresponding Access-Control-Allow-Private-Network: true header in the response for the main request to be sent.

However, I found that CSWSH attacks against private IPs are not affected by Private Network Access. In my testing, attempts to send CSRF attacks against localhost addresses failed in Chrome, but there was no problem with opening WebSockets to more private IPs. On further thought this makes sense, and is called out in the specification, since Private Network Access uses CORS preflight requests as the protection method, and WebSockets do not follow SOP and thus do not use preflight requests.
Testing
To verify all the points made above, I made a small demo app, the source code is at https://github.com/IncludeSecurity/cswsh-demo
A small NodeJS Express server sets a SameSite=None cookie when visiting the / route. The server also exposes a POST / route, and a WebSocket handler, both of which log cookies if they are seen in the request.

The WebSocket server and the demo page were then hosted on separate HTTPS domains, and requests were tried on different browsers to verify if different types of CSRF and CSWSH attacks were successful. The /reflected endpoint was added to elicit the error specific to attempting a CSRF against a private IP with Private Network Access, otherwise a generic CORS error is displayed in DevTools.
Note that inspecting DevTools for WebSockets requests can be misleading β in Chrome, cookies are not always shown in Devtools for WebSocket handshakes! Per a Chromium developer in the linked issue: βCookie headers are appended at a lower layer in the networking code, so DevTools doesnβt always have everything. Normal HTTP requests do show cookies in DevTools, including HttpOnly cookies, because they involve a different cookie reporting path that sends the raw headers directly to DevTools.β
Conclusion
So to sum up:
- SameSite=Lax which is the default for cookies in Chrome is a decent mitigation for CSWSH, so CSWSH requires SameSite=None session/auth cookies.
- While Firefox doesnβt apply SameSite=Lax by default, Firefoxβs Total Cookie Protection appears to be a complete mitigation for CSWSH.
- The Chrome team has discussed a similar third party blocking technique for years, but still havenβt implemented it and are investigating other approaches. If they did block third party cookies by default, CSWSH would be largely eliminated.
- Private Network Access in Chrome does not block CSWSH against private networks.
Revisiting the three case studies:
- A) No SameSite attribute specified on cookie -> no longer works in any major browser.
- B) Typical CSWSH with SameSite=None cookie -> works in default Chrome but not Firefox.
- C) CSWSH against private IP -> still works on all browsers even though my initial expectation was that it would no longer work in Chrome.
For defenders, adding an Origin check in the server-side WebSocket handshake handler is still the best way of defending against CSWSH attacks. This is important as while browser mitigations are slowly improving, they cannot be fully relied upon. It is still possible to perform CSWSH under the right circumstances against default Chrome, and it is possible for a user to run a browser that is configured to disable settings such as Total Cookie Protection. Similarly, it is not ideal to rely on the SameSite=Lax attribute on authentication cookies to protect against CSWSH. The cookies could later be changed to use SameSite=None as part of an unrelated code change, causing CSWSH vulnerabilities to become exploitable.
The post Cross-Site WebSocket Hijacking Exploitation in 2025 appeared first on Include Security Research Blog.