Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ benchmarks/graphs

# ignore additional files using core.excludesFile
# https://git-scm.com/docs/gitignore

AGENTS.md
CLAUDE.md
IMPLEMENTATION_SUMMARY.md
9 changes: 8 additions & 1 deletion lib/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,14 @@ app.set = function set(setting, val) {
this.set('etag fn', compileETag(val));
break;
case 'query parser':
this.set('query parser fn', compileQueryParser(val));
this.set('query parser fn', compileQueryParser(val, this.get('query parser options')));
break;
case 'query parser options':
// Re-compile the query parser with new options
var currentParser = this.get('query parser');
if (currentParser) {
this.set('query parser fn', compileQueryParser(currentParser, val));
}
break;
Comment on lines +370 to 376
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] When 'query parser options' is set before 'query parser' mode is set, the options will be stored in settings but won't be used until the parser mode is set. However, when 'query parser' is subsequently set, it will correctly retrieve and use these options via this.get('query parser options') on line 368. This behavior should be documented or the code should handle this edge case more explicitly to avoid confusion.

Copilot uses AI. Check for mistakes.
case 'trust proxy':
this.set('trust proxy fn', compileTrust(val));
Expand Down
4 changes: 3 additions & 1 deletion lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ res.send = function send(body) {
// populate ETag
var etag;
if (generateETag && len !== undefined) {
if ((etag = etagFn(chunk, encoding))) {
// Pass response headers to ETag function for CORS-aware ETags
var responseHeaders = this.getHeaders ? this.getHeaders() : this._headers || {};
if ((etag = etagFn(chunk, encoding, responseHeaders))) {
this.set('ETag', etag);
}
}
Expand Down
97 changes: 84 additions & 13 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,36 @@ exports.etag = createETagGenerator({ weak: false })

exports.wetag = createETagGenerator({ weak: true })

/**
* Return strong ETag for `body` including CORS headers.
*
* @param {String|Buffer} body
* @param {String} [encoding]
* @param {Object} [headers]
* @return {String}
* @api private
*/

exports.etagCors = createETagGenerator({
weak: false,
includeHeaders: ['access-control-allow-origin']
})

/**
* Return weak ETag for `body` including CORS headers.
*
* @param {String|Buffer} body
* @param {String} [encoding]
* @param {Object} [headers]
* @return {String}
* @api private
*/

exports.wetagCors = createETagGenerator({
weak: true,
includeHeaders: ['access-control-allow-origin']
})

/**
* Normalize the given `type`, for example "html" becomes "text/html".
*
Expand Down Expand Up @@ -144,6 +174,12 @@ exports.compileETag = function(val) {
case 'strong':
fn = exports.etag;
break;
case 'weak-cors':
fn = exports.wetagCors;
break;
case 'strong-cors':
fn = exports.etagCors;
break;
default:
throw new TypeError('unknown value for etag function: ' + val);
}
Expand All @@ -155,11 +191,12 @@ exports.compileETag = function(val) {
* Compile "query parser" value to function.
*
* @param {String|Function} val
* @param {Object} [qsOptions] - Options for qs parser
* @return {Function}
* @api private
*/

exports.compileQueryParser = function compileQueryParser(val) {
exports.compileQueryParser = function compileQueryParser(val, qsOptions) {
var fn;

if (typeof val === 'function') {
Expand All @@ -174,7 +211,7 @@ exports.compileQueryParser = function compileQueryParser(val) {
case false:
break;
case 'extended':
fn = parseExtendedQueryString;
fn = createExtendedQueryParser(qsOptions);
break;
default:
throw new TypeError('unknown value for query parser function: ' + val);
Expand Down Expand Up @@ -242,30 +279,64 @@ exports.setCharset = function setCharset(type, charset) {
* the given options.
*
* @param {object} options
* @param {boolean} options.weak - Generate weak ETags
* @param {string[]} options.includeHeaders - Response headers to include in hash
* @return {function}
* @private
*/

function createETagGenerator (options) {
return function generateETag (body, encoding) {
var weak = options.weak;
var includeHeaders = options.includeHeaders || [];

return function generateETag (body, encoding, headers) {
var buf = !Buffer.isBuffer(body)
? Buffer.from(body, encoding)
: body
: body;

return etag(buf, options)
}
// If no headers to include, use body-only hashing (backward compatible)
if (includeHeaders.length === 0 || !headers) {
return etag(buf, { weak: weak });
}

// Combine body with specified headers
var headerParts = includeHeaders
.map(function(name) {
var value = headers[name.toLowerCase()];
return value ? String(value) : '';
})
.filter(Boolean);

if (headerParts.length === 0) {
// No headers present, fall back to body-only
return etag(buf, { weak: weak });
}

// Create combined buffer: body + header values
var headerBuf = Buffer.from(headerParts.join('|'), 'utf8');
Comment on lines +303 to +316
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] If a header value contains invalid UTF-8 sequences or other non-string data that can't be properly converted to a string, the String(value) conversion on line 306 might produce unexpected results, and Buffer.from(headerParts.join('|'), 'utf8') on line 316 could potentially fail or produce incorrect results. Consider adding validation or error handling for header values to ensure they're valid strings before including them in the ETag calculation.

Copilot uses AI. Check for mistakes.
var combined = Buffer.concat([buf, Buffer.from('|'), headerBuf]);
Comment on lines +316 to +317
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The implementation uses a pipe character (|) as a delimiter between body and headers (line 317). If the body itself ends with a pipe character and a header value starts with the same content, there's a theoretical (though extremely unlikely) hash collision risk. Consider using a more robust delimiter strategy, such as including the length of each component, or using a null byte (\x00) which is invalid in HTTP headers.

Suggested change
var headerBuf = Buffer.from(headerParts.join('|'), 'utf8');
var combined = Buffer.concat([buf, Buffer.from('|'), headerBuf]);
var headerBuf = Buffer.from(headerParts.join('\x00'), 'utf8');
var combined = Buffer.concat([buf, Buffer.from('\x00'), headerBuf]);

Copilot uses AI. Check for mistakes.

return etag(combined, { weak: weak });
};
}

/**
* Parse an extended query string with qs.
* Create an extended query string parser with qs.
*
* @param {String} str
* @return {Object}
* @param {Object} [options] - Options for qs.parse
* @return {Function}
* @private
*/

function parseExtendedQueryString(str) {
return qs.parse(str, {
allowPrototypes: true
});
function createExtendedQueryParser(options) {
var qsOptions = Object.assign({
allowPrototypes: true, // Backward compatibility (consider changing to false in v6)
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting allowPrototypes: true as the default (line 333) maintains backward compatibility but leaves applications vulnerable to prototype pollution attacks unless developers explicitly set it to false. While the comment mentions "consider changing to false in v6", this is a security risk in the current version. Consider documenting this security concern prominently in the PR description and migration guide, or at minimum add a deprecation warning when this default is used.

Copilot uses AI. Check for mistakes.
parameterLimit: 1000, // Explicit default
arrayLimit: 20, // qs default
depth: 5 // qs default
}, options || {});

return function parseExtendedQueryString(str) {
return qs.parse(str, qsOptions);
};
}
Loading