Skip to content
Open
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
7 changes: 7 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
unreleased
==========

* Remove `app.set('query parser', 'extended')` option; users should pass a custom parser function instead
- To replicate old `'extended'` behavior: `app.set('query parser', str => require('qs').parse(str, { allowPrototypes: true }))`
- This allows removing `qs` from the dependencies for users who don't configure it explicitly

5.1.0 / 2025-03-31
========================

Expand Down
20 changes: 2 additions & 18 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ var contentType = require('content-type');
var etag = require('etag');
var mime = require('mime-types')
var proxyaddr = require('proxy-addr');
var qs = require('qs');
var querystring = require('node:querystring');
const { Buffer } = require('node:buffer');

Expand Down Expand Up @@ -154,7 +153,7 @@ exports.compileETag = function(val) {
/**
* Compile "query parser" value to function.
*
* @param {String|Function} val
* @param {String|Function|Boolean} val
* @return {Function}
* @api private
*/
Expand All @@ -174,8 +173,7 @@ exports.compileQueryParser = function compileQueryParser(val) {
case false:
break;
case 'extended':
fn = parseExtendedQueryString;
break;
throw new TypeError("query parser 'extended' is no longer supported; use: `app.set('query parser', str => qs.parse(str, { allowPrototypes: true }))` to replicate the old behavior");
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

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

The error message suggests using { allowPrototypes: true } but this option is a security risk. The old parseExtendedQueryString function used this option, but users migrating should be warned about the security implications or given a safer default option.

Suggested change
throw new TypeError("query parser 'extended' is no longer supported; use: `app.set('query parser', str => qs.parse(str, { allowPrototypes: true }))` to replicate the old behavior");
throw new TypeError("query parser 'extended' is no longer supported. To replicate the old behavior, you may use: `app.set('query parser', str => qs.parse(str, { allowPrototypes: true }))`, but WARNING: enabling `{ allowPrototypes: true }` is a security risk and can lead to prototype pollution vulnerabilities. Do not use this option in production unless you fully understand the risks. The default behavior is safer and recommended.");

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

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

ljharb/qs#321 => allowPrototypes: true is not a security issue

default:
throw new TypeError('unknown value for query parser function: ' + val);
}
Expand Down Expand Up @@ -255,17 +253,3 @@ function createETagGenerator (options) {
return etag(buf, options)
}
}

/**
* Parse an extended query string with qs.
*
* @param {String} str
* @return {Object}
* @private
*/

function parseExtendedQueryString(str) {
return qs.parse(str, {
allowPrototypes: true
});
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
Expand All @@ -76,6 +75,7 @@
"morgan": "1.10.1",
"nyc": "^17.1.0",
"pbkdf2-password": "1.2.1",
"qs": "^6.14.0",
"supertest": "^6.3.0",
"vhost": "~3.0.2"
},
Expand Down
15 changes: 12 additions & 3 deletions test/req.query.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

var assert = require('node:assert')
var qs = require('qs');
var express = require('../')
, request = require('supertest');

Expand All @@ -22,24 +23,32 @@ describe('req', function(){
.expect(200, '{"user[name]":"tj"}', done);
});

describe('when "query parser" is extended', function () {
describe('when "query parser" is set to qs (formerly "extended")', function () {
it('should parse complex keys', function (done) {
var app = createApp('extended');
var app = createApp(qs.parse);

request(app)
.get('/?foo[0][bar]=baz&foo[0][fizz]=buzz&foo[]=done!')
.expect(200, '{"foo":[{"bar":"baz","fizz":"buzz"},"done!"]}', done);
});

it('should parse parameters with dots', function (done) {
var app = createApp('extended');
var app = createApp(qs.parse);

request(app)
.get('/?user.name=tj')
.expect(200, '{"user.name":"tj"}', done);
});
});

describe('when "query parser" is set to extended', function () {
it('should throw with a helpful error message', function () {
assert.throws(() => createApp('extended'),
new TypeError("query parser 'extended' is no longer supported; use: `app.set('query parser', str => qs.parse(str, { allowPrototypes: true }))` to replicate the old behavior")
);
});
});

describe('when "query parser" is simple', function () {
it('should not parse complex keys', function (done) {
var app = createApp('simple');
Expand Down