Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions docs/docs/api/WebSocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ When passing an object as the second argument, the following options are availab
* **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:

Expand All @@ -43,26 +42,6 @@ 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'

// Limit decompressed messages to 1 MB
const ws = new WebSocket('wss://echo.websocket.events', {
maxDecompressedMessageSize: 1 * 1024 * 1024
})

ws.addEventListener('error', (event) => {
// Connection will be closed if a message exceeds the limit
console.error('WebSocket error:', event.error)
})
```

> ⚠️ **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.

### Example with HTTP/2:

> ⚠️ Warning: WebSocket over HTTP/2 is experimental, it is likely to change in the future.
Expand Down
9 changes: 2 additions & 7 deletions lib/web/websocket/permessage-deflate.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ class PerMessageDeflate {

#options = {}

/** @type {number} */
#maxDecompressedSize

/** @type {boolean} */
#aborted = false

Expand All @@ -28,12 +25,10 @@ class PerMessageDeflate {

/**
* @param {Map<string, string>} extensions
* @param {{ maxDecompressedMessageSize?: number }} [options]
*/
constructor (extensions, options = {}) {
constructor (extensions) {
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) {
Expand Down Expand Up @@ -75,7 +70,7 @@ class PerMessageDeflate {

this.#inflate[kLength] += data.length

if (this.#inflate[kLength] > this.#maxDecompressedSize) {
if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) {
this.#aborted = true
this.#inflate.removeAllListeners()
this.#inflate.destroy()
Expand Down
9 changes: 2 additions & 7 deletions lib/web/websocket/receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,18 @@ class ByteParser extends Writable {
/** @type {import('./websocket').Handler} */
#handler

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

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

this.#handler = handler
this.#extensions = extensions == null ? new Map() : extensions
this.#options = options

if (this.#extensions.has('permessage-deflate')) {
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options))
this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
}
}

Expand Down
22 changes: 1 addition & 21 deletions lib/web/websocket/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,6 @@ class WebSocket extends EventTarget {
#binaryType
/** @type {import('./receiver').ByteParser} */
#parser
/** @type {{ maxDecompressedMessageSize?: number }} */
#options

/**
* @param {string} url
Expand Down Expand Up @@ -156,11 +154,6 @@ class WebSocket extends EventTarget {
// 5. Set this's url to urlRecord.
this.#url = new URL(urlRecord.href)

// Store options for later use (e.g., maxDecompressedMessageSize)
this.#options = {
maxDecompressedMessageSize: options.maxDecompressedMessageSize
}

// 6. Let client be this's relevant settings object.
const client = environmentSettingsObject.settingsObject

Expand Down Expand Up @@ -463,7 +456,7 @@ class WebSocket extends EventTarget {
// once this happens, the connection is open
this.#handler.socket = response.socket

const parser = new ByteParser(this.#handler, parsedExtensions, this.#options)
const parser = new ByteParser(this.#handler, parsedExtensions)
parser.on('drain', () => this.#handler.onParserDrain())
parser.on('error', (err) => this.#handler.onParserError(err))

Expand Down Expand Up @@ -715,19 +708,6 @@ 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
})
}
])

Expand Down
Loading
Loading