From fac8994f104ff55a64a9d8128e1346d30997de78 Mon Sep 17 00:00:00 2001 From: Luigi Pinca Date: Sun, 16 Jun 2024 11:30:42 +0200 Subject: [PATCH] [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 --- lib/websocket-server.js | 5 ++-- lib/websocket.js | 4 +++- test/websocket-server.test.js | 44 +++++++++++++++++++++++++++++++++++ test/websocket.test.js | 26 +++++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/lib/websocket-server.js b/lib/websocket-server.js index 40980f6e9..67b52ffdd 100644 --- a/lib/websocket-server.js +++ b/lib/websocket-server.js @@ -235,6 +235,7 @@ class WebSocketServer extends EventEmitter { 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') { @@ -243,13 +244,13 @@ class WebSocketServer extends EventEmitter { 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 709ad825a..aa57bbade 100644 --- a/lib/websocket.js +++ b/lib/websocket.js @@ -928,7 +928,9 @@ function initAsClient(websocket, address, protocols, options) { 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; } diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js index 44c2c6709..b31096823 100644 --- a/test/websocket-server.test.js +++ b/test/websocket-server.test.js @@ -653,6 +653,50 @@ describe('WebSocketServer', () => { }); }); + it('fails if the Upgrade header is missing', (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({ diff --git a/test/websocket.test.js b/test/websocket.test.js index aa53c3bc9..1432bf41f 100644 --- a/test/websocket.test.js +++ b/test/websocket.test.js @@ -757,6 +757,32 @@ describe('WebSocket', () => { beforeEach((done) => server.listen(0, done)); afterEach((done) => server.close(done)); + it('fails if the Upgrade header is missing', (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);