fix: restore array parsing for req.query repeated keys (#7147)#7181
Conversation
krzysdz
left a comment
There was a problem hiding this comment.
Infinity completely removes protection from sending something like arr[999999]=x and allows creating large arrays by sending just 1 parameter with an index.
PR #7151 (to master) uses a value of 1000 (same as the parameter limit), which is a better idea, but it still relaxes the limit compared to qs 6.14.0.
) qs 6.14 enforces a default `arrayLimit` of 20, which caused the extended query parser to collapse query strings with more than 20 repeated values (e.g. `?ids=1&ids=2&...&ids=25`) into an object instead of an array. This regressed in 4.22.0 (with the bump to `qs@~6.14.1`) and broke existing consumers relying on the pre-6.14 behavior. Pass `arrayLimit: Infinity` to `qs.parse` in `parseExtendedQueryString` so repeated keys always produce arrays, matching the behavior of Express \<= 4.21.x. Added a regression test in `test/req.query.js` that sends 25 repeated values and asserts they round-trip as an array.
e8b025b to
c0b4eaa
Compare
|
Thanks @krzysdz — good catch. Updated to Also added a second regression test asserting that |
|
I pushed an update to drop refs to the specific issue (prefer commit history for that) and to drop the sparse array test which is not related to the regression. |
There was a problem hiding this comment.
For clarity, the specific root cause and knock on effect is:
qs's arrayLimit default of 20 was historically not enforced on repeated-key parsing.
qs 6.14.1 closed that gap, and Express 4.22.0 pulled the fix in via the qs@~6.14.1 bump (#6972) — so ?ids=1&ids=2&... past 20 values silently flips req.query.ids from array to object on default config.
This PR sets arrayLimit: 1000 in parseExtendedQueryString (matching parameterLimit) to restore the repeated-key behavior Express 4.x has historically supported.
Note on indexed-bracket notation: qs uses arrayLimit as a single knob across three parsing paths (repeated keys, empty-bracket arr[]=, and indexed-bracket arr[N]=). For indexed-bracket queries at indices 21..999, this PR widens the threshold past 4.21's effective limit of 20 — ?arr[500]=v will now parse as an array ["v"] rather than an object {"500":"v"}.
EDIT: see update and change suggestion #7181 (comment)
This is, to me, a surprising side effect. But I do think it is reasonable as my own experience expects that the repeated key form is more common. Open to input here though
|
it'd actually parse as an array where index 500 will be |
|
@ljharb this is how Im testing it: // express 4.21 baseline (qs 6.13.0, default arrayLimit:20)
qs.parse('arr[500]=v')
// { arr: { '500': 'v' } }
// Proposed fix (qs 6.14.2, arrayLimit:1000)
qs.parse('arr[500]=v', { arrayLimit: 1000 })
// { arr: [ 'v' ] }
|
|
ah right, you'd need the |
|
with |
|
Definitely. But if you treat it like an object with |
|
Right — that's the deciding factor. If the keyed access |
|
Closing — using |
There was a problem hiding this comment.
Reopened this, it is my preferred fix for the 4.x line, it is neat and tidy and fixes a regression in default settings that has been open too long.
PR restores the default extended req.query behavior 4.x had before, allowing up to 1000 array elements, by passing arrayLimit: 1000 when constructing the extended query parser.
There was a problem hiding this comment.
Objects suddenly becoming arrays should not be a big problem (arrays can be used as objects, JS being JS...).
The thing that concerns me is that someone may have relied on e.g. ?filter[1001]=abc being accessible as req.query.filter[1001]. While this has never worked for ?filter[5]=abc (req.query.filter would be ['abc']), I wouldn’t be surprised if someone has used hardcoded numbers > 20 as keys.
I think we can try releasing this as is and see if anyone reports new issues. With the allowSparse: true alternative, there would be a difference for indices <20 (small arrays suddenly are sparse and have empty elements) and surely someone would report this as a security issue (DoS).
EDIT: If this becomes an issue again, here's a small demonstration of the old behaviour and possible changes to arrayLimit and allowSparse: https://gist.github.com/krzysdz/35d7ce9962a08534226ba91d51530d7c#file-results-md
[//]: # (dependabot-start)⚠️ \*\*Dependabot is rebasing this PR\*\*⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [qs](https://github.com/ljharb/qs) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together. Updates `qs` from 6.14.2 to 6.15.2 Changelog *Sourced from [qs's changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md).* > **6.15.2** > ---------- > > * [Fix] `stringify`: skip null/undefined entries in `arrayFormat: 'comma'` + `encodeValuesOnly` instead of crashing in `encoder` > * [Fix] `stringify`: use configured `delimiter` after `charsetSentinel` ([#555](https://redirect.github.com/ljharb/qs/issues/555)) > * [Fix] `stringify`: apply `formatter` to encoded key under `strictNullHandling` ([#554](https://redirect.github.com/ljharb/qs/issues/554)) > * [Fix] `stringify`: skip null/undefined filter-array entries instead of crashing in `encoder` ([#551](https://redirect.github.com/ljharb/qs/issues/551)) > * [Fix] `parse`: handle nested bracket groups and add regression tests ([#530](https://redirect.github.com/ljharb/qs/issues/530)) > * [readme] fix grammar ([#550](https://redirect.github.com/ljharb/qs/issues/550)) > * [Dev Deps] update `@ljharb/eslint-config` > * [Tests] add regression tests for keys containing percent-encoded bracket text > > **6.15.1** > ---------- > > * [Fix] `parse`: `parameterLimit: Infinity` with `throwOnLimitExceeded: true` silently drops all parameters > * [Deps] update `@ljharb/eslint-config` > * [Dev Deps] update `@ljharb/eslint-config`, `iconv-lite` > * [Tests] increase coverage > > **6.15.0** > ---------- > > * [New] `parse`: add `strictMerge` option to wrap object/primitive conflicts in an array ([#425](https://redirect.github.com/ljharb/qs/issues/425), [#122](https://redirect.github.com/ljharb/qs/issues/122)) > * [Fix] `duplicates` option should not apply to bracket notation keys ([#514](https://redirect.github.com/ljharb/qs/issues/514)) Commits * [`9aca407`](ljharb/qs@9aca407) v6.15.2 * [`5e33d33`](ljharb/qs@5e33d33) [Dev Deps] update `@ljharb/eslint-config` * [`21f80b3`](ljharb/qs@21f80b3) [Fix] `stringify`: skip null/undefined entries in `arrayFormat: 'comma'` + `e... * [`a0a81ea`](ljharb/qs@a0a81ea) [Fix] `stringify`: use configured `delimiter` after `charsetSentinel` * [`e3062f7`](ljharb/qs@e3062f7) [Fix] `stringify`: apply `formatter` to encoded key under `strictNullHandling` * [`0c180a4`](ljharb/qs@0c180a4) [Fix] `stringify`: skip null/undefined filter-array entries instead of crashi... * [`3a8b94a`](ljharb/qs@3a8b94a) [Tests] add regression tests for keys containing percent-encoded bracket text * [`96755ab`](ljharb/qs@96755ab) [readme] fix grammar * [`a419ce5`](ljharb/qs@a419ce5) [Fix] `parse`: handle nested bracket groups and add regression tests * [`3f5e1c5`](ljharb/qs@3f5e1c5) v6.15.1 * Additional commits viewable in [compare view](ljharb/qs@v6.14.2...v6.15.2) Updates `express` from 4.22.1 to 4.22.2 Release notes *Sourced from [express's releases](https://github.com/expressjs/express/releases).* > v4.22.2 > ------- > > What's Changed > -------------- > > * fix: restore >20 array parsing for `req.query` repeated keys ([`8d09bfe6`](expressjs/express@8d09bfe)) > + This also unifies array-cap behavior across notations. Indexed notation (`a[0]=...`) was historically capped at qs's default `arrayLimit` of 20 even in older qs versions; after this change it also allows up to 1000 items. > * deps: qs@~6.15.1 > * deps: body-parser@~1.20.5 > > New Contributors > ---------------- > > * [`@suuuuuuminnnnnn`](https://github.com/suuuuuuminnnnnn) made their first contribution in [expressjs/express#7021](https://redirect.github.com/expressjs/express/pull/7021) > * [`@SAY-5`](https://github.com/SAY-5) made their first contribution in [expressjs/express#7181](https://redirect.github.com/expressjs/express/pull/7181) > > **Full Changelog**: <expressjs/express@v4.22.1...v4.22.2> Changelog *Sourced from [express's changelog](https://github.com/expressjs/express/blob/v4.22.2/History.md).* > 4.22.2 / 2026-05-011 > ==================== > > * fix: restore >20 array parsing for `req.query` repeated keys ([`8d09bfe6`](expressjs/express@8d09bfe)) > + This also unifies array-cap behavior across notations. Indexed notation (`a[0]=...`) was historically capped at qs's default `arrayLimit` of 20 even in older qs versions; after this change it also allows up to 1000 items. > * deps: qs@~6.15.1 > * deps: body-parser@~1.20.5 Commits * [`df0abc9`](expressjs/express@df0abc9) 4.22.2 * [`836d366`](expressjs/express@836d366) `4.x` update qs to 6.15.1, body-parser 1.20.5 ([#7224](https://redirect.github.com/expressjs/express/issues/7224)) * [`8d09bfe`](expressjs/express@8d09bfe) fix: restore array parsing for req.query repeated keys ([#7181](https://redirect.github.com/expressjs/express/issues/7181)) * [`d39e8ad`](expressjs/express@d39e8ad) deps: body-parser@~1.20.4 ([#7021](https://redirect.github.com/expressjs/express/issues/7021)) * [`efe85d9`](expressjs/express@efe85d9) deps: qs@^6.14.1 ([#6972](https://redirect.github.com/expressjs/express/issues/6972)) * [`f62378e`](expressjs/express@f62378e) 📝 add note to history * See full diff in [compare view](expressjs/express@v4.22.1...v4.22.2) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- Dependabot commands and options You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/robfrank/linklift/network/alerts).
Closes #7147.
The bump to
qs@~6.14.1in 4.22.0 (#6972) pulled in qs's new defaultarrayLimitof 20, which causes the extended query parser to silently collapse repeated-key query strings with more than 20 values into an object (e.g.{ "0": "a", "1": "b", ... }) instead of producing an array. This broke consumers that had been passing larger batches of repeated parameters throughreq.query.ids(the issue reporter hits it with ~20 tenancy IDs).body-parseralready got an opt-out path for the extended URL-encoded middleware, but theparseExtendedQueryStringused byreq.queryfor the'extended'query parser still only passesallowPrototypes: trueand otherwise inherits qs defaults — so the regression is only visible onreq.query, which is what #7147 reports.This PR restores the pre-6.14 behavior by passing
arrayLimit: InfinityarrayLimit: 1000toqs.parseinsideparseExtendedQueryString. Repeated-key values of any length round-trip back to an array, matching Express <= 4.21.x.Test plan
test/req.query.jsthat sends 25 repeatedidsvalues and assertsreq.query.idsis an array of 25 strings. Verified the test fails on4.xwithout the fix and passes with it.test/req.query.jssuite: 11 passing.If it would help, I'm happy to open a matching PR against
5.x— the sameparseExtendedQueryStringlives there with the same defaults.