CVE-2022-21676: Improper Handling of Exceptional Conditions (CWE-755)
value: Engine.IO is the implementation of transport-based cross-browser/cross-device bi-directional communication layer for Socket.IO. A specially crafted HTTP request can trigger an uncaught exception on the Engine.IO server, thus killing the Node.js process. This impacts all the users of the `engine.io` package starting from version `4.0.0`, including those who uses depending packages like `socket.io`. Versions prior to `4.0.0` are not impacted. A fix has been released for each major branch, namely `4.1.2` for the `4.x.x` branch, `5.2.1` for the `5.x.x` branch, and `6.1.1` for the `6.x.x` branch. There is no known workaround except upgrading to a safe version.
CWE description: The product does not handle or incorrectly handles an exceptional condition.
CVSS v3 base score: 7.5
Published: 2022-01-12T19:15Z
Repository: https://github.com/socketio/engine.io
Language: JavaScript
Files changed: lib/server.ts, test/server.js
Commit date: 2022-01-11 15:52:15 +0100
Commit message: fix: properly handle invalid data sent by a malicious websocket client

**IMPORTANT SECURITY FIX**

A malicious client could send a specially crafted HTTP request,
triggering an uncaught exception and killing the Node.js process:

> RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear
>   at Receiver.getInfo (/.../node_modules/ws/lib/receiver.js:176:14)
>   at Receiver.startLoop (/.../node_modules/ws/lib/receiver.js:136:22)
>   at Receiver._write (/.../node_modules/ws/lib/receiver.js:83:10)
>   at writeOrBuffer (internal/streams/writable.js:358:12)

This bug was introduced by [1], included in `engine.io@4.0.0`, so
previous releases are not impacted.

[1]: https://github.com/socketio/engine.io/commit/f3c291fa613a9d50c924d74293035737fdace4f2

Thanks to Marcus Wejderot from Mevisio for the responsible disclosure.

Diff: diff --git a/lib/server.ts b/lib/server.ts
index 78648df..c09b2e1 100644
--- a/lib/server.ts
+++ b/lib/server.ts
@@ -607,13 +607,10 @@ export class Server extends BaseServer {
         }
         transport.perMessageDeflate = this.opts.perMessageDeflate;
         client.maybeUpgrade(transport);
       }
     } else {
-      // transport error handling takes over
-      websocket.removeListener("error", onUpgradeError);
-
       const closeConnection = (errorCode, errorContext) =>
         abortUpgrade(socket, errorCode, errorContext);
       this.handshake(req._query.transport, req, closeConnection);
     }
 
diff --git a/test/server.js b/test/server.js
index 80086c8..05f5d7b 100644
--- a/test/server.js
+++ b/test/server.js
@@ -155,10 +155,50 @@ describe("server", () => {
             done();
           });
         }
       );
     });
+
+    it("should not throw when the client sends invalid data during the handshake (ws only)", done => {
+      listen(port => {
+        // will throw "RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear"
+        request
+          .get(`http://localhost:${port}/engine.io/`)
+          .set("connection", "upgrade")
+          .set("upgrade", "websocket")
+          .set("Sec-WebSocket-Version", "13")
+          .set("Sec-WebSocket-Key", "DXR4dX615eRds8nRmlhqtw==")
+          .query({ transport: "websocket", EIO: 4 })
+          .send("test")
+          .end(() => {});
+
+        setTimeout(done, 50);
+      });
+    });
+
+    it("should not throw when the client sends invalid data during the handshake (upgrade)", done => {
+      listen(port => {
+        request
+          .get(`http://localhost:${port}/engine.io/`)
+          .query({ transport: "polling", EIO: 4 })
+          .end((err, res) => {
+            const sid = JSON.parse(res.text.substring(1)).sid;
+
+            request
+              .get(`http://localhost:${port}/engine.io/`)
+              .set("connection", "upgrade")
+              .set("upgrade", "websocket")
+              .set("Sec-WebSocket-Version", "13")
+              .set("Sec-WebSocket-Key", "DXR4dX615eRds8nRmlhqtw==")
+              .query({ transport: "websocket", EIO: 4, sid })
+              .send("test")
+              .end(() => {});
+
+            setTimeout(done, 50);
+          });
+      });
+    });
   });
 
   describe("handshake", () => {
     it("should send the io cookie", done => {
       listen({ cookie: true }, port => {

Vulnerable code:       // transport error handling takes over
      websocket.removeListener("error", onUpgradeError);

Fixed code: 
    it("should not throw when the client sends invalid data during the handshake (ws only)", done => {
      listen(port => {
        // will throw "RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear"
        request
          .get(`http://localhost:${port}/engine.io/`)
          .set("connection", "upgrade")
          .set("upgrade", "websocket")
          .set("Sec-WebSocket-Version", "13")
          .set("Sec-WebSocket-Key", "DXR4dX615eRds8nRmlhqtw==")
          .query({ transport: "websocket", EIO: 4 })
          .send("test")
          .end(() => {});

        setTimeout(done, 50);
      });
    });

    it("should not throw when the client sends invalid data during the handshake (upgrade)", done => {
      listen(port => {
        request
          .get(`http://localhost:${port}/engine.io/`)
          .query({ transport: "polling", EIO: 4 })
          .end((err, res) => {
            const sid = JSON.parse(res.text.substring(1)).sid;

            request
              .get(`http://localhost:${port}/engine.io/`)
              .set("connection", "upgrade")
              .set("upgrade", "websocket")
              .set("Sec-WebSocket-Version", "13")
              .set("Sec-WebSocket-Key", "DXR4dX615eRds8nRmlhqtw==")
              .query({ transport: "websocket", EIO: 4, sid })
              .send("test")
              .end(() => {});

            setTimeout(done, 50);
          });
      });
    });