From 9ce08130ac0827cf6b050f654a17d53f95b73604 Mon Sep 17 00:00:00 2001 From: Antoine Froger Date: Fri, 18 Sep 2026 10:20:36 +0200 Subject: [PATCH 1/2] Bump Node.js from 22 to 24 and refresh lockfile Run npm audit fix --- dist/index.js | 842 +++++++++++++----- dist/index.js.map | 2 +- package-lock.json | 2135 ++++++++++++++++++++++++--------------------- 3 files changed, 1774 insertions(+), 1205 deletions(-) diff --git a/dist/index.js b/dist/index.js index 5a5e36b..b9aef23 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2563,7 +2563,13 @@ function requireRequest$1 () { } else if (typeof val[i] === 'object') { throw new InvalidArgumentError(`invalid ${key} header`) } else { - arr.push(`${val[i]}`); + // Coerce primitives (and reject unsafe coercions such as functions + // with a crafted toString/Symbol.toPrimitive). + const str = `${val[i]}`; + if (!isValidHeaderValue(str)) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + arr.push(str); } } val = arr; @@ -2574,7 +2580,12 @@ function requireRequest$1 () { } else if (val === null) { val = ''; } else { + // Coerce primitives (and reject unsafe coercions such as functions + // with a crafted toString/Symbol.toPrimitive). val = `${val}`; + if (!isValidHeaderValue(val)) { + throw new InvalidArgumentError(`invalid ${key} header`) + } } if (headerName === 'host') { @@ -2725,6 +2736,7 @@ function requireDispatcherBase () { get webSocketOptions () { return { + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 } } @@ -8629,6 +8641,7 @@ function requireClientH1 () { RequestContentLengthMismatchError, ResponseContentLengthMismatchError, RequestAbortedError, + InvalidArgumentError, HeadersTimeoutError, HeadersOverflowError, SocketError, @@ -8676,6 +8689,9 @@ function requireClientH1 () { const FastBuffer = Buffer[Symbol.species]; const addListener = util.addListener; const removeAllListeners = util.removeAllListeners; + const kIdleSocketValidation = Symbol('kIdleSocketValidation'); + const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout'); + const kSocketUsed = Symbol('kSocketUsed'); let extractBody; @@ -8898,29 +8914,71 @@ function requireClientH1 () { const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr; - if (ret === constants.ERROR.PAUSED_UPGRADE) { - this.onUpgrade(data.slice(offset)); - } else if (ret === constants.ERROR.PAUSED) { - this.paused = true; - socket.unshift(data.slice(offset)); - } else if (ret !== constants.ERROR.OK) { - const ptr = llhttp.llhttp_get_error_reason(this.ptr); - let message = ''; - /* istanbul ignore else: difficult to make a test case for */ - if (ptr) { - const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0); - message = - 'Response does not match the HTTP/1.1 protocol (' + - Buffer.from(llhttp.memory.buffer, ptr, len).toString() + - ')'; + if (ret !== constants.ERROR.OK) { + const body = data.subarray(offset); + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(body); + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true; + socket.unshift(body); + } else { + throw this.createError(ret, body) } - throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) } } catch (err) { util.destroy(socket, err); } } + finish () { + assert(currentParser === null); + assert(this.ptr != null); + assert(!this.paused); + + const { llhttp } = this; + + let ret; + + try { + currentParser = this; + ret = llhttp.llhttp_finish(this.ptr); + } finally { + currentParser = null; + } + + if (ret === constants.ERROR.OK) { + return null + } + + if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) { + this.paused = true; + return null + } + + return this.createError(ret, EMPTY_BUF) + } + + createError (ret, data) { + const { llhttp, contentLength, bytesRead } = this; + + if (contentLength && bytesRead !== parseInt(contentLength, 10)) { + return new ResponseContentLengthMismatchError() + } + + const ptr = llhttp.llhttp_get_error_reason(this.ptr); + let message = ''; + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0); + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')'; + } + + return new HTTPParserError(message, constants.ERROR[ret], data) + } + destroy () { assert(this.ptr != null); assert(currentParser == null); @@ -8948,6 +9006,11 @@ function requireClientH1 () { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))); + return -1 + } + const request = client[kQueue][client[kRunningIdx]]; if (!request) { return -1 @@ -9051,6 +9114,11 @@ function requireClientH1 () { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))); + return -1 + } + const request = client[kQueue][client[kRunningIdx]]; /* istanbul ignore next: difficult to make a test case for */ @@ -9224,6 +9292,7 @@ function requireClientH1 () { request.onComplete(headers); client[kQueue][client[kRunningIdx]++] = null; + socket[kSocketUsed] = true; if (socket[kWriting]) { assert(client[kRunning] === 0); @@ -9282,6 +9351,9 @@ function requireClientH1 () { socket[kWriting] = false; socket[kReset] = false; socket[kBlocking] = false; + socket[kIdleSocketValidation] = 0; + socket[kIdleSocketValidationTimeout] = null; + socket[kSocketUsed] = false; socket[kParser] = new Parser(client, socket, llhttpInstance); addListener(socket, 'error', function (err) { @@ -9292,8 +9364,11 @@ function requireClientH1 () { // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded // to the user. if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so for as a valid response. - parser.onMessageComplete(); + const parserErr = parser.finish(); + if (parserErr) { + this[kError] = parserErr; + this[kClient][kOnError](parserErr); + } return } @@ -9312,8 +9387,10 @@ function requireClientH1 () { const parser = this[kParser]; if (parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete(); + const parserErr = parser.finish(); + if (parserErr) { + util.destroy(this, parserErr); + } return } @@ -9323,10 +9400,11 @@ function requireClientH1 () { const client = this[kClient]; const parser = this[kParser]; + clearIdleSocketValidation(this); + if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete(); + this[kError] = parser.finish() || this[kError]; } this[kParser].destroy(); @@ -9389,7 +9467,7 @@ function requireClientH1 () { return socket.destroyed }, busy (request) { - if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) { return true } @@ -9427,6 +9505,39 @@ function requireClientH1 () { } } + function clearIdleSocketValidation (socket) { + if (socket[kIdleSocketValidationTimeout]) { + clearImmediate(socket[kIdleSocketValidationTimeout]); + socket[kIdleSocketValidationTimeout] = null; + } + + socket[kIdleSocketValidation] = 0; + } + + function scheduleIdleSocketValidation (client, socket) { + socket[kIdleSocketValidation] = 1; + // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST + // already pending on this idle keep-alive socket are processed before the + // next request is written (GHSA-35p6-xmwp-9g52). + // + // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse + // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll + // block for ~500ms when the event loop is otherwise idle (#5600 / #5606). + // A ref'd Immediate both keeps the pending request alive and makes poll + // return immediately — the hybrid those issues asked for. + socket[kIdleSocketValidationTimeout] = setImmediate(() => { + socket[kIdleSocketValidationTimeout] = null; + socket[kIdleSocketValidation] = 2; + + if (client[kSocket] === socket && !socket.destroyed) { + client[kResume](); + } + }); + } + + /** + * @param {import('./client.js')} client + */ function resumeH1 (client) { const socket = client[kSocket]; @@ -9441,6 +9552,32 @@ function requireClientH1 () { socket[kNoRef] = false; } + if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) { + if (socket[kIdleSocketValidation] === 0) { + scheduleIdleSocketValidation(client, socket); + socket[kParser].readMore(); + if (socket.destroyed) { + return + } + return + } + + if (socket[kIdleSocketValidation] === 1) { + socket[kParser].readMore(); + if (socket.destroyed) { + return + } + return + } + } + + if (client[kRunning] === 0) { + socket[kParser].readMore(); + if (socket.destroyed) { + return + } + } + if (client[kSize] === 0) { if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE); @@ -9496,8 +9633,16 @@ function requireClientH1 () { } body = bodyStream.stream; contentLength = bodyStream.length; - } else if (util.isBlobLike(body) && request.contentType == null && body.type) { - headers.push('content-type', body.type); + } else if (util.isBlobLike(body) && request.contentType == null) { + const contentType = body.type; + if (contentType) { + const contentTypeValue = `${contentType}`; + if (!util.isValidHeaderValue(contentTypeValue)) { + util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header')); + return false + } + headers.push('content-type', contentTypeValue); + } } if (body && typeof body.read === 'function') { @@ -9534,6 +9679,7 @@ function requireClientH1 () { } const socket = client[kSocket]; + clearIdleSocketValidation(socket); const abort = (err) => { if (request.aborted || request.completed) { @@ -12353,7 +12499,6 @@ function requireAgent () { class Agent extends DispatcherBase { constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { - if (typeof factory !== 'function') { throw new InvalidArgumentError('factory must be a function.') } @@ -12932,6 +13077,28 @@ function requireRetryHandler () { return new Date(retryAfter).getTime() - current } + function validatePartialResponseContentLength (headers, range, statusCode, retryCount) { + const contentLength = headers['content-length']; + if (contentLength == null) { + return null + } + + if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) { + return null + } + + const length = Number(contentLength); + const expectedLength = range.end - range.start + 1; + if (!Number.isFinite(length) || length !== expectedLength) { + return new RequestRetryError('Content-Length mismatch', statusCode, { + headers, + data: { count: retryCount } + }) + } + + return null + } + class RetryHandler { constructor (opts, handlers) { const { retryOptions, ...dispatchOpts } = opts; @@ -12985,6 +13152,7 @@ function requireRetryHandler () { this.end = null; this.etag = null; this.resume = null; + this.headersSent = false; // Handle possible onConnect duplication this.handler.onConnect(reason => { @@ -12997,6 +13165,20 @@ function requireRetryHandler () { }); } + checkpointResponseEnd (headers, resume) { + if (this.end == null && this.opts.method !== 'HEAD') { + const contentLength = headers['content-length']; + this.end = contentLength != null ? Number(contentLength) - 1 : null; + + assert( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ); + } + + this.resume = this.end != null ? resume : null; + } + onRequestSent () { if (this.handler.onRequestSent) { this.handler.onRequestSent(); @@ -13086,6 +13268,8 @@ function requireRetryHandler () { if (statusCode >= 300) { if (this.retryOpts.statusCodes.includes(statusCode) === false) { + this.headersSent = true; + this.checkpointResponseEnd(headers, resume); return this.handler.onHeaders( statusCode, rawHeaders, @@ -13146,10 +13330,23 @@ function requireRetryHandler () { return false } + const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount); + if (contentLengthError != null) { + this.abort(contentLengthError); + return false + } + const { start, size, end = size - 1 } = contentRange; - assert(this.start === start, 'content-range mismatch'); - assert(this.end == null || this.end === end, 'content-range mismatch'); + if (this.start !== start || (this.end != null && this.end !== end)) { + this.abort( + new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + data: { count: this.retryCount } + }) + ); + return false + } this.resume = resume; return true @@ -13161,6 +13358,7 @@ function requireRetryHandler () { const range = parseRangeHeader(headers['content-range']); if (range == null) { + this.headersSent = true; return this.handler.onHeaders( statusCode, rawHeaders, @@ -13169,6 +13367,12 @@ function requireRetryHandler () { ) } + const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount); + if (contentLengthError != null) { + this.abort(contentLengthError); + return false + } + const { start, size, end = size - 1 } = range; assert( start != null && Number.isFinite(start), @@ -13193,6 +13397,7 @@ function requireRetryHandler () { ); this.resume = resume; + this.headersSent = true; this.etag = headers.etag != null ? headers.etag : null; // Weak etags are not useful for comparison nor cache @@ -13232,7 +13437,7 @@ function requireRetryHandler () { } onError (err) { - if (this.aborted || isDisturbed(this.opts.body)) { + if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) { return this.handler.onError(err) } @@ -23557,7 +23762,7 @@ function requireUtil$2 () { if ( code < 0x20 || // exclude CTLs (0-31) - code === 0x7F || // DEL + code > 0x7E || // exclude DEL and non-ascii code === 0x3B // ; ) { throw new Error('Invalid cookie path') @@ -23566,16 +23771,80 @@ function requireUtil$2 () { } /** - * I have no idea why these values aren't allowed to be honest, - * but Deno tests these. - Khafra + * ::= | + * + * ::= any one of the 52 alphabetic characters A through Z in + * upper case and a through z in lower case + * + * ::= any one of the ten digits 0 through 9r + * + * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5 + * @param {number} code + */ + function isLetterOrDigit (code) { + return ( + (code >= 0x30 && code <= 0x39) || // 0-9 + (code >= 0x41 && code <= 0x5A) || // A-Z + (code >= 0x61 && code <= 0x7A) // a-z + ) + } + + /** + * Validates a cookie domain against the "preferred name syntax". + * + * ::= | " " + * ::=