@@ -13788,6 +13788,24 @@ class SecureProxyConnectionError extends UndiciError {
1378813788 [kSecureProxyConnectionError] = true
1378913789}
1379013790
13791+ const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED')
13792+ class MessageSizeExceededError extends UndiciError {
13793+ constructor (message) {
13794+ super(message)
13795+ this.name = 'MessageSizeExceededError'
13796+ this.message = message || 'Max decompressed message size exceeded'
13797+ this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
13798+ }
13799+
13800+ static [Symbol.hasInstance] (instance) {
13801+ return instance && instance[kMessageSizeExceededError] === true
13802+ }
13803+
13804+ get [kMessageSizeExceededError] () {
13805+ return true
13806+ }
13807+ }
13808+
1379113809module.exports = {
1379213810 AbortError,
1379313811 HTTPParserError,
@@ -13811,7 +13829,8 @@ module.exports = {
1381113829 ResponseExceededMaxSizeError,
1381213830 RequestRetryError,
1381313831 ResponseError,
13814- SecureProxyConnectionError
13832+ SecureProxyConnectionError,
13833+ MessageSizeExceededError
1381513834}
1381613835
1381713836
@@ -13889,6 +13908,10 @@ class Request {
1388913908 throw new InvalidArgumentError('upgrade must be a string')
1389013909 }
1389113910
13911+ if (upgrade && !isValidHeaderValue(upgrade)) {
13912+ throw new InvalidArgumentError('invalid upgrade header')
13913+ }
13914+
1389213915 if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
1389313916 throw new InvalidArgumentError('invalid headersTimeout')
1389413917 }
@@ -14183,13 +14206,19 @@ function processHeader (request, key, val) {
1418314206 val = `${val}`
1418414207 }
1418514208
14186- if (request.host === null && headerName === 'host') {
14209+ if (headerName === 'host') {
14210+ if (request.host !== null) {
14211+ throw new InvalidArgumentError('duplicate host header')
14212+ }
1418714213 if (typeof val !== 'string') {
1418814214 throw new InvalidArgumentError('invalid host header')
1418914215 }
1419014216 // Consumed by Client
1419114217 request.host = val
14192- } else if (request.contentLength === null && headerName === 'content-length') {
14218+ } else if (headerName === 'content-length') {
14219+ if (request.contentLength !== null) {
14220+ throw new InvalidArgumentError('duplicate content-length header')
14221+ }
1419314222 request.contentLength = parseInt(val, 10)
1419414223 if (!Number.isFinite(request.contentLength)) {
1419514224 throw new InvalidArgumentError('invalid content-length header')
@@ -36980,17 +37009,30 @@ module.exports = {
3698037009
3698137010const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = __nccwpck_require__(8522)
3698237011const { isValidClientWindowBits } = __nccwpck_require__(8625)
37012+ const { MessageSizeExceededError } = __nccwpck_require__(8707)
3698337013
3698437014const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
3698537015const kBuffer = Symbol('kBuffer')
3698637016const kLength = Symbol('kLength')
3698737017
37018+ // Default maximum decompressed message size: 4 MB
37019+ const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
37020+
3698837021class PerMessageDeflate {
3698937022 /** @type {import('node:zlib').InflateRaw} */
3699037023 #inflate
3699137024
3699237025 #options = {}
3699337026
37027+ /** @type {boolean} */
37028+ #aborted = false
37029+
37030+ /** @type {Function|null} */
37031+ #currentCallback = null
37032+
37033+ /**
37034+ * @param {Map<string, string>} extensions
37035+ */
3699437036 constructor (extensions) {
3699537037 this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
3699637038 this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
@@ -37002,6 +37044,11 @@ class PerMessageDeflate {
3700237044 // payload of the message.
3700337045 // 2. Decompress the resulting data using DEFLATE.
3700437046
37047+ if (this.#aborted) {
37048+ callback(new MessageSizeExceededError())
37049+ return
37050+ }
37051+
3700537052 if (!this.#inflate) {
3700637053 let windowBits = Z_DEFAULT_WINDOWBITS
3700737054
@@ -37014,13 +37061,37 @@ class PerMessageDeflate {
3701437061 windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
3701537062 }
3701637063
37017- this.#inflate = createInflateRaw({ windowBits })
37064+ try {
37065+ this.#inflate = createInflateRaw({ windowBits })
37066+ } catch (err) {
37067+ callback(err)
37068+ return
37069+ }
3701837070 this.#inflate[kBuffer] = []
3701937071 this.#inflate[kLength] = 0
3702037072
3702137073 this.#inflate.on('data', (data) => {
37022- this.#inflate[kBuffer].push(data)
37074+ if (this.#aborted) {
37075+ return
37076+ }
37077+
3702337078 this.#inflate[kLength] += data.length
37079+
37080+ if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) {
37081+ this.#aborted = true
37082+ this.#inflate.removeAllListeners()
37083+ this.#inflate.destroy()
37084+ this.#inflate = null
37085+
37086+ if (this.#currentCallback) {
37087+ const cb = this.#currentCallback
37088+ this.#currentCallback = null
37089+ cb(new MessageSizeExceededError())
37090+ }
37091+ return
37092+ }
37093+
37094+ this.#inflate[kBuffer].push(data)
3702437095 })
3702537096
3702637097 this.#inflate.on('error', (err) => {
@@ -37029,16 +37100,22 @@ class PerMessageDeflate {
3702937100 })
3703037101 }
3703137102
37103+ this.#currentCallback = callback
3703237104 this.#inflate.write(chunk)
3703337105 if (fin) {
3703437106 this.#inflate.write(tail)
3703537107 }
3703637108
3703737109 this.#inflate.flush(() => {
37110+ if (this.#aborted || !this.#inflate) {
37111+ return
37112+ }
37113+
3703837114 const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
3703937115
3704037116 this.#inflate[kBuffer].length = 0
3704137117 this.#inflate[kLength] = 0
37118+ this.#currentCallback = null
3704237119
3704337120 callback(null, full)
3704437121 })
@@ -37093,6 +37170,10 @@ class ByteParser extends Writable {
3709337170 /** @type {Map<string, PerMessageDeflate>} */
3709437171 #extensions
3709537172
37173+ /**
37174+ * @param {import('./websocket').WebSocket} ws
37175+ * @param {Map<string, string>|null} extensions
37176+ */
3709637177 constructor (ws, extensions) {
3709737178 super()
3709837179
@@ -37235,21 +37316,20 @@ class ByteParser extends Writable {
3723537316
3723637317 const buffer = this.consume(8)
3723737318 const upper = buffer.readUInt32BE(0)
37319+ const lower = buffer.readUInt32BE(4)
3723837320
3723937321 // 2^31 is the maximum bytes an arraybuffer can contain
3724037322 // on 32-bit systems. Although, on 64-bit systems, this is
3724137323 // 2^53-1 bytes.
3724237324 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
3724337325 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
3724437326 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
37245- if (upper > 2 ** 31 - 1) {
37327+ if (upper !== 0 || lower > 2 ** 31 - 1) {
3724637328 failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.')
3724737329 return
3724837330 }
3724937331
37250- const lower = buffer.readUInt32BE(4)
37251-
37252- this.#info.payloadLength = (upper << 8) + lower
37332+ this.#info.payloadLength = lower
3725337333 this.#state = parserStates.READ_DATA
3725437334 } else if (this.#state === parserStates.READ_DATA) {
3725537335 if (this.#byteOffset < this.#info.payloadLength) {
@@ -37279,7 +37359,7 @@ class ByteParser extends Writable {
3727937359 } else {
3728037360 this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
3728137361 if (error) {
37282- closeWebSocketConnection (this.ws, 1007, error.message, error.message.length )
37362+ failWebsocketConnection (this.ws, error.message)
3728337363 return
3728437364 }
3728537365
@@ -37886,6 +37966,12 @@ function parseExtensions (extensions) {
3788637966 * @param {string} value
3788737967 */
3788837968function isValidClientWindowBits (value) {
37969+ // Must have at least one character
37970+ if (value.length === 0) {
37971+ return false
37972+ }
37973+
37974+ // Check all characters are ASCII digits
3788937975 for (let i = 0; i < value.length; i++) {
3789037976 const byte = value.charCodeAt(i)
3789137977
@@ -37894,7 +37980,9 @@ function isValidClientWindowBits (value) {
3789437980 }
3789537981 }
3789637982
37897- return true
37983+ // Check numeric range: zlib requires windowBits in range 8-15
37984+ const num = Number.parseInt(value, 10)
37985+ return num >= 8 && num <= 15
3789837986}
3789937987
3790037988// https://nodejs.org/api/intl.html#detecting-internationalization-support
@@ -38373,7 +38461,7 @@ class WebSocket extends EventTarget {
3837338461 * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
3837438462 */
3837538463 #onConnectionEstablished (response, parsedExtensions) {
38376- // processResponse is called when the "response’ s header list has been received and initialized."
38464+ // processResponse is called when the "response' s header list has been received and initialized."
3837738465 // once this happens, the connection is open
3837838466 this[kResponse] = response
3837938467
0 commit comments