From 4b4f93ae292f0d245d3957bc08ee2e03d0839faa Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 16 Jan 2026 12:51:59 +0100 Subject: [PATCH 01/11] fix(websocket): add maxDecompressedMessageSize limit for permessage-deflate Add protection against decompression bomb attacks in WebSocket permessage-deflate extension. A malicious server could send a small compressed payload that expands to an extremely large size, causing memory exhaustion. Changes: - Add maxDecompressedMessageSize option to WebSocket constructor - Default limit: 4 MB - Abort decompression immediately when limit exceeded - Close connection with status code 1009 (Message Too Big) - Add MessageSizeExceededError (UND_ERR_WS_MESSAGE_SIZE_EXCEEDED) - Add comprehensive tests for the new limit behavior - Update TypeScript types and documentation Signed-off-by: Matteo Collina (cherry picked from commit 2ee00cb322c76b0bf56829462d7c1dc53d1cbe3d) --- docs/docs/api/Errors.md | 1 + docs/docs/api/WebSocket.md | 23 ++ lib/core/errors.js | 21 +- lib/web/websocket/permessage-deflate.js | 52 +++- lib/web/websocket/receiver.js | 18 +- lib/web/websocket/websocket.js | 25 +- test/websocket/permessage-deflate-limit.js | 282 +++++++++++++++++++++ types/errors.d.ts | 6 + types/websocket.d.ts | 9 +- 9 files changed, 428 insertions(+), 9 deletions(-) create mode 100644 test/websocket/permessage-deflate-limit.js diff --git a/docs/docs/api/Errors.md b/docs/docs/api/Errors.md index c32868912a6..a6af44de762 100644 --- a/docs/docs/api/Errors.md +++ b/docs/docs/api/Errors.md @@ -27,6 +27,7 @@ import { errors } from 'undici' | `InformationalError` | `UND_ERR_INFO` | expected error with reason | | `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed | | `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed | +| `MessageSizeExceededError` | `UND_ERR_WS_MESSAGE_SIZE_EXCEEDED` | WebSocket decompressed message exceeded the maximum allowed size | ### `SocketError` diff --git a/docs/docs/api/WebSocket.md b/docs/docs/api/WebSocket.md index 9d374f4046c..a9c5619cadf 100644 --- a/docs/docs/api/WebSocket.md +++ b/docs/docs/api/WebSocket.md @@ -13,6 +13,15 @@ Arguments: * **url** `URL | string` - The url's protocol *must* be `ws` or `wss`. * **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](./Dispatcher.md). +### WebSocketInit + +When passing an object as the second argument, the following options are available: + +* **protocols** `string | string[]` (optional) - Subprotocol(s) to request the server use. +* **dispatcher** `Dispatcher` (optional) - A custom [`Dispatcher`](/docs/docs/api/Dispatcher.md) to use for the connection. +* **headers** `HeadersInit` (optional) - Custom headers to include in the WebSocket handshake request. +* **maxDecompressedMessageSize** `number` (optional) - Maximum allowed size in bytes for decompressed messages when using the `permessage-deflate` extension. **Default:** `4194304` (4 MB). + ### Example: This example will not work in browsers or other platforms that don't allow passing an object. @@ -36,6 +45,20 @@ import { WebSocket } from 'undici' const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat']) ``` +### Example with custom decompression limit: + +To protect against decompression bombs (small compressed payloads that expand to very large sizes), you can set a custom limit: + +```mjs +import { WebSocket } from 'undici' + +const ws = new WebSocket('wss://echo.websocket.events', { + maxDecompressedMessageSize: 1 * 1024 * 1024 +}) +``` + +> ⚠️ **Security Note**: The `maxDecompressedMessageSize` option protects against memory exhaustion attacks where a malicious server sends a small compressed payload that decompresses to an extremely large size. If you increase this limit significantly above the default, ensure your application can handle the increased memory usage. + ## Read More - [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) diff --git a/lib/core/errors.js b/lib/core/errors.js index 535c7339e39..202880132db 100644 --- a/lib/core/errors.js +++ b/lib/core/errors.js @@ -379,6 +379,24 @@ class SecureProxyConnectionError extends UndiciError { [kSecureProxyConnectionError] = true } +const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED') +class MessageSizeExceededError extends UndiciError { + constructor (message) { + super(message) + this.name = 'MessageSizeExceededError' + this.message = message || 'Max decompressed message size exceeded' + this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED' + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kMessageSizeExceededError] === true + } + + get [kMessageSizeExceededError] () { + return true + } +} + module.exports = { AbortError, HTTPParserError, @@ -402,5 +420,6 @@ module.exports = { ResponseExceededMaxSizeError, RequestRetryError, ResponseError, - SecureProxyConnectionError + SecureProxyConnectionError, + MessageSizeExceededError } diff --git a/lib/web/websocket/permessage-deflate.js b/lib/web/websocket/permessage-deflate.js index 76cb366d5e5..81e6953bec5 100644 --- a/lib/web/websocket/permessage-deflate.js +++ b/lib/web/websocket/permessage-deflate.js @@ -2,20 +2,38 @@ const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib') const { isValidClientWindowBits } = require('./util') +const { MessageSizeExceededError } = require('../../core/errors') const tail = Buffer.from([0x00, 0x00, 0xff, 0xff]) const kBuffer = Symbol('kBuffer') const kLength = Symbol('kLength') +// Default maximum decompressed message size: 4 MB +const kDefaultMaxDecompressedSize = 4 * 1024 * 1024 + class PerMessageDeflate { /** @type {import('node:zlib').InflateRaw} */ #inflate #options = {} - constructor (extensions) { + /** @type {number} */ + #maxDecompressedSize + + /** @type {boolean} */ + #aborted = false + + /** @type {Function|null} */ + #currentCallback = null + + /** + * @param {Map} extensions + * @param {{ maxDecompressedMessageSize?: number }} [options] + */ + constructor (extensions, options = {}) { this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover') this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits') + this.#maxDecompressedSize = options.maxDecompressedMessageSize ?? kDefaultMaxDecompressedSize } decompress (chunk, fin, callback) { @@ -24,6 +42,11 @@ class PerMessageDeflate { // payload of the message. // 2. Decompress the resulting data using DEFLATE. + if (this.#aborted) { + callback(new MessageSizeExceededError()) + return + } + if (!this.#inflate) { let windowBits = Z_DEFAULT_WINDOWBITS @@ -41,8 +64,27 @@ class PerMessageDeflate { this.#inflate[kLength] = 0 this.#inflate.on('data', (data) => { - this.#inflate[kBuffer].push(data) + if (this.#aborted) { + return + } + this.#inflate[kLength] += data.length + + if (this.#inflate[kLength] > this.#maxDecompressedSize) { + this.#aborted = true + this.#inflate.removeAllListeners() + this.#inflate.destroy() + this.#inflate = null + + if (this.#currentCallback) { + const cb = this.#currentCallback + this.#currentCallback = null + cb(new MessageSizeExceededError()) + } + return + } + + this.#inflate[kBuffer].push(data) }) this.#inflate.on('error', (err) => { @@ -51,16 +93,22 @@ class PerMessageDeflate { }) } + this.#currentCallback = callback this.#inflate.write(chunk) if (fin) { this.#inflate.write(tail) } this.#inflate.flush(() => { + if (this.#aborted || !this.#inflate) { + return + } + const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength]) this.#inflate[kBuffer].length = 0 this.#inflate[kLength] = 0 + this.#currentCallback = null callback(null, full) }) diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js index 581c251074c..82a3553d14b 100644 --- a/lib/web/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -18,6 +18,7 @@ const { const { WebsocketFrameSend } = require('./frame') const { closeWebSocketConnection } = require('./connection') const { PerMessageDeflate } = require('./permessage-deflate') +const { MessageSizeExceededError } = require('../../core/errors') // This code was influenced by ws released under the MIT license. // Copyright (c) 2011 Einar Otto Stangvik @@ -37,14 +38,23 @@ class ByteParser extends Writable { /** @type {Map} */ #extensions - constructor (ws, extensions) { + /** @type {{ maxDecompressedMessageSize?: number }} */ + #options + + /** + * @param {import('./websocket').WebSocket} ws + * @param {Map|null} extensions + * @param {{ maxDecompressedMessageSize?: number }} [options] + */ + constructor (ws, extensions, options = {}) { super() this.ws = ws this.#extensions = extensions == null ? new Map() : extensions + this.#options = options if (this.#extensions.has('permessage-deflate')) { - this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions)) + this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options)) } } @@ -223,7 +233,9 @@ class ByteParser extends Writable { } else { this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => { if (error) { - closeWebSocketConnection(this.ws, 1007, error.message, error.message.length) + // Use 1009 (Message Too Big) for decompression size limit errors + const code = error instanceof MessageSizeExceededError ? 1009 : 1007 + closeWebSocketConnection(this.ws, code, error.message, error.message.length) return } diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index e4053024756..663bc5bcaea 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -44,6 +44,9 @@ class WebSocket extends EventTarget { /** @type {SendQueue} */ #sendQueue + /** @type {{ maxDecompressedMessageSize?: number }} */ + #options + /** * @param {string} url * @param {string|string[]} protocols @@ -117,6 +120,11 @@ class WebSocket extends EventTarget { // 10. Set this's url to urlRecord. this[kWebSocketURL] = new URL(urlRecord.href) + // Store options for later use (e.g., maxDecompressedMessageSize) + this.#options = { + maxDecompressedMessageSize: options.maxDecompressedMessageSize + } + // 11. Let client be this's relevant settings object. const client = environmentSettingsObject.settingsObject @@ -431,11 +439,11 @@ class WebSocket extends EventTarget { * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol */ #onConnectionEstablished (response, parsedExtensions) { - // processResponse is called when the "response’s header list has been received and initialized." + // processResponse is called when the "response's header list has been received and initialized." // once this happens, the connection is open this[kResponse] = response - const parser = new ByteParser(this, parsedExtensions) + const parser = new ByteParser(this, parsedExtensions, this.#options) parser.on('drain', onParserDrain) parser.on('error', onParserError.bind(this)) @@ -538,6 +546,19 @@ webidl.converters.WebSocketInit = webidl.dictionaryConverter([ { key: 'headers', converter: webidl.nullableConverter(webidl.converters.HeadersInit) + }, + { + key: 'maxDecompressedMessageSize', + converter: webidl.nullableConverter((V) => { + V = webidl.converters['unsigned long long'](V) + if (V <= 0) { + throw webidl.errors.exception({ + header: 'WebSocket constructor', + message: 'maxDecompressedMessageSize must be greater than 0' + }) + } + return V + }) } ]) diff --git a/test/websocket/permessage-deflate-limit.js b/test/websocket/permessage-deflate-limit.js new file mode 100644 index 00000000000..d2a7b2193c6 --- /dev/null +++ b/test/websocket/permessage-deflate-limit.js @@ -0,0 +1,282 @@ +'use strict' + +const { test } = require('node:test') +const { once } = require('node:events') +const http = require('node:http') +const crypto = require('node:crypto') +const zlib = require('node:zlib') +const { WebSocketServer } = require('ws') +const { WebSocket } = require('../..') + +/** + * Creates a WebSocket frame. + * @param {object} options + * @param {number} options.opcode - Frame opcode (1=text, 2=binary) + * @param {boolean} options.fin - Final frame flag + * @param {boolean} options.rsv1 - RSV1 flag (compression) + * @param {Buffer} options.payload - Frame payload + * @returns {Buffer} + */ +function createWebSocketFrame ({ opcode, fin = true, rsv1 = false, payload }) { + const payloadLength = payload.length + let headerLength = 2 + + if (payloadLength > 65535) { + headerLength += 8 + } else if (payloadLength > 125) { + headerLength += 2 + } + + const header = Buffer.alloc(headerLength) + + // First byte: FIN + RSV1 + opcode + header[0] = (fin ? 0x80 : 0x00) | (rsv1 ? 0x40 : 0x00) | opcode + + // Second byte: MASK (0) + payload length + if (payloadLength > 65535) { + header[1] = 127 + header.writeBigUInt64BE(BigInt(payloadLength), 2) + } else if (payloadLength > 125) { + header[1] = 126 + header.writeUInt16BE(payloadLength, 2) + } else { + header[1] = payloadLength + } + + return Buffer.concat([header, payload]) +} + +/** + * Creates a compressed payload using DEFLATE raw. + * @param {number} targetSize - Target decompressed size in bytes + * @returns {Buffer} + */ +function createCompressedPayload (targetSize) { + // Create highly compressible data (repeated 'A' characters) + const data = Buffer.alloc(targetSize, 0x41) + return zlib.deflateRawSync(data) +} + +test('Compressed message under limit decompresses successfully', async (t) => { + const server = new WebSocketServer({ + port: 0, + perMessageDeflate: true + }) + + t.after(() => server.close()) + + await once(server, 'listening') + + server.on('connection', (ws) => { + // Send 1 KB of data (well under any reasonable limit) + ws.send(Buffer.alloc(1024, 0x41), { binary: true }) + }) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) + + const [event] = await once(client, 'message') + t.assert.strictEqual(event.data.size, 1024) + client.close() +}) + +test('Custom maxDecompressedMessageSize is enforced', async (t) => { + const server = new WebSocketServer({ + port: 0, + perMessageDeflate: true + }) + + t.after(() => server.close()) + + await once(server, 'listening') + + let messageReceived = false + + server.on('connection', (ws) => { + // Send 2 MB of data + ws.send(Buffer.alloc(2 * 1024 * 1024, 0x41), { binary: true }) + }) + + // Set custom limit of 1 MB + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`, { + maxDecompressedMessageSize: 1 * 1024 * 1024 + }) + + client.addEventListener('message', () => { + messageReceived = true + }) + + // Wait for the connection to close + await once(client, 'close') + + // The message should NOT have been received due to size limit + t.assert.strictEqual(messageReceived, false) +}) + +test('Message exactly at limit succeeds', async (t) => { + const limit = 1 * 1024 * 1024 // 1 MB + const server = new WebSocketServer({ + port: 0, + perMessageDeflate: true + }) + + t.after(() => server.close()) + + await once(server, 'listening') + + server.on('connection', (ws) => { + ws.send(Buffer.alloc(limit, 0x41), { binary: true }) + }) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`, { + maxDecompressedMessageSize: limit + }) + + const [event] = await once(client, 'message') + t.assert.strictEqual(event.data.size, limit) + client.close() +}) + +test('Message one byte over limit fails', async (t) => { + const limit = 1 * 1024 * 1024 // 1 MB + const server = new WebSocketServer({ + port: 0, + perMessageDeflate: true + }) + + t.after(() => server.close()) + + await once(server, 'listening') + + server.on('connection', (ws) => { + ws.send(Buffer.alloc(limit + 1, 0x41), { binary: true }) + }) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`, { + maxDecompressedMessageSize: limit + }) + + const [event] = await once(client, 'error') + t.assert.ok(event.error instanceof Error) +}) + +test('Connection closes when limit exceeded', async (t) => { + const server = new WebSocketServer({ + port: 0, + perMessageDeflate: true + }) + + t.after(() => server.close()) + + await once(server, 'listening') + + server.on('connection', (ws) => { + ws.send(Buffer.alloc(2 * 1024 * 1024, 0x41), { binary: true }) + }) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`, { + maxDecompressedMessageSize: 1 * 1024 * 1024 + }) + + const [event] = await once(client, 'close') + // Connection should be closed - code 1006 (abnormal) or 1009 (too big) + t.assert.ok(event.code === 1006 || event.code === 1009) +}) + +test('Non-compressed messages are not affected by decompression limit', async (t) => { + const server = new WebSocketServer({ + port: 0, + perMessageDeflate: false // Compression disabled + }) + + t.after(() => server.close()) + + await once(server, 'listening') + + server.on('connection', (ws) => { + ws.send(Buffer.alloc(2 * 1024 * 1024, 0x41), { binary: true }) + }) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`, { + maxDecompressedMessageSize: 1 * 1024 * 1024 + }) + + // Should succeed because compression is not used + const [event] = await once(client, 'message') + t.assert.strictEqual(event.data.size, 2 * 1024 * 1024) + client.close() +}) + +test('Decompression bomb is mitigated via raw WebSocket handshake', async (t) => { + // This test validates the fix using a technique similar to the original PoC + // by creating a minimal malicious server that sends a compressed payload + const server = http.createServer() + + let messageReceived = false + + server.on('upgrade', (req, socket) => { + const key = req.headers['sec-websocket-key'] + const accept = crypto + .createHash('sha1') + .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11') + .digest('base64') + + socket.write([ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Accept: ${accept}`, + 'Sec-WebSocket-Extensions: permessage-deflate', + '', '' + ].join('\r\n')) + + // Send a small payload that decompresses to ~10 MB + setTimeout(() => { + const bomb = createCompressedPayload(10 * 1024 * 1024) + const frame = createWebSocketFrame({ opcode: 2, rsv1: true, payload: bomb }) + socket.write(frame) + }, 100) + }) + + await new Promise(resolve => server.listen(0, resolve)) + t.after(() => server.close()) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`, { + maxDecompressedMessageSize: 1 * 1024 * 1024 // 1 MB limit + }) + + client.addEventListener('message', () => { + messageReceived = true + }) + + // Wait for the connection to close + await once(client, 'close') + + // The message should NOT have been received due to size limit + t.assert.strictEqual(messageReceived, false) +}) + +test('Higher custom limit allows larger messages', async (t) => { + const server = new WebSocketServer({ + port: 0, + perMessageDeflate: true + }) + + t.after(() => server.close()) + + await once(server, 'listening') + + const dataSize = 5 * 1024 * 1024 // 5 MB + + server.on('connection', (ws) => { + ws.send(Buffer.alloc(dataSize, 0x41), { binary: true }) + }) + + // Set custom limit of 10 MB + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`, { + maxDecompressedMessageSize: 10 * 1024 * 1024 + }) + + const [event] = await once(client, 'message') + t.assert.strictEqual(event.data.size, dataSize) + client.close() +}) diff --git a/types/errors.d.ts b/types/errors.d.ts index f6fb73b5a90..654988423d7 100644 --- a/types/errors.d.ts +++ b/types/errors.d.ts @@ -146,4 +146,10 @@ declare namespace Errors { name: 'SecureProxyConnectionError'; code: 'UND_ERR_PRX_TLS'; } + + /** WebSocket decompressed message exceeded maximum size. */ + export class MessageSizeExceededError extends UndiciError { + name: 'MessageSizeExceededError' + code: 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED' + } } diff --git a/types/websocket.d.ts b/types/websocket.d.ts index dfdd8156cce..787f5ee5f32 100644 --- a/types/websocket.d.ts +++ b/types/websocket.d.ts @@ -146,5 +146,12 @@ export declare const ErrorEvent: { interface WebSocketInit { protocols?: string | string[], dispatcher?: Dispatcher, - headers?: HeadersInit + headers?: HeadersInit, + /** + * Maximum size in bytes for decompressed WebSocket messages. + * When a message exceeds this limit during decompression, the connection + * will be closed with status code 1009 (Message Too Big). + * @default 4194304 (4 MB) + */ + maxDecompressedMessageSize?: number } From e9e2997ed18bff6ae389712d5f0e169f8a6546a0 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 16 Jan 2026 14:09:55 +0100 Subject: [PATCH 02/11] fix: validate server_max_window_bits range in permessage-deflate The isValidClientWindowBits() function only checked for ASCII digits, allowing out-of-range values like "1000" to pass validation. When these values were passed to zlib's createInflateRaw(), it threw an unhandled RangeError that crashed the process. Changes: - Update isValidClientWindowBits() to validate range 8-15 (per RFC 7692) - Add try-catch around createInflateRaw() as defense in depth - Add comprehensive tests for windowBits validation (cherry picked from commit cb79c5704ac47e42ce01a72269994fc70e377536) --- lib/web/websocket/permessage-deflate.js | 7 +- lib/web/websocket/util.js | 10 +- .../permessage-deflate-windowbits.js | 242 ++++++++++++++++++ 3 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 test/websocket/permessage-deflate-windowbits.js diff --git a/lib/web/websocket/permessage-deflate.js b/lib/web/websocket/permessage-deflate.js index 81e6953bec5..9ff22ef2965 100644 --- a/lib/web/websocket/permessage-deflate.js +++ b/lib/web/websocket/permessage-deflate.js @@ -59,7 +59,12 @@ class PerMessageDeflate { windowBits = Number.parseInt(this.#options.serverMaxWindowBits) } - this.#inflate = createInflateRaw({ windowBits }) + try { + this.#inflate = createInflateRaw({ windowBits }) + } catch (err) { + callback(err) + return + } this.#inflate[kBuffer] = [] this.#inflate[kLength] = 0 diff --git a/lib/web/websocket/util.js b/lib/web/websocket/util.js index e5ce7899752..2a04887f263 100644 --- a/lib/web/websocket/util.js +++ b/lib/web/websocket/util.js @@ -266,6 +266,12 @@ function parseExtensions (extensions) { * @param {string} value */ function isValidClientWindowBits (value) { + // Must have at least one character + if (value.length === 0) { + return false + } + + // Check all characters are ASCII digits for (let i = 0; i < value.length; i++) { const byte = value.charCodeAt(i) @@ -274,7 +280,9 @@ function isValidClientWindowBits (value) { } } - return true + // Check numeric range: zlib requires windowBits in range 8-15 + const num = Number.parseInt(value, 10) + return num >= 8 && num <= 15 } // https://nodejs.org/api/intl.html#detecting-internationalization-support diff --git a/test/websocket/permessage-deflate-windowbits.js b/test/websocket/permessage-deflate-windowbits.js new file mode 100644 index 00000000000..762125e3b9a --- /dev/null +++ b/test/websocket/permessage-deflate-windowbits.js @@ -0,0 +1,242 @@ +'use strict' + +const { describe, test, after } = require('node:test') +const { once } = require('node:events') +const http = require('node:http') +const crypto = require('node:crypto') +const zlib = require('node:zlib') +const { WebSocket } = require('../..') +const { isValidClientWindowBits } = require('../../lib/web/websocket/util') + +/** + * Creates a minimal WebSocket server that responds with a custom + * server_max_window_bits value and sends a compressed frame. + */ +function createMaliciousServer (windowBitsValue) { + const server = http.createServer() + const sockets = new Set() + + server.on('upgrade', (req, socket) => { + sockets.add(socket) + socket.on('close', () => sockets.delete(socket)) + + const key = req.headers['sec-websocket-key'] + const accept = crypto + .createHash('sha1') + .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11') + .digest('base64') + + const headers = [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Accept: ${accept}`, + `Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=${windowBitsValue}`, + '', '' + ] + + socket.write(headers.join('\r\n')) + + // Send a compressed frame to trigger decompression + setTimeout(() => { + if (!socket.destroyed) { + const payload = zlib.deflateRawSync(Buffer.from('Hello')) + // Remove trailing 00 00 ff ff if present + const trimmed = payload.subarray(0, payload.length - 4) + const frame = makeWsFrame({ opcode: 2, rsv1: true, payload: trimmed }) + socket.write(frame) + } + }, 50) + }) + + // Override close to also destroy sockets + const originalClose = server.close.bind(server) + server.close = (cb) => { + for (const socket of sockets) { + socket.destroy() + } + sockets.clear() + originalClose(cb) + } + + return server +} + +/** + * Creates a WebSocket frame (server-to-client, unmasked) + */ +function makeWsFrame ({ opcode, rsv1, payload }) { + const fin = 1 + const b0 = (fin << 7) | ((rsv1 ? 1 : 0) << 6) | (opcode & 0x0f) + const len = payload.length + + let header + if (len <= 125) { + header = Buffer.from([b0, len]) + } else if (len <= 0xffff) { + header = Buffer.alloc(4) + header[0] = b0 + header[1] = 126 + header.writeUInt16BE(len, 2) + } else { + header = Buffer.alloc(10) + header[0] = b0 + header[1] = 127 + header.writeUInt32BE(0, 2) + header.writeUInt32BE(len, 6) + } + + return Buffer.concat([header, payload]) +} + +describe('isValidClientWindowBits', () => { + test('rejects empty string', (t) => { + t.assert.strictEqual(isValidClientWindowBits(''), false) + }) + + test('rejects values below 8', (t) => { + t.assert.strictEqual(isValidClientWindowBits('0'), false) + t.assert.strictEqual(isValidClientWindowBits('1'), false) + t.assert.strictEqual(isValidClientWindowBits('7'), false) + }) + + test('accepts values 8-15', (t) => { + for (let i = 8; i <= 15; i++) { + t.assert.strictEqual(isValidClientWindowBits(String(i)), true, `${i} should be valid`) + } + }) + + test('rejects values above 15', (t) => { + t.assert.strictEqual(isValidClientWindowBits('16'), false) + t.assert.strictEqual(isValidClientWindowBits('100'), false) + t.assert.strictEqual(isValidClientWindowBits('1000'), false) + t.assert.strictEqual(isValidClientWindowBits('999999'), false) + }) + + test('rejects non-numeric values', (t) => { + t.assert.strictEqual(isValidClientWindowBits('abc'), false) + t.assert.strictEqual(isValidClientWindowBits('12a'), false) + t.assert.strictEqual(isValidClientWindowBits('-1'), false) + t.assert.strictEqual(isValidClientWindowBits('8.5'), false) + }) +}) + +describe('permessage-deflate server_max_window_bits', () => { + test('server_max_window_bits=8 works correctly', async (t) => { + const server = createMaliciousServer('8') + await new Promise(resolve => server.listen(0, resolve)) + after(() => server.close()) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) + + const [event] = await once(client, 'message') + t.assert.ok(event.data) + client.close() + }) + + test('server_max_window_bits=15 works correctly', async (t) => { + const server = createMaliciousServer('15') + await new Promise(resolve => server.listen(0, resolve)) + after(() => server.close()) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) + + const [event] = await once(client, 'message') + t.assert.ok(event.data) + client.close() + }) + + test('server_max_window_bits=0 is rejected gracefully', async (t) => { + const server = createMaliciousServer('0') + await new Promise(resolve => server.listen(0, resolve)) + after(() => server.close()) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) + + const [event] = await once(client, 'error') + t.assert.ok(event.error instanceof Error) + }) + + test('server_max_window_bits=7 is rejected gracefully', async (t) => { + const server = createMaliciousServer('7') + await new Promise(resolve => server.listen(0, resolve)) + after(() => server.close()) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) + + const [event] = await once(client, 'error') + t.assert.ok(event.error instanceof Error) + }) + + test('server_max_window_bits=16 is rejected gracefully', async (t) => { + const server = createMaliciousServer('16') + await new Promise(resolve => server.listen(0, resolve)) + after(() => server.close()) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) + + const [event] = await once(client, 'error') + t.assert.ok(event.error instanceof Error) + }) + + test('server_max_window_bits=1000 is rejected gracefully (PoC attack)', async (t) => { + const server = createMaliciousServer('1000') + await new Promise(resolve => server.listen(0, resolve)) + after(() => server.close()) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) + + const [event] = await once(client, 'error') + // The key assertion: we got an error event instead of crashing + t.assert.ok(event.error instanceof Error, 'Should receive an Error') + }) + + test('no uncaught exception with invalid windowBits', async (t) => { + let uncaughtException = false + + const handler = () => { + uncaughtException = true + } + process.on('uncaughtException', handler) + + const server = createMaliciousServer('1000') + await new Promise(resolve => server.listen(0, resolve)) + after(() => { + server.close() + process.off('uncaughtException', handler) + }) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) + + await Promise.race([ + once(client, 'error'), + once(client, 'close'), + new Promise(resolve => setTimeout(resolve, 1000)) + ]) + + t.assert.strictEqual(uncaughtException, false, 'No uncaught exception should occur') + }) + + test('invalid windowBits closes connection without crashing process', async (t) => { + const server = createMaliciousServer('999999') + await new Promise(resolve => server.listen(0, resolve)) + after(() => server.close()) + + const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) + + // Wait for either error or close event + const result = await Promise.race([ + once(client, 'error').then(([e]) => ({ type: 'error', event: e })), + once(client, 'close').then(([e]) => ({ type: 'close', event: e })) + ]) + + // Either error or close is acceptable, as long as we didn't crash + t.assert.ok( + result.type === 'error' || result.type === 'close', + 'Connection should close gracefully' + ) + + // This assertion is reached = process did not crash + t.assert.ok(true, 'Process did not crash') + }) +}) From e43e898603dd5e0c14a75b08b83257598d664a39 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 16 Jan 2026 16:04:10 +0100 Subject: [PATCH 03/11] fix: validate upgrade header to prevent CRLF injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add validation for the upgrade option in Request constructor using isValidHeaderValue() to prevent CRLF injection attacks that could enable protocol smuggling to internal services. Signed-off-by: Matteo Collina Co-Authored-By: Ulises Gascón Signed-off-by: Ulises Gascón (cherry picked from commit 77594f923cef4c27ee0bad365e7b4c44a199edae) --- lib/core/request.js | 4 + test/upgrade-crlf.js | 188 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 test/upgrade-crlf.js diff --git a/lib/core/request.js b/lib/core/request.js index 78003038ba9..98cd2945207 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -66,6 +66,10 @@ class Request { throw new InvalidArgumentError('upgrade must be a string') } + if (upgrade && !isValidHeaderValue(upgrade)) { + throw new InvalidArgumentError('invalid upgrade header') + } + if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { throw new InvalidArgumentError('invalid headersTimeout') } diff --git a/test/upgrade-crlf.js b/test/upgrade-crlf.js new file mode 100644 index 00000000000..2fcb15deb21 --- /dev/null +++ b/test/upgrade-crlf.js @@ -0,0 +1,188 @@ +'use strict' + +const { tspl } = require('@matteo.collina/tspl') +const { test, after } = require('node:test') +const { Client, errors } = require('..') +const net = require('node:net') + +test('CRLF injection in upgrade header via CRLF sequence', async (t) => { + t = tspl(t, { plan: 2 }) + + const server = net.createServer({ joinDuplicateHeaders: true }, (c) => { + c.on('data', () => { + c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n') + }) + c.on('error', () => {}) + }) + after(() => server.close()) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + try { + await client.upgrade({ + path: '/', + method: 'GET', + protocol: 'websocket\r\n\r\nSET pwned true' + }) + t.fail('should have thrown') + } catch (err) { + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'invalid upgrade header') + } + }) + + await t.completed +}) + +test('CRLF injection in upgrade header via lone CR', async (t) => { + t = tspl(t, { plan: 2 }) + + const server = net.createServer({ joinDuplicateHeaders: true }, (c) => { + c.on('data', () => { + c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n') + }) + c.on('error', () => {}) + }) + after(() => server.close()) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + try { + await client.upgrade({ + path: '/', + method: 'GET', + protocol: 'websocket\rinjected' + }) + t.fail('should have thrown') + } catch (err) { + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'invalid upgrade header') + } + }) + + await t.completed +}) + +test('CRLF injection in upgrade header via lone LF', async (t) => { + t = tspl(t, { plan: 2 }) + + const server = net.createServer({ joinDuplicateHeaders: true }, (c) => { + c.on('data', () => { + c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n') + }) + c.on('error', () => {}) + }) + after(() => server.close()) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + try { + await client.upgrade({ + path: '/', + method: 'GET', + protocol: 'websocket\ninjected' + }) + t.fail('should have thrown') + } catch (err) { + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'invalid upgrade header') + } + }) + + await t.completed +}) + +test('CRLF injection in upgrade header via null byte', async (t) => { + t = tspl(t, { plan: 2 }) + + const server = net.createServer({ joinDuplicateHeaders: true }, (c) => { + c.on('data', () => { + c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n') + }) + c.on('error', () => {}) + }) + after(() => server.close()) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + try { + await client.upgrade({ + path: '/', + method: 'GET', + protocol: 'websocket\0injected' + }) + t.fail('should have thrown') + } catch (err) { + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'invalid upgrade header') + } + }) + + await t.completed +}) + +test('CRLF injection in upgrade option via client.request', async (t) => { + t = tspl(t, { plan: 2 }) + + const server = net.createServer({ joinDuplicateHeaders: true }, (c) => { + c.on('data', () => { + c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n') + }) + c.on('error', () => {}) + }) + after(() => server.close()) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + try { + await client.request({ + path: '/', + method: 'GET', + upgrade: 'websocket\r\n\r\nGET /smuggled HTTP/1.1' + }) + t.fail('should have thrown') + } catch (err) { + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'invalid upgrade header') + } + }) + + await t.completed +}) + +test('valid upgrade value is accepted', async (t) => { + t = tspl(t, { plan: 1 }) + + const server = net.createServer({ joinDuplicateHeaders: true }, (c) => { + c.on('data', () => { + c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n') + }) + c.on('error', () => {}) + }) + after(() => server.close()) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + const { socket } = await client.upgrade({ + path: '/', + method: 'GET', + protocol: 'websocket' + }) + t.ok(socket) + socket.destroy() + }) + + await t.completed +}) From 5a97f0893b53ba7d1d5549d3df7e55d9c2673f89 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 5 Feb 2026 20:53:23 +0000 Subject: [PATCH 04/11] Fix websocket 64-bit length overflow Signed-off-by: Matteo Collina (cherry picked from commit 84235c62e0fe7494cec13f81d5732db0859df417) --- lib/web/websocket/receiver.js | 7 +++--- test/websocket/receive.js | 23 +++++++++++++++++++ test/websocket/receiver-unit.js | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 test/websocket/receiver-unit.js diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js index 82a3553d14b..375fce98179 100644 --- a/lib/web/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -189,6 +189,7 @@ class ByteParser extends Writable { const buffer = this.consume(8) const upper = buffer.readUInt32BE(0) + const lower = buffer.readUInt32BE(4) // 2^31 is the maximum bytes an arraybuffer can contain // on 32-bit systems. Although, on 64-bit systems, this is @@ -196,14 +197,12 @@ class ByteParser extends Writable { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e - if (upper > 2 ** 31 - 1) { + if (upper !== 0 || lower > 2 ** 31 - 1) { failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.') return } - const lower = buffer.readUInt32BE(4) - - this.#info.payloadLength = (upper << 8) + lower + this.#info.payloadLength = lower this.#state = parserStates.READ_DATA } else if (this.#state === parserStates.READ_DATA) { if (this.#byteOffset < this.#info.payloadLength) { diff --git a/test/websocket/receive.js b/test/websocket/receive.js index ad14b88d904..d5aaa13be98 100644 --- a/test/websocket/receive.js +++ b/test/websocket/receive.js @@ -28,6 +28,29 @@ test('Receiving a frame with a payload length > 2^31-1 bytes', () => { }) }) +test('Receiving a 64-bit payload length with a non-zero upper word', () => { + const server = new WebSocketServer({ port: 0 }) + + server.on('connection', (ws) => { + const socket = ws._socket + + socket.write(Buffer.from([0x82, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])) + }) + + const ws = new WebSocket(`ws://localhost:${server.address().port}`) + + return new Promise((resolve, reject) => { + ws.onmessage = reject + + ws.addEventListener('error', (event) => { + assert.ok(event.error instanceof Error) + ws.close() + server.close() + resolve() + }) + }) +}) + test('Receiving an ArrayBuffer', () => { const server = new WebSocketServer({ port: 0 }) diff --git a/test/websocket/receiver-unit.js b/test/websocket/receiver-unit.js new file mode 100644 index 00000000000..2ebea57d22b --- /dev/null +++ b/test/websocket/receiver-unit.js @@ -0,0 +1,40 @@ +'use strict' + +const { test } = require('node:test') +const { ByteParser } = require('../../lib/web/websocket/receiver') +const { states } = require('../../lib/web/websocket/constants') + +const invalidFrame = Buffer.from([0x82, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]) + +test('ByteParser rejects 64-bit payload lengths with a non-zero upper word', (t) => { + const calls = { + abort: 0, + close: 0 + } + + const handler = { + readyState: states.CONNECTING, + controller: { + abort: () => { + calls.abort += 1 + } + }, + onSocketClose: () => { + calls.close += 1 + }, + closeState: new Set() + } + + const parser = new ByteParser(handler) + + parser.write(invalidFrame) + + return new Promise((resolve) => { + setImmediate(() => { + t.assert.strictEqual(calls.abort, 1) + t.assert.strictEqual(calls.close, 1) + parser.destroy() + resolve() + }) + }) +}) From 4e0179ae643e6f4380f24cc3683c1b1ca2afb094 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 16 Feb 2026 11:33:44 +0100 Subject: [PATCH 05/11] fix: reject duplicate content-length and host headers When headers are passed as an array, reject duplicate content-length and host headers regardless of casing. This prevents malformed HTTP/1.1 requests with multiple Content-Length values from being sent on the wire. Previously, case-variant duplicates (e.g., 'Content-Length' and 'content-length') would bypass the duplicate check, resulting in ambiguous HTTP requests that could be interpreted inconsistently by proxies and backends. Signed-off-by: Matteo Collina (cherry picked from commit 74495c63ab23ef39be99983ed6de81df5d203d45) --- lib/core/request.js | 10 ++++- test/headers-as-array.js | 94 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 98cd2945207..4da60667ec2 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -364,13 +364,19 @@ function processHeader (request, key, val) { val = `${val}` } - if (request.host === null && headerName === 'host') { + if (headerName === 'host') { + if (request.host !== null) { + throw new InvalidArgumentError('duplicate host header') + } if (typeof val !== 'string') { throw new InvalidArgumentError('invalid host header') } // Consumed by Client request.host = val - } else if (request.contentLength === null && headerName === 'content-length') { + } else if (headerName === 'content-length') { + if (request.contentLength !== null) { + throw new InvalidArgumentError('duplicate content-length header') + } request.contentLength = parseInt(val, 10) if (!Number.isFinite(request.contentLength)) { throw new InvalidArgumentError('invalid content-length header') diff --git a/test/headers-as-array.js b/test/headers-as-array.js index 693979e92b7..43f569011d6 100644 --- a/test/headers-as-array.js +++ b/test/headers-as-array.js @@ -153,3 +153,97 @@ test('fail if headers is not an object or an array', async (t) => { await t.completed }) + +test('fail if duplicate content-length headers (different case)', async (t) => { + t = tspl(t, { plan: 2 }) + const headers = ['Content-Length', '5', 'content-length', '0'] + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() }) + after(() => server.close()) + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'POST', + headers, + body: 'hello' + }, (err) => { + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'duplicate content-length header') + }) + }) + + await t.completed +}) + +test('fail if duplicate content-length headers (same case)', async (t) => { + t = tspl(t, { plan: 2 }) + const headers = ['content-length', '5', 'content-length', '0'] + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() }) + after(() => server.close()) + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'POST', + headers, + body: 'hello' + }, (err) => { + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'duplicate content-length header') + }) + }) + + await t.completed +}) + +test('fail if duplicate host headers (different case)', async (t) => { + t = tspl(t, { plan: 2 }) + const headers = ['Host', 'example.com', 'host', 'evil.com'] + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() }) + after(() => server.close()) + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET', + headers + }, (err) => { + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'duplicate host header') + }) + }) + + await t.completed +}) + +test('fail if duplicate host headers (same case)', async (t) => { + t = tspl(t, { plan: 2 }) + const headers = ['host', 'example.com', 'host', 'evil.com'] + + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.end() }) + after(() => server.close()) + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.close()) + + client.request({ + path: '/', + method: 'GET', + headers + }, (err) => { + t.ok(err instanceof errors.InvalidArgumentError) + t.strictEqual(err.message, 'duplicate host header') + }) + }) + + await t.completed +}) From 7df6442194b7a54e9ac734335e6e0a56a9bc6666 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 12 Mar 2026 15:24:05 +0100 Subject: [PATCH 06/11] fix: adapt websocket frame-limit handling for v6 parser Signed-off-by: Matteo Collina --- lib/web/websocket/receiver.js | 5 +---- test/websocket/receiver-unit.js | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js index 375fce98179..55130825ec6 100644 --- a/lib/web/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -18,7 +18,6 @@ const { const { WebsocketFrameSend } = require('./frame') const { closeWebSocketConnection } = require('./connection') const { PerMessageDeflate } = require('./permessage-deflate') -const { MessageSizeExceededError } = require('../../core/errors') // This code was influenced by ws released under the MIT license. // Copyright (c) 2011 Einar Otto Stangvik @@ -232,9 +231,7 @@ class ByteParser extends Writable { } else { this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => { if (error) { - // Use 1009 (Message Too Big) for decompression size limit errors - const code = error instanceof MessageSizeExceededError ? 1009 : 1007 - closeWebSocketConnection(this.ws, code, error.message, error.message.length) + failWebsocketConnection(this.ws, error.message) return } diff --git a/test/websocket/receiver-unit.js b/test/websocket/receiver-unit.js index 2ebea57d22b..64b16846432 100644 --- a/test/websocket/receiver-unit.js +++ b/test/websocket/receiver-unit.js @@ -2,37 +2,39 @@ const { test } = require('node:test') const { ByteParser } = require('../../lib/web/websocket/receiver') -const { states } = require('../../lib/web/websocket/constants') +const { kController, kResponse } = require('../../lib/web/websocket/symbols') const invalidFrame = Buffer.from([0x82, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]) test('ByteParser rejects 64-bit payload lengths with a non-zero upper word', (t) => { const calls = { abort: 0, - close: 0 + destroy: 0 } - const handler = { - readyState: states.CONNECTING, - controller: { - abort: () => { - calls.abort += 1 + const ws = new EventTarget() + ws[kController] = { + abort: () => { + calls.abort += 1 + } + } + ws[kResponse] = { + socket: { + destroyed: false, + destroy: () => { + calls.destroy += 1 } - }, - onSocketClose: () => { - calls.close += 1 - }, - closeState: new Set() + } } - const parser = new ByteParser(handler) + const parser = new ByteParser(ws) parser.write(invalidFrame) return new Promise((resolve) => { setImmediate(() => { t.assert.strictEqual(calls.abort, 1) - t.assert.strictEqual(calls.close, 1) + t.assert.strictEqual(calls.destroy, 1) parser.destroy() resolve() }) From 4cd3f4b3a2ef910ba728c47ae78294d956410450 Mon Sep 17 00:00:00 2001 From: Livia Medeiros Date: Tue, 1 Oct 2024 19:04:48 +0900 Subject: [PATCH 07/11] test: increase bitness in `test/fixtures/*.pem` (#3659) (cherry picked from commit e04abdd195d5dbaca1cc3c71c1aa32fd4b638f14) --- test/fixtures/ca.pem | 46 ++++++++++++++++++++--------- test/fixtures/cert.pem | 47 +++++++++++++++++++---------- test/fixtures/key.pem | 67 ++++++++++++++++++++++++++++++++---------- 3 files changed, 115 insertions(+), 45 deletions(-) diff --git a/test/fixtures/ca.pem b/test/fixtures/ca.pem index c126543553d..7ceba673dd2 100644 --- a/test/fixtures/ca.pem +++ b/test/fixtures/ca.pem @@ -1,16 +1,34 @@ -----BEGIN CERTIFICATE----- -MIIChDCCAe2gAwIBAgIJAMsVOuISYJ/GMA0GCSqGSIb3DQEBCwUAMHoxCzAJBgNV -BAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNVBAoMBkpveWVu -dDEQMA4GA1UECwwHTm9kZS5qczEMMAoGA1UEAwwDY2ExMSAwHgYJKoZIhvcNAQkB -FhFyeUB0aW55Y2xvdWRzLm9yZzAgFw0xODExMTYxODQyMjBaGA8yMjkyMDgzMDE4 -NDIyMFowejELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEP -MA0GA1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQwwCgYDVQQDDANjYTEx -IDAeBgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMIGfMA0GCSqGSIb3DQEB -AQUAA4GNADCBiQKBgQDrNdKjVKhbxKbrDRLdy45u9vsU3IH8C3qFcLF5wqf+g7OC -vMOOrFDM6mL5iYwuYaLRvAtsC0mtGPzBGyFflxGhiBYaOhi7nCKEsUkFuNYlCzX+ -FflT04JYT3qWPLL7rT32GXpABND/8DEnj5D5liYYNR05PjV1fUnGg1gPqXVxbwID -AQABoxAwDjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GBAHhsWFy6m6VO -AjK14n0XCSM66ltk9qMKpOryXneLhmmkOQbJd7oavueUWzMdszWLMKhrBoXjmvuW -QceutP9IUq1Kzw7a/B+lLPD90xfLMr7tNLAxZoJmq/NAUI63M3nJGpX0HkjnYwoU -ekzNkKt5TggwcqqzK+cCSG1wDvJ+wjiD +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 +1UV2bT78VPpeGn8= -----END CERTIFICATE----- diff --git a/test/fixtures/cert.pem b/test/fixtures/cert.pem index 664d00ca6d8..903d5f57ba2 100644 --- a/test/fixtures/cert.pem +++ b/test/fixtures/cert.pem @@ -1,18 +1,33 @@ -----BEGIN CERTIFICATE----- -MIIC2DCCAkGgAwIBAgIJAOzJuFYnDamoMA0GCSqGSIb3DQEBCwUAMHoxCzAJBgNV -BAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNVBAoMBkpveWVu -dDEQMA4GA1UECwwHTm9kZS5qczEMMAoGA1UEAwwDY2ExMSAwHgYJKoZIhvcNAQkB -FhFyeUB0aW55Y2xvdWRzLm9yZzAgFw0xODExMTYxODQyMjFaGA8yMjkyMDgzMDE4 -NDIyMVowfTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEP -MA0GA1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQ8wDQYDVQQDDAZhZ2Vu -dDExIDAeBgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMIGfMA0GCSqGSIb3 -DQEBAQUAA4GNADCBiQKBgQDvVEBwFjfiirsDjlZB+CjYNMNCqdJe27hqK/b72AnL -jgN6mLcXCOABJC5N61TGFkiF9Zndh6IyFXRZVb4gQX4zxNDRuAydo95BmiYHGV0v -t1ZXsLv7XrfQu6USLRtpZMe1cNULjsAB7raN+1hEN1CPMSmSjWc7MKPgv09QYJ5j -cQIDAQABo2EwXzBdBggrBgEFBQcBAQRRME8wIwYIKwYBBQUHMAGGF2h0dHA6Ly9v -Y3NwLm5vZGVqcy5vcmcvMCgGCCsGAQUFBzAChhxodHRwOi8vY2Eubm9kZWpzLm9y -Zy9jYS5jZXJ0MA0GCSqGSIb3DQEBCwUAA4GBAHrKvx2Z4fsF7b3VRgiIbdbFCfxY -ICvoJ0+BObYPjqIZZm9+/5c36SpzKzGO9CN9qUEj3KxPmijnb+Zjsm1CSCrG1m04 -C73+AjAIPnQ+eWZnF1K4L2kuEDTpv8nQzYKYiGxsmW58PSMeAq1TmaFwtSW3TxHX -7ROnqBX0uXQlOo1m +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 -----END CERTIFICATE----- diff --git a/test/fixtures/key.pem b/test/fixtures/key.pem index fe750dee3f4..facb3118fee 100644 --- a/test/fixtures/key.pem +++ b/test/fixtures/key.pem @@ -1,15 +1,52 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXQIBAAKBgQDvVEBwFjfiirsDjlZB+CjYNMNCqdJe27hqK/b72AnLjgN6mLcX -COABJC5N61TGFkiF9Zndh6IyFXRZVb4gQX4zxNDRuAydo95BmiYHGV0vt1ZXsLv7 -XrfQu6USLRtpZMe1cNULjsAB7raN+1hEN1CPMSmSjWc7MKPgv09QYJ5jcQIDAQAB -AoGAbqk3TlyHpKFfDarf6Yr0X9wtuQJK+n+ACt+fSR3AkbVtmF9KsUTyRrTTEEZT -IXCmQgKpDYysi5nt/WyvB70gu6xGYbT6PzZaf1RmcpWd1pLcdyBOppY6y7nTMZA3 -BVFfmIPSmAvtCuzZwQFFnNoKH3d6cqna+ZQJ0zvCLCSLcw0CQQD6tswNlhCIfguh -tvhw7hJB5vZPWWEzyTQl8nVdY6SbxAT8FTx0UjxsKgOiJFzAGAVoCi40oRKIHhrw -pKwHsEqTAkEA9GABbi2xqAmhPn66e0AiU8t2uv69PISBSt2tXbUAburJFj+4rYZW -71QIbSKEYceveb7wm0NP+adgZqJlxn7oawJBAOjfK4+fCIJPWWx+8Cqs5yZxae1w -HrokNBzfJSZ2bCoGm36uFvYQgHETYUaUsdX3OeZWNm7KAdWO6QUGX4fQtqMCQGXv -OgmEY+utAKZ55D2PFgKQB1me8r6wouHgr/U7kA+0Peba86TmOZMhIVaspD3JNqf4 -/pI1NMH1kF+fdAalXzsCQQCelwr9I3FWhx336CWrfAY20xbiMOWMyAhrjVrexgUD -53Y6AhSaRC725pZTgO2PQ4AjkGLIP61sZKgTrXS85KmJ ------END RSA PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +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 +zhkPVDbGIYpzFskiOGNjvnvBhwVdVg== +-----END PRIVATE KEY----- From dc032a1050d5489b8ce9b4c22aafba98a942f87b Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Mon, 11 Aug 2025 08:07:46 +0200 Subject: [PATCH 08/11] fix: h2 CI (#4395) * test: fix key-size pem errors * chore: use @metcoder95/https-pem * fix: ci * fix: ci (cherry picked from commit 8dd120e3332de496602477affd85d20577e98cc6) --- package.json | 2 +- scripts/generate-pem.js | 2 +- test/connect-pre-shared-session.js | 2 +- test/fetch/cookies.js | 2 +- test/fetch/http2.js | 20 +++++------ test/http2.js | 56 ++++++++++++++++-------------- test/https.js | 2 +- test/interceptors/dns.js | 2 +- test/node-test/ca-fingerprint.js | 2 +- test/node-test/client-dispatch.js | 2 +- test/node-test/client-errors.js | 2 +- test/node-test/unix.js | 2 +- 12 files changed, 49 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index 291ed14ba52..a9b7bb8e944 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "devDependencies": { "@fastify/busboy": "2.1.1", "@matteo.collina/tspl": "^0.1.1", + "@metcoder95/https-pem": "^1.0.0", "@sinonjs/fake-timers": "^11.1.0", "@types/node": "~18.19.50", "abort-controller": "^3.0.0", @@ -117,7 +118,6 @@ "fast-check": "^3.17.1", "form-data": "^4.0.0", "formdata-node": "^6.0.3", - "https-pem": "^3.0.0", "husky": "^9.0.7", "jest": "^29.0.2", "jsdom": "^24.0.0", diff --git a/scripts/generate-pem.js b/scripts/generate-pem.js index 0d7e628e209..172701f183e 100644 --- a/scripts/generate-pem.js +++ b/scripts/generate-pem.js @@ -1,3 +1,3 @@ /* istanbul ignore file */ -require('https-pem/install') +require('@metcoder95/https-pem/install') diff --git a/test/connect-pre-shared-session.js b/test/connect-pre-shared-session.js index 5ee7b308885..bdad840627e 100644 --- a/test/connect-pre-shared-session.js +++ b/test/connect-pre-shared-session.js @@ -4,7 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after, mock } = require('node:test') const { Client } = require('..') const { createServer } = require('node:https') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const tls = require('node:tls') test('custom session passed to client will be used in tls connect call', async (t) => { diff --git a/test/fetch/cookies.js b/test/fetch/cookies.js index 3bc69c6837b..9c560814786 100644 --- a/test/fetch/cookies.js +++ b/test/fetch/cookies.js @@ -7,7 +7,7 @@ const assert = require('node:assert') const { tspl } = require('@matteo.collina/tspl') const { Client, fetch, Headers } = require('../..') const { closeServerAsPromise } = require('../utils/node-http') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const { createSecureServer } = require('node:http2') const { closeClientAndServerAsPromise } = require('../utils/node-http') diff --git a/test/fetch/http2.js b/test/fetch/http2.js index f64756de788..08963550362 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -8,7 +8,7 @@ const { Readable } = require('node:stream') const { test } = require('node:test') const { tspl } = require('@matteo.collina/tspl') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const { Client, fetch, Headers } = require('../..') @@ -17,7 +17,7 @@ const { closeClientAndServerAsPromise } = require('../utils/node-http') test('[Fetch] Issue#2311', async (t) => { const expectedBody = 'hello from client!' - const server = createSecureServer(pem, async (req, res) => { + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }), async (req, res) => { let body = '' req.setEncoding('utf8') @@ -69,7 +69,7 @@ test('[Fetch] Issue#2311', async (t) => { }) test('[Fetch] Simple GET with h2', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedRequestBody = 'hello h2!' server.on('stream', async (stream, headers) => { @@ -125,7 +125,7 @@ test('[Fetch] Simple GET with h2', async (t) => { }) test('[Fetch] Should handle h2 request with body (string or buffer)', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'hello from client!' const expectedRequestBody = 'hello h2!' const requestBody = [] @@ -180,7 +180,7 @@ test('[Fetch] Should handle h2 request with body (string or buffer)', async (t) test( '[Fetch] Should handle h2 request with body (stream)', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = readFileSync(__filename, 'utf-8') const stream = createReadStream(__filename) const requestChunks = [] @@ -242,7 +242,7 @@ test( } ) test('Should handle h2 request with body (Blob)', { skip: !Blob }, async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'asd' const requestChunks = [] const body = new Blob(['asd'], { @@ -306,7 +306,7 @@ test( 'Should handle h2 request with body (Blob:ArrayBuffer)', { skip: !Blob }, async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'hello' const requestChunks = [] const expectedResponseBody = { hello: 'h2' } @@ -371,7 +371,7 @@ test( test('Issue#2415', async (t) => { const { doesNotThrow } = tspl(t, { plan: 1 }) - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', async (stream, headers) => { stream.respond({ @@ -407,7 +407,7 @@ test('Issue#2415', async (t) => { }) test('Issue #2386', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const body = Buffer.from('hello') const requestChunks = [] const expectedResponseBody = { hello: 'h2' } @@ -464,7 +464,7 @@ test('Issue #2386', async (t) => { }) test('Issue #3046', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const { strictEqual, deepStrictEqual } = tspl(t, { plan: 6 }) diff --git a/test/http2.js b/test/http2.js index 7d130e670a9..74c3ef374f2 100644 --- a/test/http2.js +++ b/test/http2.js @@ -8,7 +8,7 @@ const { once } = require('node:events') const { Blob } = require('node:buffer') const { Writable, pipeline, PassThrough, Readable } = require('node:stream') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const { Client, Agent, FormData } = require('..') @@ -16,7 +16,7 @@ const isGreaterThanv20 = process.versions.node.split('.').map(Number)[0] >= 20 test('Should support H2 connection', async t => { const body = [] - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', (stream, headers, _flags, rawHeaders) => { t.strictEqual(headers['x-my-header'], 'foo') @@ -63,7 +63,7 @@ test('Should support H2 connection', async t => { }) test('Should support H2 connection(multiple requests)', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', async (stream, headers, _flags, rawHeaders) => { t.strictEqual(headers['x-my-header'], 'foo') @@ -122,7 +122,7 @@ test('Should support H2 connection(multiple requests)', async t => { test('Should support H2 connection (headers as array)', async t => { const body = [] - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', (stream, headers) => { t.strictEqual(headers['x-my-header'], 'foo') @@ -168,7 +168,7 @@ test('Should support H2 connection (headers as array)', async t => { }) test('Should support H2 connection(POST Buffer)', async t => { - const server = createSecureServer({ ...pem, allowHTTP1: false }) + const server = createSecureServer({ ...await pem.generate({ opts: { keySize: 2048 } }), allowHTTP1: false }) server.on('stream', async (stream, headers, _flags, rawHeaders) => { t.strictEqual(headers[':method'], 'POST') @@ -273,7 +273,7 @@ test( const server = createSecureServer( { - ...pem, + ...await pem.generate({ opts: { keySize: 2048 } }), allowHTTP1: false, ALPNProtocols: ['http/1.1'] }, @@ -313,7 +313,7 @@ test( async t => { const server = createSecureServer( { - ...pem, + ...await pem.generate({ opts: { keySize: 2048 } }), allowHTTP1: false, ALPNProtocols: ['http/1.1'] }, @@ -348,7 +348,7 @@ test( test('Should handle h2 continue', async t => { const requestBody = [] - const server = createSecureServer(pem, () => {}) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }), () => {}) const responseBody = [] server.on('checkContinue', (request, response) => { @@ -404,7 +404,7 @@ test('Should handle h2 continue', async t => { }) test('Dispatcher#Stream', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'hello from client!' const bufs = [] let requestBody = '' @@ -455,7 +455,7 @@ test('Dispatcher#Stream', async t => { }) test('Dispatcher#Pipeline', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'hello from client!' const bufs = [] let requestBody = '' @@ -517,7 +517,7 @@ test('Dispatcher#Pipeline', async t => { }) test('Dispatcher#Connect', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'hello from client!' let requestBody = '' @@ -574,7 +574,7 @@ test('Dispatcher#Connect', async t => { }) test('Dispatcher#Upgrade', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', async (stream, headers) => { stream.end() @@ -605,7 +605,7 @@ test('Dispatcher#Upgrade', async t => { test('Dispatcher#destroy', async t => { const promises = [] - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', (stream, headers) => { setTimeout(stream.end.bind(stream), 1500) @@ -675,7 +675,7 @@ test('Dispatcher#destroy', async t => { }) test('Should handle h2 request without body', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = '' const requestChunks = [] const responseBody = [] @@ -734,7 +734,7 @@ test('Should handle h2 request without body', async t => { }) test('Should handle h2 request with body (string or buffer) - dispatch', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'hello from client!' const response = [] const requestBody = [] @@ -812,7 +812,7 @@ test('Should handle h2 request with body (string or buffer) - dispatch', async t }) test('Should handle h2 request with body (stream)', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = readFileSync(__filename, 'utf-8') const stream = createReadStream(__filename) const requestChunks = [] @@ -872,7 +872,7 @@ test('Should handle h2 request with body (stream)', async t => { }) test('Should handle h2 request with body (iterable)', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'hello' const requestChunks = [] const responseBody = [] @@ -941,7 +941,7 @@ test('Should handle h2 request with body (iterable)', async t => { }) test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'asd' const requestChunks = [] const responseBody = [] @@ -1006,7 +1006,7 @@ test( 'Should handle h2 request with body (Blob:ArrayBuffer)', { skip: !Blob }, async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) const expectedBody = 'hello' const requestChunks = [] const responseBody = [] @@ -1071,7 +1071,7 @@ test( test('Agent should support H2 connection', async t => { const body = [] - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', (stream, headers) => { t.strictEqual(headers['x-my-header'], 'foo') @@ -1121,7 +1121,7 @@ test('Agent should support H2 connection', async t => { test('Should provide pseudo-headers in proper order', async t => { t = tspl(t, { plan: 2 }) - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', (stream, _headers, _flags, rawHeaders) => { t.deepStrictEqual(rawHeaders, [ ':authority', @@ -1167,7 +1167,7 @@ test('Should provide pseudo-headers in proper order', async t => { }) test('The h2 pseudo-headers is not included in the headers', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', (stream, headers) => { stream.respond({ @@ -1202,7 +1202,7 @@ test('The h2 pseudo-headers is not included in the headers', async t => { }) test('Should throw informational error on half-closed streams (remote)', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', (stream, headers) => { stream.destroy() @@ -1236,7 +1236,7 @@ test('Should throw informational error on half-closed streams (remote)', async t }) test('#2364 - Concurrent aborts', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', (stream, headers, _flags, rawHeaders) => { t.strictEqual(headers['x-my-header'], 'foo') @@ -1336,7 +1336,7 @@ test('#2364 - Concurrent aborts', async t => { }) test('#3046 - GOAWAY Frame', async t => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', (stream, headers) => { setTimeout(() => { @@ -1398,7 +1398,7 @@ test('#3046 - GOAWAY Frame', async t => { }) test('#3671 - Graceful close', async (t) => { - const server = createSecureServer(pem) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', (stream, headers) => { setTimeout(() => { @@ -1443,10 +1443,11 @@ test('#3671 - Graceful close', async (t) => { await t.completed }) + test('#3803 - sending FormData bodies works', async (t) => { const assert = tspl(t, { plan: 4 }) - const server = createSecureServer(pem).listen(0) + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })).listen(0) server.on('stream', async (stream, headers) => { const contentLength = Number(headers['content-length']) @@ -1493,3 +1494,4 @@ test('#3803 - sending FormData bodies works', async (t) => { await assert.completed }) + diff --git a/test/https.js b/test/https.js index 418fb969f45..e7bc9098c64 100644 --- a/test/https.js +++ b/test/https.js @@ -4,7 +4,7 @@ const { tspl } = require('@matteo.collina/tspl') const { test, after } = require('node:test') const { Client } = require('..') const { createServer } = require('node:https') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') test('https get with tls opts', async (t) => { t = tspl(t, { plan: 6 }) diff --git a/test/interceptors/dns.js b/test/interceptors/dns.js index 6b4b30b13cc..6edcab99c77 100644 --- a/test/interceptors/dns.js +++ b/test/interceptors/dns.js @@ -9,7 +9,7 @@ const { once } = require('node:events') const { setTimeout: sleep } = require('node:timers/promises') const { tspl } = require('@matteo.collina/tspl') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const { interceptors, Agent } = require('../..') const { dns } = interceptors diff --git a/test/node-test/ca-fingerprint.js b/test/node-test/ca-fingerprint.js index bad8aeff555..3fef353b874 100644 --- a/test/node-test/ca-fingerprint.js +++ b/test/node-test/ca-fingerprint.js @@ -4,7 +4,7 @@ const crypto = require('node:crypto') const https = require('node:https') const { test } = require('node:test') const { Client, buildConnector } = require('../..') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const { tspl } = require('@matteo.collina/tspl') const caFingerprint = getFingerprint(pem.cert.toString() diff --git a/test/node-test/client-dispatch.js b/test/node-test/client-dispatch.js index 296e3b8d075..b7059b5c4d4 100644 --- a/test/node-test/client-dispatch.js +++ b/test/node-test/client-dispatch.js @@ -7,7 +7,7 @@ const https = require('node:https') const { Client, Pool, errors } = require('../..') const stream = require('node:stream') const { createSecureServer } = require('node:http2') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const { tspl } = require('@matteo.collina/tspl') const { closeServerAsPromise, closeClientAndServerAsPromise } = require('../utils/node-http') diff --git a/test/node-test/client-errors.js b/test/node-test/client-errors.js index 3ac832b1635..0a0456c05d8 100644 --- a/test/node-test/client-errors.js +++ b/test/node-test/client-errors.js @@ -5,7 +5,7 @@ const assert = require('node:assert') const { Client, Pool, errors } = require('../..') const { createServer } = require('node:http') const https = require('node:https') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const { Readable } = require('node:stream') const { tspl } = require('@matteo.collina/tspl') diff --git a/test/node-test/unix.js b/test/node-test/unix.js index cdeda6d1b3d..22b6ba9284d 100644 --- a/test/node-test/unix.js +++ b/test/node-test/unix.js @@ -4,7 +4,7 @@ const { test } = require('node:test') const { Client, Pool } = require('../../') const http = require('node:http') const https = require('node:https') -const pem = require('https-pem') +const pem = require('@metcoder95/https-pem') const fs = require('node:fs') const { tspl } = require('@matteo.collina/tspl') From a444e4f13e8958b4e1ac42bc0d53ace7fba0a9c1 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 12 Mar 2026 15:59:16 +0100 Subject: [PATCH 09/11] test: stabilize h2 and tls-cert-leak under current test runner Signed-off-by: Matteo Collina --- test/http2.js | 5 +++-- test/tls-cert-leak.js | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/http2.js b/test/http2.js index 74c3ef374f2..2a24fdc1a2f 100644 --- a/test/http2.js +++ b/test/http2.js @@ -1389,7 +1389,7 @@ test('#3046 - GOAWAY Frame', async t => { t.strictEqual(response.headers['x-custom-h2'], 'hello') t.strictEqual(response.statusCode, 200) - t.rejects(response.body.text(), { + await t.rejects(response.body.text(), { message: 'HTTP/2: "GOAWAY" frame received with code 0', code: 'UND_ERR_SOCKET' }) @@ -1486,12 +1486,13 @@ test('#3803 - sending FormData bodies works', async (t) => { fd.set('a', 'b') fd.set('c', new Blob(['d']), 'e.fgh') - await client.request({ + const { body } = await client.request({ path: '/', method: 'POST', body: fd }) + await body.dump() await assert.completed }) diff --git a/test/tls-cert-leak.js b/test/tls-cert-leak.js index 2a579e7a0a6..bd243619684 100644 --- a/test/tls-cert-leak.js +++ b/test/tls-cert-leak.js @@ -15,7 +15,8 @@ const hasGC = typeof global.gc !== 'undefined' // It simulates the error by using a server with a self-signed certificate. test('no memory leak with TLS certificate errors', { timeout: 20000 }, async (t) => { if (!hasGC) { - throw new Error('gc is not available. Run with \'--expose-gc\'.') + t.skip('gc is not available') + return } const { ok } = tspl(t, { plan: 1 }) From 844bf59699d778944f78a24ae819c0e8f295766e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 12 Mar 2026 16:23:59 +0100 Subject: [PATCH 10/11] test: fix http2 lint regressions in backport Signed-off-by: Matteo Collina --- test/http2.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/http2.js b/test/http2.js index 2a24fdc1a2f..d87659d5329 100644 --- a/test/http2.js +++ b/test/http2.js @@ -1443,7 +1443,6 @@ test('#3671 - Graceful close', async (t) => { await t.completed }) - test('#3803 - sending FormData bodies works', async (t) => { const assert = tspl(t, { plan: 4 }) @@ -1495,4 +1494,3 @@ test('#3803 - sending FormData bodies works', async (t) => { await body.dump() await assert.completed }) - From 411bd01a42e7917009bbf686f7628b99d67bbce9 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 12 Mar 2026 16:54:27 +0100 Subject: [PATCH 11/11] test(websocket): use node:assert for Node 18 compatibility Signed-off-by: Matteo Collina --- test/websocket/permessage-deflate-limit.js | 17 +++---- .../permessage-deflate-windowbits.js | 45 ++++++++++--------- test/websocket/receiver-unit.js | 7 +-- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/test/websocket/permessage-deflate-limit.js b/test/websocket/permessage-deflate-limit.js index d2a7b2193c6..dbf14227103 100644 --- a/test/websocket/permessage-deflate-limit.js +++ b/test/websocket/permessage-deflate-limit.js @@ -1,6 +1,7 @@ 'use strict' const { test } = require('node:test') +const assert = require('node:assert') const { once } = require('node:events') const http = require('node:http') const crypto = require('node:crypto') @@ -75,7 +76,7 @@ test('Compressed message under limit decompresses successfully', async (t) => { const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) const [event] = await once(client, 'message') - t.assert.strictEqual(event.data.size, 1024) + assert.strictEqual(event.data.size, 1024) client.close() }) @@ -109,7 +110,7 @@ test('Custom maxDecompressedMessageSize is enforced', async (t) => { await once(client, 'close') // The message should NOT have been received due to size limit - t.assert.strictEqual(messageReceived, false) + assert.strictEqual(messageReceived, false) }) test('Message exactly at limit succeeds', async (t) => { @@ -132,7 +133,7 @@ test('Message exactly at limit succeeds', async (t) => { }) const [event] = await once(client, 'message') - t.assert.strictEqual(event.data.size, limit) + assert.strictEqual(event.data.size, limit) client.close() }) @@ -156,7 +157,7 @@ test('Message one byte over limit fails', async (t) => { }) const [event] = await once(client, 'error') - t.assert.ok(event.error instanceof Error) + assert.ok(event.error instanceof Error) }) test('Connection closes when limit exceeded', async (t) => { @@ -179,7 +180,7 @@ test('Connection closes when limit exceeded', async (t) => { const [event] = await once(client, 'close') // Connection should be closed - code 1006 (abnormal) or 1009 (too big) - t.assert.ok(event.code === 1006 || event.code === 1009) + assert.ok(event.code === 1006 || event.code === 1009) }) test('Non-compressed messages are not affected by decompression limit', async (t) => { @@ -202,7 +203,7 @@ test('Non-compressed messages are not affected by decompression limit', async (t // Should succeed because compression is not used const [event] = await once(client, 'message') - t.assert.strictEqual(event.data.size, 2 * 1024 * 1024) + assert.strictEqual(event.data.size, 2 * 1024 * 1024) client.close() }) @@ -252,7 +253,7 @@ test('Decompression bomb is mitigated via raw WebSocket handshake', async (t) => await once(client, 'close') // The message should NOT have been received due to size limit - t.assert.strictEqual(messageReceived, false) + assert.strictEqual(messageReceived, false) }) test('Higher custom limit allows larger messages', async (t) => { @@ -277,6 +278,6 @@ test('Higher custom limit allows larger messages', async (t) => { }) const [event] = await once(client, 'message') - t.assert.strictEqual(event.data.size, dataSize) + assert.strictEqual(event.data.size, dataSize) client.close() }) diff --git a/test/websocket/permessage-deflate-windowbits.js b/test/websocket/permessage-deflate-windowbits.js index 762125e3b9a..be30510d62e 100644 --- a/test/websocket/permessage-deflate-windowbits.js +++ b/test/websocket/permessage-deflate-windowbits.js @@ -1,6 +1,7 @@ 'use strict' const { describe, test, after } = require('node:test') +const assert = require('node:assert') const { once } = require('node:events') const http = require('node:http') const crypto = require('node:crypto') @@ -91,33 +92,33 @@ function makeWsFrame ({ opcode, rsv1, payload }) { describe('isValidClientWindowBits', () => { test('rejects empty string', (t) => { - t.assert.strictEqual(isValidClientWindowBits(''), false) + assert.strictEqual(isValidClientWindowBits(''), false) }) test('rejects values below 8', (t) => { - t.assert.strictEqual(isValidClientWindowBits('0'), false) - t.assert.strictEqual(isValidClientWindowBits('1'), false) - t.assert.strictEqual(isValidClientWindowBits('7'), false) + assert.strictEqual(isValidClientWindowBits('0'), false) + assert.strictEqual(isValidClientWindowBits('1'), false) + assert.strictEqual(isValidClientWindowBits('7'), false) }) test('accepts values 8-15', (t) => { for (let i = 8; i <= 15; i++) { - t.assert.strictEqual(isValidClientWindowBits(String(i)), true, `${i} should be valid`) + assert.strictEqual(isValidClientWindowBits(String(i)), true, `${i} should be valid`) } }) test('rejects values above 15', (t) => { - t.assert.strictEqual(isValidClientWindowBits('16'), false) - t.assert.strictEqual(isValidClientWindowBits('100'), false) - t.assert.strictEqual(isValidClientWindowBits('1000'), false) - t.assert.strictEqual(isValidClientWindowBits('999999'), false) + assert.strictEqual(isValidClientWindowBits('16'), false) + assert.strictEqual(isValidClientWindowBits('100'), false) + assert.strictEqual(isValidClientWindowBits('1000'), false) + assert.strictEqual(isValidClientWindowBits('999999'), false) }) test('rejects non-numeric values', (t) => { - t.assert.strictEqual(isValidClientWindowBits('abc'), false) - t.assert.strictEqual(isValidClientWindowBits('12a'), false) - t.assert.strictEqual(isValidClientWindowBits('-1'), false) - t.assert.strictEqual(isValidClientWindowBits('8.5'), false) + assert.strictEqual(isValidClientWindowBits('abc'), false) + assert.strictEqual(isValidClientWindowBits('12a'), false) + assert.strictEqual(isValidClientWindowBits('-1'), false) + assert.strictEqual(isValidClientWindowBits('8.5'), false) }) }) @@ -130,7 +131,7 @@ describe('permessage-deflate server_max_window_bits', () => { const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) const [event] = await once(client, 'message') - t.assert.ok(event.data) + assert.ok(event.data) client.close() }) @@ -142,7 +143,7 @@ describe('permessage-deflate server_max_window_bits', () => { const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) const [event] = await once(client, 'message') - t.assert.ok(event.data) + assert.ok(event.data) client.close() }) @@ -154,7 +155,7 @@ describe('permessage-deflate server_max_window_bits', () => { const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) const [event] = await once(client, 'error') - t.assert.ok(event.error instanceof Error) + assert.ok(event.error instanceof Error) }) test('server_max_window_bits=7 is rejected gracefully', async (t) => { @@ -165,7 +166,7 @@ describe('permessage-deflate server_max_window_bits', () => { const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) const [event] = await once(client, 'error') - t.assert.ok(event.error instanceof Error) + assert.ok(event.error instanceof Error) }) test('server_max_window_bits=16 is rejected gracefully', async (t) => { @@ -176,7 +177,7 @@ describe('permessage-deflate server_max_window_bits', () => { const client = new WebSocket(`ws://127.0.0.1:${server.address().port}`) const [event] = await once(client, 'error') - t.assert.ok(event.error instanceof Error) + assert.ok(event.error instanceof Error) }) test('server_max_window_bits=1000 is rejected gracefully (PoC attack)', async (t) => { @@ -188,7 +189,7 @@ describe('permessage-deflate server_max_window_bits', () => { const [event] = await once(client, 'error') // The key assertion: we got an error event instead of crashing - t.assert.ok(event.error instanceof Error, 'Should receive an Error') + assert.ok(event.error instanceof Error, 'Should receive an Error') }) test('no uncaught exception with invalid windowBits', async (t) => { @@ -214,7 +215,7 @@ describe('permessage-deflate server_max_window_bits', () => { new Promise(resolve => setTimeout(resolve, 1000)) ]) - t.assert.strictEqual(uncaughtException, false, 'No uncaught exception should occur') + assert.strictEqual(uncaughtException, false, 'No uncaught exception should occur') }) test('invalid windowBits closes connection without crashing process', async (t) => { @@ -231,12 +232,12 @@ describe('permessage-deflate server_max_window_bits', () => { ]) // Either error or close is acceptable, as long as we didn't crash - t.assert.ok( + assert.ok( result.type === 'error' || result.type === 'close', 'Connection should close gracefully' ) // This assertion is reached = process did not crash - t.assert.ok(true, 'Process did not crash') + assert.ok(true, 'Process did not crash') }) }) diff --git a/test/websocket/receiver-unit.js b/test/websocket/receiver-unit.js index 64b16846432..f82523e6b54 100644 --- a/test/websocket/receiver-unit.js +++ b/test/websocket/receiver-unit.js @@ -1,12 +1,13 @@ 'use strict' const { test } = require('node:test') +const assert = require('node:assert') const { ByteParser } = require('../../lib/web/websocket/receiver') const { kController, kResponse } = require('../../lib/web/websocket/symbols') const invalidFrame = Buffer.from([0x82, 0x7F, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]) -test('ByteParser rejects 64-bit payload lengths with a non-zero upper word', (t) => { +test('ByteParser rejects 64-bit payload lengths with a non-zero upper word', () => { const calls = { abort: 0, destroy: 0 @@ -33,8 +34,8 @@ test('ByteParser rejects 64-bit payload lengths with a non-zero upper word', (t) return new Promise((resolve) => { setImmediate(() => { - t.assert.strictEqual(calls.abort, 1) - t.assert.strictEqual(calls.destroy, 1) + assert.strictEqual(calls.abort, 1) + assert.strictEqual(calls.destroy, 1) parser.destroy() resolve() })