@@ -10783,6 +10783,24 @@ class SecureProxyConnectionError extends UndiciError {
1078310783 [kSecureProxyConnectionError] = true
1078410784}
1078510785
10786+ const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED')
10787+ class MessageSizeExceededError extends UndiciError {
10788+ constructor (message) {
10789+ super(message)
10790+ this.name = 'MessageSizeExceededError'
10791+ this.message = message || 'Max decompressed message size exceeded'
10792+ this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
10793+ }
10794+
10795+ static [Symbol.hasInstance] (instance) {
10796+ return instance && instance[kMessageSizeExceededError] === true
10797+ }
10798+
10799+ get [kMessageSizeExceededError] () {
10800+ return true
10801+ }
10802+ }
10803+
1078610804module.exports = {
1078710805 AbortError,
1078810806 HTTPParserError,
@@ -10806,7 +10824,8 @@ module.exports = {
1080610824 ResponseExceededMaxSizeError,
1080710825 RequestRetryError,
1080810826 ResponseError,
10809- SecureProxyConnectionError
10827+ SecureProxyConnectionError,
10828+ MessageSizeExceededError
1081010829}
1081110830
1081210831
@@ -10884,6 +10903,10 @@ class Request {
1088410903 throw new InvalidArgumentError('upgrade must be a string')
1088510904 }
1088610905
10906+ if (upgrade && !isValidHeaderValue(upgrade)) {
10907+ throw new InvalidArgumentError('invalid upgrade header')
10908+ }
10909+
1088710910 if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
1088810911 throw new InvalidArgumentError('invalid headersTimeout')
1088910912 }
@@ -11178,13 +11201,19 @@ function processHeader (request, key, val) {
1117811201 val = `${val}`
1117911202 }
1118011203
11181- if (request.host === null && headerName === 'host') {
11204+ if (headerName === 'host') {
11205+ if (request.host !== null) {
11206+ throw new InvalidArgumentError('duplicate host header')
11207+ }
1118211208 if (typeof val !== 'string') {
1118311209 throw new InvalidArgumentError('invalid host header')
1118411210 }
1118511211 // Consumed by Client
1118611212 request.host = val
11187- } else if (request.contentLength === null && headerName === 'content-length') {
11213+ } else if (headerName === 'content-length') {
11214+ if (request.contentLength !== null) {
11215+ throw new InvalidArgumentError('duplicate content-length header')
11216+ }
1118811217 request.contentLength = parseInt(val, 10)
1118911218 if (!Number.isFinite(request.contentLength)) {
1119011219 throw new InvalidArgumentError('invalid content-length header')
@@ -33975,17 +34004,30 @@ module.exports = {
3397534004
3397634005const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = __nccwpck_require__(8522)
3397734006const { isValidClientWindowBits } = __nccwpck_require__(8625)
34007+ const { MessageSizeExceededError } = __nccwpck_require__(8707)
3397834008
3397934009const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
3398034010const kBuffer = Symbol('kBuffer')
3398134011const kLength = Symbol('kLength')
3398234012
34013+ // Default maximum decompressed message size: 4 MB
34014+ const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
34015+
3398334016class PerMessageDeflate {
3398434017 /** @type {import('node:zlib').InflateRaw} */
3398534018 #inflate
3398634019
3398734020 #options = {}
3398834021
34022+ /** @type {boolean} */
34023+ #aborted = false
34024+
34025+ /** @type {Function|null} */
34026+ #currentCallback = null
34027+
34028+ /**
34029+ * @param {Map<string, string>} extensions
34030+ */
3398934031 constructor (extensions) {
3399034032 this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
3399134033 this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
@@ -33997,6 +34039,11 @@ class PerMessageDeflate {
3399734039 // payload of the message.
3399834040 // 2. Decompress the resulting data using DEFLATE.
3399934041
34042+ if (this.#aborted) {
34043+ callback(new MessageSizeExceededError())
34044+ return
34045+ }
34046+
3400034047 if (!this.#inflate) {
3400134048 let windowBits = Z_DEFAULT_WINDOWBITS
3400234049
@@ -34009,13 +34056,37 @@ class PerMessageDeflate {
3400934056 windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
3401034057 }
3401134058
34012- this.#inflate = createInflateRaw({ windowBits })
34059+ try {
34060+ this.#inflate = createInflateRaw({ windowBits })
34061+ } catch (err) {
34062+ callback(err)
34063+ return
34064+ }
3401334065 this.#inflate[kBuffer] = []
3401434066 this.#inflate[kLength] = 0
3401534067
3401634068 this.#inflate.on('data', (data) => {
34017- this.#inflate[kBuffer].push(data)
34069+ if (this.#aborted) {
34070+ return
34071+ }
34072+
3401834073 this.#inflate[kLength] += data.length
34074+
34075+ if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) {
34076+ this.#aborted = true
34077+ this.#inflate.removeAllListeners()
34078+ this.#inflate.destroy()
34079+ this.#inflate = null
34080+
34081+ if (this.#currentCallback) {
34082+ const cb = this.#currentCallback
34083+ this.#currentCallback = null
34084+ cb(new MessageSizeExceededError())
34085+ }
34086+ return
34087+ }
34088+
34089+ this.#inflate[kBuffer].push(data)
3401934090 })
3402034091
3402134092 this.#inflate.on('error', (err) => {
@@ -34024,16 +34095,22 @@ class PerMessageDeflate {
3402434095 })
3402534096 }
3402634097
34098+ this.#currentCallback = callback
3402734099 this.#inflate.write(chunk)
3402834100 if (fin) {
3402934101 this.#inflate.write(tail)
3403034102 }
3403134103
3403234104 this.#inflate.flush(() => {
34105+ if (this.#aborted || !this.#inflate) {
34106+ return
34107+ }
34108+
3403334109 const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
3403434110
3403534111 this.#inflate[kBuffer].length = 0
3403634112 this.#inflate[kLength] = 0
34113+ this.#currentCallback = null
3403734114
3403834115 callback(null, full)
3403934116 })
@@ -34088,6 +34165,10 @@ class ByteParser extends Writable {
3408834165 /** @type {Map<string, PerMessageDeflate>} */
3408934166 #extensions
3409034167
34168+ /**
34169+ * @param {import('./websocket').WebSocket} ws
34170+ * @param {Map<string, string>|null} extensions
34171+ */
3409134172 constructor (ws, extensions) {
3409234173 super()
3409334174
@@ -34230,21 +34311,20 @@ class ByteParser extends Writable {
3423034311
3423134312 const buffer = this.consume(8)
3423234313 const upper = buffer.readUInt32BE(0)
34314+ const lower = buffer.readUInt32BE(4)
3423334315
3423434316 // 2^31 is the maximum bytes an arraybuffer can contain
3423534317 // on 32-bit systems. Although, on 64-bit systems, this is
3423634318 // 2^53-1 bytes.
3423734319 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
3423834320 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
3423934321 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
34240- if (upper > 2 ** 31 - 1) {
34322+ if (upper !== 0 || lower > 2 ** 31 - 1) {
3424134323 failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.')
3424234324 return
3424334325 }
3424434326
34245- const lower = buffer.readUInt32BE(4)
34246-
34247- this.#info.payloadLength = (upper << 8) + lower
34327+ this.#info.payloadLength = lower
3424834328 this.#state = parserStates.READ_DATA
3424934329 } else if (this.#state === parserStates.READ_DATA) {
3425034330 if (this.#byteOffset < this.#info.payloadLength) {
@@ -34274,7 +34354,7 @@ class ByteParser extends Writable {
3427434354 } else {
3427534355 this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
3427634356 if (error) {
34277- closeWebSocketConnection (this.ws, 1007, error.message, error.message.length )
34357+ failWebsocketConnection (this.ws, error.message)
3427834358 return
3427934359 }
3428034360
@@ -34881,6 +34961,12 @@ function parseExtensions (extensions) {
3488134961 * @param {string} value
3488234962 */
3488334963function isValidClientWindowBits (value) {
34964+ // Must have at least one character
34965+ if (value.length === 0) {
34966+ return false
34967+ }
34968+
34969+ // Check all characters are ASCII digits
3488434970 for (let i = 0; i < value.length; i++) {
3488534971 const byte = value.charCodeAt(i)
3488634972
@@ -34889,7 +34975,9 @@ function isValidClientWindowBits (value) {
3488934975 }
3489034976 }
3489134977
34892- return true
34978+ // Check numeric range: zlib requires windowBits in range 8-15
34979+ const num = Number.parseInt(value, 10)
34980+ return num >= 8 && num <= 15
3489334981}
3489434982
3489534983// https://nodejs.org/api/intl.html#detecting-internationalization-support
@@ -35368,7 +35456,7 @@ class WebSocket extends EventTarget {
3536835456 * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
3536935457 */
3537035458 #onConnectionEstablished (response, parsedExtensions) {
35371- // processResponse is called when the "response’ s header list has been received and initialized."
35459+ // processResponse is called when the "response' s header list has been received and initialized."
3537235460 // once this happens, the connection is open
3537335461 this[kResponse] = response
3537435462
0 commit comments