@@ -2929,6 +2929,24 @@ class SecureProxyConnectionError extends UndiciError {
29292929 [kSecureProxyConnectionError] = true
29302930}
29312931
2932+ const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED')
2933+ class MessageSizeExceededError extends UndiciError {
2934+ constructor (message) {
2935+ super(message)
2936+ this.name = 'MessageSizeExceededError'
2937+ this.message = message || 'Max decompressed message size exceeded'
2938+ this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
2939+ }
2940+
2941+ static [Symbol.hasInstance] (instance) {
2942+ return instance && instance[kMessageSizeExceededError] === true
2943+ }
2944+
2945+ get [kMessageSizeExceededError] () {
2946+ return true
2947+ }
2948+ }
2949+
29322950module.exports = {
29332951 AbortError,
29342952 HTTPParserError,
@@ -2952,7 +2970,8 @@ module.exports = {
29522970 ResponseExceededMaxSizeError,
29532971 RequestRetryError,
29542972 ResponseError,
2955- SecureProxyConnectionError
2973+ SecureProxyConnectionError,
2974+ MessageSizeExceededError
29562975}
29572976
29582977
@@ -3029,6 +3048,10 @@ class Request {
30293048 throw new InvalidArgumentError('upgrade must be a string')
30303049 }
30313050
3051+ if (upgrade && !isValidHeaderValue(upgrade)) {
3052+ throw new InvalidArgumentError('invalid upgrade header')
3053+ }
3054+
30323055 if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
30333056 throw new InvalidArgumentError('invalid headersTimeout')
30343057 }
@@ -3323,13 +3346,19 @@ function processHeader (request, key, val) {
33233346 val = `${val}`
33243347 }
33253348
3326- if (request.host === null && headerName === 'host') {
3349+ if (headerName === 'host') {
3350+ if (request.host !== null) {
3351+ throw new InvalidArgumentError('duplicate host header')
3352+ }
33273353 if (typeof val !== 'string') {
33283354 throw new InvalidArgumentError('invalid host header')
33293355 }
33303356 // Consumed by Client
33313357 request.host = val
3332- } else if (request.contentLength === null && headerName === 'content-length') {
3358+ } else if (headerName === 'content-length') {
3359+ if (request.contentLength !== null) {
3360+ throw new InvalidArgumentError('duplicate content-length header')
3361+ }
33333362 request.contentLength = parseInt(val, 10)
33343363 if (!Number.isFinite(request.contentLength)) {
33353364 throw new InvalidArgumentError('invalid content-length header')
@@ -26046,17 +26075,30 @@ module.exports = {
2604626075
2604726076const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = __nccwpck_require__(8522)
2604826077const { isValidClientWindowBits } = __nccwpck_require__(8625)
26078+ const { MessageSizeExceededError } = __nccwpck_require__(8707)
2604926079
2605026080const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
2605126081const kBuffer = Symbol('kBuffer')
2605226082const kLength = Symbol('kLength')
2605326083
26084+ // Default maximum decompressed message size: 4 MB
26085+ const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
26086+
2605426087class PerMessageDeflate {
2605526088 /** @type {import('node:zlib').InflateRaw} */
2605626089 #inflate
2605726090
2605826091 #options = {}
2605926092
26093+ /** @type {boolean} */
26094+ #aborted = false
26095+
26096+ /** @type {Function|null} */
26097+ #currentCallback = null
26098+
26099+ /**
26100+ * @param {Map<string, string>} extensions
26101+ */
2606026102 constructor (extensions) {
2606126103 this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
2606226104 this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
@@ -26068,6 +26110,11 @@ class PerMessageDeflate {
2606826110 // payload of the message.
2606926111 // 2. Decompress the resulting data using DEFLATE.
2607026112
26113+ if (this.#aborted) {
26114+ callback(new MessageSizeExceededError())
26115+ return
26116+ }
26117+
2607126118 if (!this.#inflate) {
2607226119 let windowBits = Z_DEFAULT_WINDOWBITS
2607326120
@@ -26080,13 +26127,37 @@ class PerMessageDeflate {
2608026127 windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
2608126128 }
2608226129
26083- this.#inflate = createInflateRaw({ windowBits })
26130+ try {
26131+ this.#inflate = createInflateRaw({ windowBits })
26132+ } catch (err) {
26133+ callback(err)
26134+ return
26135+ }
2608426136 this.#inflate[kBuffer] = []
2608526137 this.#inflate[kLength] = 0
2608626138
2608726139 this.#inflate.on('data', (data) => {
26088- this.#inflate[kBuffer].push(data)
26140+ if (this.#aborted) {
26141+ return
26142+ }
26143+
2608926144 this.#inflate[kLength] += data.length
26145+
26146+ if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) {
26147+ this.#aborted = true
26148+ this.#inflate.removeAllListeners()
26149+ this.#inflate.destroy()
26150+ this.#inflate = null
26151+
26152+ if (this.#currentCallback) {
26153+ const cb = this.#currentCallback
26154+ this.#currentCallback = null
26155+ cb(new MessageSizeExceededError())
26156+ }
26157+ return
26158+ }
26159+
26160+ this.#inflate[kBuffer].push(data)
2609026161 })
2609126162
2609226163 this.#inflate.on('error', (err) => {
@@ -26095,16 +26166,22 @@ class PerMessageDeflate {
2609526166 })
2609626167 }
2609726168
26169+ this.#currentCallback = callback
2609826170 this.#inflate.write(chunk)
2609926171 if (fin) {
2610026172 this.#inflate.write(tail)
2610126173 }
2610226174
2610326175 this.#inflate.flush(() => {
26176+ if (this.#aborted || !this.#inflate) {
26177+ return
26178+ }
26179+
2610426180 const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
2610526181
2610626182 this.#inflate[kBuffer].length = 0
2610726183 this.#inflate[kLength] = 0
26184+ this.#currentCallback = null
2610826185
2610926186 callback(null, full)
2611026187 })
@@ -26158,6 +26235,10 @@ class ByteParser extends Writable {
2615826235 /** @type {Map<string, PerMessageDeflate>} */
2615926236 #extensions
2616026237
26238+ /**
26239+ * @param {import('./websocket').WebSocket} ws
26240+ * @param {Map<string, string>|null} extensions
26241+ */
2616126242 constructor (ws, extensions) {
2616226243 super()
2616326244
@@ -26300,21 +26381,20 @@ class ByteParser extends Writable {
2630026381
2630126382 const buffer = this.consume(8)
2630226383 const upper = buffer.readUInt32BE(0)
26384+ const lower = buffer.readUInt32BE(4)
2630326385
2630426386 // 2^31 is the maximum bytes an arraybuffer can contain
2630526387 // on 32-bit systems. Although, on 64-bit systems, this is
2630626388 // 2^53-1 bytes.
2630726389 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
2630826390 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
2630926391 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
26310- if (upper > 2 ** 31 - 1) {
26392+ if (upper !== 0 || lower > 2 ** 31 - 1) {
2631126393 failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.')
2631226394 return
2631326395 }
2631426396
26315- const lower = buffer.readUInt32BE(4)
26316-
26317- this.#info.payloadLength = (upper << 8) + lower
26397+ this.#info.payloadLength = lower
2631826398 this.#state = parserStates.READ_DATA
2631926399 } else if (this.#state === parserStates.READ_DATA) {
2632026400 if (this.#byteOffset < this.#info.payloadLength) {
@@ -26344,7 +26424,7 @@ class ByteParser extends Writable {
2634426424 } else {
2634526425 this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
2634626426 if (error) {
26347- closeWebSocketConnection (this.ws, 1007, error.message, error.message.length )
26427+ failWebsocketConnection (this.ws, error.message)
2634826428 return
2634926429 }
2635026430
@@ -26948,6 +27028,12 @@ function parseExtensions (extensions) {
2694827028 * @param {string} value
2694927029 */
2695027030function isValidClientWindowBits (value) {
27031+ // Must have at least one character
27032+ if (value.length === 0) {
27033+ return false
27034+ }
27035+
27036+ // Check all characters are ASCII digits
2695127037 for (let i = 0; i < value.length; i++) {
2695227038 const byte = value.charCodeAt(i)
2695327039
@@ -26956,7 +27042,9 @@ function isValidClientWindowBits (value) {
2695627042 }
2695727043 }
2695827044
26959- return true
27045+ // Check numeric range: zlib requires windowBits in range 8-15
27046+ const num = Number.parseInt(value, 10)
27047+ return num >= 8 && num <= 15
2696027048}
2696127049
2696227050// https://nodejs.org/api/intl.html#detecting-internationalization-support
@@ -27434,7 +27522,7 @@ class WebSocket extends EventTarget {
2743427522 * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
2743527523 */
2743627524 #onConnectionEstablished (response, parsedExtensions) {
27437- // processResponse is called when the "response’ s header list has been received and initialized."
27525+ // processResponse is called when the "response' s header list has been received and initialized."
2743827526 // once this happens, the connection is open
2743927527 this[kResponse] = response
2744027528
0 commit comments