Socket
Book a DemoSign in
Socket

undici

Package Overview
Dependencies
Maintainers
3
Versions
272
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

undici - npm Package Compare versions

Comparing version
6.23.0
to
6.24.0
+1
-0
docs/docs/api/Errors.md

@@ -30,2 +30,3 @@ # Errors

| `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 |

@@ -32,0 +33,0 @@ ### `SocketError`

@@ -16,2 +16,11 @@ # Class: WebSocket

### 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:

@@ -40,2 +49,16 @@

### 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

@@ -42,0 +65,0 @@

@@ -382,2 +382,20 @@ 'use strict'

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 = {

@@ -406,3 +424,4 @@ AbortError,

ResponseError,
SecureProxyConnectionError
SecureProxyConnectionError,
MessageSizeExceededError
}

@@ -69,2 +69,6 @@ 'use strict'

if (upgrade && !isValidHeaderValue(upgrade)) {
throw new InvalidArgumentError('invalid upgrade header')
}
if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {

@@ -364,3 +368,6 @@ throw new InvalidArgumentError('invalid headersTimeout')

if (request.host === null && headerName === 'host') {
if (headerName === 'host') {
if (request.host !== null) {
throw new InvalidArgumentError('duplicate host header')
}
if (typeof val !== 'string') {

@@ -371,3 +378,6 @@ throw new InvalidArgumentError('invalid host header')

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)

@@ -374,0 +384,0 @@ if (!Number.isFinite(request.contentLength)) {

@@ -5,2 +5,3 @@ 'use strict'

const { isValidClientWindowBits } = require('./util')
const { MessageSizeExceededError } = require('../../core/errors')

@@ -11,2 +12,5 @@ const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])

// Default maximum decompressed message size: 4 MB
const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
class PerMessageDeflate {

@@ -18,5 +22,19 @@ /** @type {import('node:zlib').InflateRaw} */

constructor (extensions) {
/** @type {number} */
#maxDecompressedSize
/** @type {boolean} */
#aborted = false
/** @type {Function|null} */
#currentCallback = null
/**
* @param {Map<string, string>} 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
}

@@ -30,2 +48,7 @@

if (this.#aborted) {
callback(new MessageSizeExceededError())
return
}
if (!this.#inflate) {

@@ -43,3 +66,8 @@ let windowBits = Z_DEFAULT_WINDOWBITS

this.#inflate = createInflateRaw({ windowBits })
try {
this.#inflate = createInflateRaw({ windowBits })
} catch (err) {
callback(err)
return
}
this.#inflate[kBuffer] = []

@@ -49,4 +77,23 @@ this.#inflate[kLength] = 0

this.#inflate.on('data', (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[kLength] += data.length
})

@@ -60,2 +107,3 @@

this.#currentCallback = callback
this.#inflate.write(chunk)

@@ -67,2 +115,6 @@ if (fin) {

this.#inflate.flush(() => {
if (this.#aborted || !this.#inflate) {
return
}
const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])

@@ -72,2 +124,3 @@

this.#inflate[kLength] = 0
this.#currentCallback = null

@@ -74,0 +127,0 @@ callback(null, full)

+15
-7

@@ -40,3 +40,11 @@ 'use strict'

constructor (ws, extensions) {
/** @type {{ maxDecompressedMessageSize?: number }} */
#options
/**
* @param {import('./websocket').WebSocket} ws
* @param {Map<string, string>|null} extensions
* @param {{ maxDecompressedMessageSize?: number }} [options]
*/
constructor (ws, extensions, options = {}) {
super()

@@ -46,5 +54,6 @@

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))
}

@@ -184,2 +193,3 @@ }

const upper = buffer.readUInt32BE(0)
const lower = buffer.readUInt32BE(4)

@@ -192,3 +202,3 @@ // 2^31 is the maximum bytes an arraybuffer can contain

// 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.')

@@ -198,5 +208,3 @@ return

const lower = buffer.readUInt32BE(4)
this.#info.payloadLength = (upper << 8) + lower
this.#info.payloadLength = lower
this.#state = parserStates.READ_DATA

@@ -231,3 +239,3 @@ } else if (this.#state === parserStates.READ_DATA) {

if (error) {
closeWebSocketConnection(this.ws, 1007, error.message, error.message.length)
failWebsocketConnection(this.ws, error.message)
return

@@ -234,0 +242,0 @@ }

@@ -269,2 +269,8 @@ 'use strict'

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++) {

@@ -278,3 +284,5 @@ const byte = value.charCodeAt(i)

return true
// Check numeric range: zlib requires windowBits in range 8-15
const num = Number.parseInt(value, 10)
return num >= 8 && num <= 15
}

@@ -281,0 +289,0 @@

@@ -47,2 +47,5 @@ 'use strict'

/** @type {{ maxDecompressedMessageSize?: number }} */
#options
/**

@@ -121,2 +124,7 @@ * @param {string} url

// Store options for later use (e.g., maxDecompressedMessageSize)
this.#options = {
maxDecompressedMessageSize: options.maxDecompressedMessageSize
}
// 11. Let client be this's relevant settings object.

@@ -436,7 +444,7 @@ const client = environmentSettingsObject.settingsObject

#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)

@@ -544,2 +552,15 @@ parser.on('error', onParserError.bind(this))

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
})
}

@@ -546,0 +567,0 @@ ])

{
"name": "undici",
"version": "6.23.0",
"version": "6.24.0",
"description": "An HTTP/1.1 client, written from scratch for Node.js",

@@ -110,2 +110,3 @@ "homepage": "https://undici.nodejs.org",

"@matteo.collina/tspl": "^0.1.1",
"@metcoder95/https-pem": "^1.0.0",
"@sinonjs/fake-timers": "^11.1.0",

@@ -121,3 +122,2 @@ "@types/node": "~18.19.50",

"formdata-node": "^6.0.3",
"https-pem": "^3.0.0",
"husky": "^9.0.7",

@@ -124,0 +124,0 @@ "jest": "^29.0.2",

@@ -149,2 +149,8 @@ import { IncomingHttpHeaders } from "./header";

}
/** WebSocket decompressed message exceeded maximum size. */
export class MessageSizeExceededError extends UndiciError {
name: 'MessageSizeExceededError'
code: 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
}
}

@@ -149,3 +149,10 @@ /// <reference types="node" />

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
}