@@ -34943,6 +34943,24 @@ class SecureProxyConnectionError extends UndiciError {
3494334943 [kSecureProxyConnectionError] = true
3494434944}
3494534945
34946+ const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED')
34947+ class MessageSizeExceededError extends UndiciError {
34948+ constructor (message) {
34949+ super(message)
34950+ this.name = 'MessageSizeExceededError'
34951+ this.message = message || 'Max decompressed message size exceeded'
34952+ this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
34953+ }
34954+
34955+ static [Symbol.hasInstance] (instance) {
34956+ return instance && instance[kMessageSizeExceededError] === true
34957+ }
34958+
34959+ get [kMessageSizeExceededError] () {
34960+ return true
34961+ }
34962+ }
34963+
3494634964module.exports = {
3494734965 AbortError,
3494834966 HTTPParserError,
@@ -34966,7 +34984,8 @@ module.exports = {
3496634984 ResponseExceededMaxSizeError,
3496734985 RequestRetryError,
3496834986 ResponseError,
34969- SecureProxyConnectionError
34987+ SecureProxyConnectionError,
34988+ MessageSizeExceededError
3497034989}
3497134990
3497234991
@@ -35044,6 +35063,10 @@ class Request {
3504435063 throw new InvalidArgumentError('upgrade must be a string')
3504535064 }
3504635065
35066+ if (upgrade && !isValidHeaderValue(upgrade)) {
35067+ throw new InvalidArgumentError('invalid upgrade header')
35068+ }
35069+
3504735070 if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
3504835071 throw new InvalidArgumentError('invalid headersTimeout')
3504935072 }
@@ -35338,13 +35361,19 @@ function processHeader (request, key, val) {
3533835361 val = `${val}`
3533935362 }
3534035363
35341- if (request.host === null && headerName === 'host') {
35364+ if (headerName === 'host') {
35365+ if (request.host !== null) {
35366+ throw new InvalidArgumentError('duplicate host header')
35367+ }
3534235368 if (typeof val !== 'string') {
3534335369 throw new InvalidArgumentError('invalid host header')
3534435370 }
3534535371 // Consumed by Client
3534635372 request.host = val
35347- } else if (request.contentLength === null && headerName === 'content-length') {
35373+ } else if (headerName === 'content-length') {
35374+ if (request.contentLength !== null) {
35375+ throw new InvalidArgumentError('duplicate content-length header')
35376+ }
3534835377 request.contentLength = parseInt(val, 10)
3534935378 if (!Number.isFinite(request.contentLength)) {
3535035379 throw new InvalidArgumentError('invalid content-length header')
@@ -58135,20 +58164,38 @@ module.exports = {
5813558164
5813658165const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = __nccwpck_require__(8522)
5813758166const { isValidClientWindowBits } = __nccwpck_require__(8625)
58167+ const { MessageSizeExceededError } = __nccwpck_require__(8707)
5813858168
5813958169const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
5814058170const kBuffer = Symbol('kBuffer')
5814158171const kLength = Symbol('kLength')
5814258172
58173+ // Default maximum decompressed message size: 4 MB
58174+ const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
58175+
5814358176class PerMessageDeflate {
5814458177 /** @type {import('node:zlib').InflateRaw} */
5814558178 #inflate
5814658179
5814758180 #options = {}
5814858181
58149- constructor (extensions) {
58182+ /** @type {number} */
58183+ #maxDecompressedSize
58184+
58185+ /** @type {boolean} */
58186+ #aborted = false
58187+
58188+ /** @type {Function|null} */
58189+ #currentCallback = null
58190+
58191+ /**
58192+ * @param {Map<string, string>} extensions
58193+ * @param {{ maxDecompressedMessageSize?: number }} [options]
58194+ */
58195+ constructor (extensions, options = {}) {
5815058196 this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
5815158197 this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
58198+ this.#maxDecompressedSize = options.maxDecompressedMessageSize ?? kDefaultMaxDecompressedSize
5815258199 }
5815358200
5815458201 decompress (chunk, fin, callback) {
@@ -58157,6 +58204,11 @@ class PerMessageDeflate {
5815758204 // payload of the message.
5815858205 // 2. Decompress the resulting data using DEFLATE.
5815958206
58207+ if (this.#aborted) {
58208+ callback(new MessageSizeExceededError())
58209+ return
58210+ }
58211+
5816058212 if (!this.#inflate) {
5816158213 let windowBits = Z_DEFAULT_WINDOWBITS
5816258214
@@ -58169,13 +58221,37 @@ class PerMessageDeflate {
5816958221 windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
5817058222 }
5817158223
58172- this.#inflate = createInflateRaw({ windowBits })
58224+ try {
58225+ this.#inflate = createInflateRaw({ windowBits })
58226+ } catch (err) {
58227+ callback(err)
58228+ return
58229+ }
5817358230 this.#inflate[kBuffer] = []
5817458231 this.#inflate[kLength] = 0
5817558232
5817658233 this.#inflate.on('data', (data) => {
58177- this.#inflate[kBuffer].push(data)
58234+ if (this.#aborted) {
58235+ return
58236+ }
58237+
5817858238 this.#inflate[kLength] += data.length
58239+
58240+ if (this.#inflate[kLength] > this.#maxDecompressedSize) {
58241+ this.#aborted = true
58242+ this.#inflate.removeAllListeners()
58243+ this.#inflate.destroy()
58244+ this.#inflate = null
58245+
58246+ if (this.#currentCallback) {
58247+ const cb = this.#currentCallback
58248+ this.#currentCallback = null
58249+ cb(new MessageSizeExceededError())
58250+ }
58251+ return
58252+ }
58253+
58254+ this.#inflate[kBuffer].push(data)
5817958255 })
5818058256
5818158257 this.#inflate.on('error', (err) => {
@@ -58184,16 +58260,22 @@ class PerMessageDeflate {
5818458260 })
5818558261 }
5818658262
58263+ this.#currentCallback = callback
5818758264 this.#inflate.write(chunk)
5818858265 if (fin) {
5818958266 this.#inflate.write(tail)
5819058267 }
5819158268
5819258269 this.#inflate.flush(() => {
58270+ if (this.#aborted || !this.#inflate) {
58271+ return
58272+ }
58273+
5819358274 const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
5819458275
5819558276 this.#inflate[kBuffer].length = 0
5819658277 this.#inflate[kLength] = 0
58278+ this.#currentCallback = null
5819758279
5819858280 callback(null, full)
5819958281 })
@@ -58248,14 +58330,23 @@ class ByteParser extends Writable {
5824858330 /** @type {Map<string, PerMessageDeflate>} */
5824958331 #extensions
5825058332
58251- constructor (ws, extensions) {
58333+ /** @type {{ maxDecompressedMessageSize?: number }} */
58334+ #options
58335+
58336+ /**
58337+ * @param {import('./websocket').WebSocket} ws
58338+ * @param {Map<string, string>|null} extensions
58339+ * @param {{ maxDecompressedMessageSize?: number }} [options]
58340+ */
58341+ constructor (ws, extensions, options = {}) {
5825258342 super()
5825358343
5825458344 this.ws = ws
5825558345 this.#extensions = extensions == null ? new Map() : extensions
58346+ this.#options = options
5825658347
5825758348 if (this.#extensions.has('permessage-deflate')) {
58258- this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
58349+ this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options ))
5825958350 }
5826058351 }
5826158352
@@ -58390,21 +58481,20 @@ class ByteParser extends Writable {
5839058481
5839158482 const buffer = this.consume(8)
5839258483 const upper = buffer.readUInt32BE(0)
58484+ const lower = buffer.readUInt32BE(4)
5839358485
5839458486 // 2^31 is the maximum bytes an arraybuffer can contain
5839558487 // on 32-bit systems. Although, on 64-bit systems, this is
5839658488 // 2^53-1 bytes.
5839758489 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
5839858490 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
5839958491 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
58400- if (upper > 2 ** 31 - 1) {
58492+ if (upper !== 0 || lower > 2 ** 31 - 1) {
5840158493 failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.')
5840258494 return
5840358495 }
5840458496
58405- const lower = buffer.readUInt32BE(4)
58406-
58407- this.#info.payloadLength = (upper << 8) + lower
58497+ this.#info.payloadLength = lower
5840858498 this.#state = parserStates.READ_DATA
5840958499 } else if (this.#state === parserStates.READ_DATA) {
5841058500 if (this.#byteOffset < this.#info.payloadLength) {
@@ -58434,7 +58524,7 @@ class ByteParser extends Writable {
5843458524 } else {
5843558525 this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
5843658526 if (error) {
58437- closeWebSocketConnection (this.ws, 1007, error.message, error.message.length )
58527+ failWebsocketConnection (this.ws, error.message)
5843858528 return
5843958529 }
5844058530
@@ -59041,6 +59131,12 @@ function parseExtensions (extensions) {
5904159131 * @param {string} value
5904259132 */
5904359133function isValidClientWindowBits (value) {
59134+ // Must have at least one character
59135+ if (value.length === 0) {
59136+ return false
59137+ }
59138+
59139+ // Check all characters are ASCII digits
5904459140 for (let i = 0; i < value.length; i++) {
5904559141 const byte = value.charCodeAt(i)
5904659142
@@ -59049,7 +59145,9 @@ function isValidClientWindowBits (value) {
5904959145 }
5905059146 }
5905159147
59052- return true
59148+ // Check numeric range: zlib requires windowBits in range 8-15
59149+ const num = Number.parseInt(value, 10)
59150+ return num >= 8 && num <= 15
5905359151}
5905459152
5905559153// https://nodejs.org/api/intl.html#detecting-internationalization-support
@@ -59141,6 +59239,9 @@ class WebSocket extends EventTarget {
5914159239 /** @type {SendQueue} */
5914259240 #sendQueue
5914359241
59242+ /** @type {{ maxDecompressedMessageSize?: number }} */
59243+ #options
59244+
5914459245 /**
5914559246 * @param {string} url
5914659247 * @param {string|string[]} protocols
@@ -59214,6 +59315,11 @@ class WebSocket extends EventTarget {
5921459315 // 10. Set this's url to urlRecord.
5921559316 this[kWebSocketURL] = new URL(urlRecord.href)
5921659317
59318+ // Store options for later use (e.g., maxDecompressedMessageSize)
59319+ this.#options = {
59320+ maxDecompressedMessageSize: options.maxDecompressedMessageSize
59321+ }
59322+
5921759323 // 11. Let client be this's relevant settings object.
5921859324 const client = environmentSettingsObject.settingsObject
5921959325
@@ -59528,11 +59634,11 @@ class WebSocket extends EventTarget {
5952859634 * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
5952959635 */
5953059636 #onConnectionEstablished (response, parsedExtensions) {
59531- // processResponse is called when the "response’ s header list has been received and initialized."
59637+ // processResponse is called when the "response' s header list has been received and initialized."
5953259638 // once this happens, the connection is open
5953359639 this[kResponse] = response
5953459640
59535- const parser = new ByteParser(this, parsedExtensions)
59641+ const parser = new ByteParser(this, parsedExtensions, this.#options )
5953659642 parser.on('drain', onParserDrain)
5953759643 parser.on('error', onParserError.bind(this))
5953859644
@@ -59635,6 +59741,19 @@ webidl.converters.WebSocketInit = webidl.dictionaryConverter([
5963559741 {
5963659742 key: 'headers',
5963759743 converter: webidl.nullableConverter(webidl.converters.HeadersInit)
59744+ },
59745+ {
59746+ key: 'maxDecompressedMessageSize',
59747+ converter: webidl.nullableConverter((V) => {
59748+ V = webidl.converters['unsigned long long'](V)
59749+ if (V <= 0) {
59750+ throw webidl.errors.exception({
59751+ header: 'WebSocket constructor',
59752+ message: 'maxDecompressedMessageSize must be greater than 0'
59753+ })
59754+ }
59755+ return V
59756+ })
5963859757 }
5963959758])
5964059759
0 commit comments