Software Integrity Blog

Man in the middle: When Bob met Alice, and Eve heard everything

We discovered a flaw that enables a man-in-the-middle attack, or MitM attack, on a secure connection between a Socket.IO server and client.

Earlier this year, we did some research on Socket.IO to evaluate the overall security of the framework. David Johannson of Synopsys took a deep dive into the code and discovered an interesting flaw in Socket.IO that allows an attacker to get in the middle of a secure connection between a Socket.IO server and client. Then the attacker can view the encrypted transactions in the clear or even alter them in transit by using a certificate and masquerading as the Socket.IO server. This attack, commonly known as a man-in-the-middle attack, or meet-in-the-middle attack (MitM for short), is not new. As a matter of fact, it’s been around for quite some time.

What’s a man-in-the-middle attack?

The premise of encrypted communications such as TLS relies on the ability to authenticate that an endpoint is who it says it is using a certificate signed by a certificate authority (CA). In most clients, the default behavior for certificate validation when a certificate cannot be validated, because it’s either not signed by a trusted CA or maybe even not signed at all, is to reject the connection and notify the user. You may have noticed this occasionally when you visit a site in your browser and get a popup stating that you may not be visiting the site you believe you are visiting. This is a function of certificate validation.

MitM attacks take advantage of unencrypted connections by pretending to be an endpoint (since no one’s verifying their identity anyway). Let’s say Alice is running a Socket.IO client, and she’s trying to connect to Bob’s server. In a man-in-the-middle attack, Eve will intercept this connection, but Alice and Bob won’t know that. Even though the connection is encrypted, what Alice doesn’t know is that Eve pretended to be Bob, so she is the endpoint for the encrypted communications. She simply decrypts, then re-encrypts and passes along to Bob. Alice has no way of knowing that she’s being Evesdropped, because no one checked to make sure that Bob was actually Bob.

The flaw in Socket.IO

When David Johansson dug into the Socket.IO client code, he found a mistake in the configuration-handling code of the engine.io-client module. The mistake stemmed from an assumption in the way Node’s TLS module functions when handling the rejectUnauthorized setting. According to the principle of secure defaults, the configuration-handling code in the TLS module should be written in such a way that an error results in a secure configuration. However, that wasn’t the case. The TLS module in Node.js essentially defaults to an insecure configuration by default. From a design perspective, this is exactly the opposite of what you’d want to do when you get a bad certificate. The worst part is that due to the nature of the configuration option, you would never know that your configuration was insecure, since when Node is configured to accept invalid certificates, it won’t log that certificates are invalid. You can check out the technical details of the flaw in David’s post, Node.js and Socket.IO: How security fails when ‘null’ is ‘false’.

This chain of vulnerable design and improper implementation is difficult to detect in code, unless you have all the dependency code you’re using within the scope of a review. So these types of flaws can often slip through the cracks and go unnoticed for days, months, or even years. In a world where bad actors are constantly on the lookout for ways to intercept encrypted communications, this once-overlooked classification of flaw is now at the center of one of the most important and impactful debates in the history of the Crypto Wars.

A quick Google search shows around 2,000 package.json files that import the socket.io-clientlibrary. This indicates that potentially hundreds of frameworks, libraries, and open source applications today could be vulnerable to this man-in-the-middle attack. Given the nature of the design flaw in Node.js that leads to this problem (principle of secure defaults) and the probability that other frameworks using the TLS module are making the same mistake, I would rate this a higher likelihood than I would do otherwise.

A search on Github for instances where socket.io-client is imported and no calls (in the same file) are made to set the rejectUnauthorized property reveals around 19,000 instances in publicly available code. However, due to the nature of JavaScript development and the viral dependency management approach of the Node Community, this simple flaw has been fixed in the TLS module and in the Socket.IO mainline already. (Although neither has pushed a version yet, you can be assured it will happen soon.) I anticipate seeing this hole close and propagating upward in the node dependency graph very quickly.

Overall, this can serve as a lesson in secure software design, the importance that frameworks play in modern software, and especially the impact that a framework vulnerability and a responsive development community can have on the global state of application security.

Learn about industry-leading tools for every stage of your SDLC