CVE-2024-37890: Insufficient Information (NVD-CWE-noinfo)
value: ws is an open source WebSocket client and server for Node.js. A request with a number of headers exceeding theserver.maxHeadersCount threshold could be used to crash a ws server. The vulnerability was fixed in ws@8.17.1 (e55e510) and backported to ws@7.5.10 (22c2876), ws@6.2.3 (eeb76d3), and ws@5.2.4 (4abd8f6). In vulnerable versions of ws, the issue can be mitigated in the following ways: 1. Reduce the maximum allowed length of the request headers using the --max-http-header-size=size and/or the maxHeaderSize options so that no more headers than the server.maxHeadersCount limit can be sent. 2. Set server.maxHeadersCount to 0 so that no limit is applied.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-06-17T20:15Z
Repository: https://github.com/websockets/ws
Language: JavaScript
Files changed: lib/websocket-server.js, lib/websocket.js, test/websocket-server.test.js, test/websocket.test.js
Commit date: 2024-06-16 11:42:59 +0200
Commit message: [security] Fix crash when the Upgrade header cannot be read (#2231)

It is possible that the Upgrade header is correctly received and handled
(the `'upgrade'` event is emitted) without its value being returned to
the user. This can happen if the number of received headers exceed the
`server.maxHeadersCount` or `request.maxHeadersCount` threshold. In this
case `incomingMessage.headers.upgrade` may not be set.

Handle the case correctly and abort the handshake.

Fixes #2230

Diff: diff --git a/lib/websocket-server.js b/lib/websocket-server.js
index 40980f6..67b52ff 100644
--- a/lib/websocket-server.js
+++ b/lib/websocket-server.js
@@ -233,25 +233,26 @@ class WebSocketServer extends EventEmitter {
    */
   handleUpgrade(req, socket, head, cb) {
     socket.on('error', socketOnError);
 
     const key = req.headers['sec-websocket-key'];
+    const upgrade = req.headers.upgrade;
     const version = +req.headers['sec-websocket-version'];
 
     if (req.method !== 'GET') {
       const message = 'Invalid HTTP method';
       abortHandshakeOrEmitwsClientError(this, req, socket, 405, message);
       return;
     }
 
-    if (req.headers.upgrade.toLowerCase() !== 'websocket') {
+    if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') {
       const message = 'Invalid Upgrade header';
       abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
       return;
     }
 
-    if (!key || !keyRegex.test(key)) {
+    if (key === undefined || !keyRegex.test(key)) {
       const message = 'Missing or invalid Sec-WebSocket-Key header';
       abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
       return;
     }
 
diff --git a/lib/websocket.js b/lib/websocket.js
index 709ad82..aa57bba 100644
--- a/lib/websocket.js
+++ b/lib/websocket.js
@@ -926,11 +926,13 @@ function initAsClient(websocket, address, protocols, options) {
     //
     if (websocket.readyState !== WebSocket.CONNECTING) return;
 
     req = websocket._req = null;
 
-    if (res.headers.upgrade.toLowerCase() !== 'websocket') {
+    const upgrade = res.headers.upgrade;
+
+    if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') {
       abortHandshake(websocket, socket, 'Invalid Upgrade header');
       return;
     }
 
     const digest = createHash('sha1')
diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js
index 44c2c67..34de4dc 100644
--- a/test/websocket-server.test.js
+++ b/test/websocket-server.test.js
@@ -651,10 +651,54 @@ describe('WebSocketServer', () => {
       wss.on('connection', () => {
         done(new Error("Unexpected 'connection' event"));
       });
     });
 
+    it('fails if the Upgrade header field value cannot be read', (done) => {
+      const server = http.createServer();
+      const wss = new WebSocket.Server({ noServer: true });
+
+      server.maxHeadersCount = 1;
+
+      server.on('upgrade', (req, socket, head) => {
+        assert.deepStrictEqual(req.headers, { foo: 'bar' });
+        wss.handleUpgrade(req, socket, head, () => {
+          done(new Error('Unexpected callback invocation'));
+        });
+      });
+
+      server.listen(() => {
+        const req = http.get({
+          port: server.address().port,
+          headers: {
+            foo: 'bar',
+            bar: 'baz',
+            Connection: 'Upgrade',
+            Upgrade: 'websocket'
+          }
+        });
+
+        req.on('response', (res) => {
+          assert.strictEqual(res.statusCode, 400);
+
+          const chunks = [];
+
+          res.on('data', (chunk) => {
+            chunks.push(chunk);
+          });
+
+          res.on('end', () => {
+            assert.strictEqual(
+              Buffer.concat(chunks).toString(),
+              'Invalid Upgrade header'
+            );
+            server.close(done);
+          });
+        });
+      });
+    });
+
     it('fails if the Upgrade header field value is not "websocket"', (done) => {
       const wss = new WebSocket.Server({ port: 0 }, () => {
         const req = http.get({
           port: wss.address().port,
           headers: {
diff --git a/test/websocket.test.js b/test/websocket.test.js
index aa53c3b..8a05f07 100644
--- a/test/websocket.test.js
+++ b/test/websocket.test.js
@@ -755,10 +755,36 @@ describe('WebSocket', () => {
     const server = http.createServer();
 
     beforeEach((done) => server.listen(0, done));
     afterEach((done) => server.close(done));
 
+    it('fails if the Upgrade header field value cannot be read', (done) => {
+      server.once('upgrade', (req, socket) => {
+        socket.on('end', socket.end);
+        socket.write(
+          'HTTP/1.1 101 Switching Protocols\r\n' +
+            'Connection: Upgrade\r\n' +
+            'Upgrade: websocket\r\n' +
+            '\r\n'
+        );
+      });
+
+      const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+      ws._req.maxHeadersCount = 1;
+
+      ws.on('upgrade', (res) => {
+        assert.deepStrictEqual(res.headers, { connection: 'Upgrade' });
+
+        ws.on('error', (err) => {
+          assert.ok(err instanceof Error);
+          assert.strictEqual(err.message, 'Invalid Upgrade header');
+          done();
+        });
+      });
+    });
+
     it('fails if the Upgrade header field value is not "websocket"', (done) => {
       server.once('upgrade', (req, socket) => {
         socket.on('end', socket.end);
         socket.write(
           'HTTP/1.1 101 Switching Protocols\r\n' +

Vulnerable code:     if (req.headers.upgrade.toLowerCase() !== 'websocket') {
    if (!key || !keyRegex.test(key)) {
    if (res.headers.upgrade.toLowerCase() !== 'websocket') {
Fixed code:     const upgrade = req.headers.upgrade;
    if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') {
    if (key === undefined || !keyRegex.test(key)) {
    const upgrade = res.headers.upgrade;

    if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') {
    it('fails if the Upgrade header field value cannot be read', (done) => {
      const server = http.createServer();
      const wss = new WebSocket.Server({ noServer: true });

      server.maxHeadersCount = 1;

      server.on('upgrade', (req, socket, head) => {
        assert.deepStrictEqual(req.headers, { foo: 'bar' });
        wss.handleUpgrade(req, socket, head, () => {
          done(new Error('Unexpected callback invocation'));
        });
      });

      server.listen(() => {
        const req = http.get({
          port: server.address().port,
          headers: {
            foo: 'bar',
            bar: 'baz',
            Connection: 'Upgrade',
            Upgrade: 'websocket'
          }
        });

        req.on('response', (res) => {
          assert.strictEqual(res.statusCode, 400);

          const chunks = [];

          res.on('data', (chunk) => {
            chunks.push(chunk);
          });

          res.on('end', () => {
            assert.strictEqual(
              Buffer.concat(chunks).toString(),
              'Invalid Upgrade header'
            );
            server.close(done);
          });
        });
      });
    });

    it('fails if the Upgrade header field value cannot be read', (done) => {
      server.once('upgrade', (req, socket) => {
        socket.on('end', socket.end);
        socket.write(
          'HTTP/1.1 101 Switching Protocols\r\n' +
            'Connection: Upgrade\r\n' +
            'Upgrade: websocket\r\n' +
            '\r\n'
        );
      });

      const ws = new WebSocket(`ws://localhost:${server.address().port}`);

      ws._req.maxHeadersCount = 1;

      ws.on('upgrade', (res) => {
        assert.deepStrictEqual(res.headers, { connection: 'Upgrade' });

        ws.on('error', (err) => {
          assert.ok(err instanceof Error);
          assert.strictEqual(err.message, 'Invalid Upgrade header');
          done();
        });
      });
    });