qs 6.7.4 → 6.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/qs.js +2 -2
- package/lib/parse.js +1 -1
- package/lib/utils.js +1 -1
- package/package.json +13 -4
- package/test/parse.js +9 -0
- package/test/stringify.js +6 -0
- package/.claude/backport-record.md +0 -79
- package/.claude/ghsa-2856-comment.md +0 -7
- package/.claude/ghsa-6rw7-update.md +0 -53
- package/.claude/ghsa-w7fw-comment.md +0 -10
- package/.claude/settings.local.json +0 -36
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## **6.7.5**
|
|
2
|
+
- [Fix] fix regressions from robustness refactor
|
|
3
|
+
- [meta] add `npmignore` to autogenerate an npmignore file
|
|
4
|
+
- [actions] update reusable workflows
|
|
5
|
+
|
|
1
6
|
## **6.7.4**
|
|
2
7
|
- [Robustness] avoid `.push`, use `void`
|
|
3
8
|
- [readme] clarify `parseArrays` and `arrayLimit` documentation (#543)
|
package/dist/qs.js
CHANGED
|
@@ -247,7 +247,7 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesPars
|
|
|
247
247
|
// If there's a remainder, just add whatever is left
|
|
248
248
|
|
|
249
249
|
if (segment) {
|
|
250
|
-
keys[keys.length] = '[' + key.slice(segment.index + ']'
|
|
250
|
+
keys[keys.length] = '[' + key.slice(segment.index) + ']';
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
return parseObject(keys, val, options, valuesParsed);
|
|
@@ -600,7 +600,7 @@ var isArray = Array.isArray;
|
|
|
600
600
|
var hexTable = (function () {
|
|
601
601
|
var array = [];
|
|
602
602
|
for (var i = 0; i < 256; ++i) {
|
|
603
|
-
array[array.length] = '%' + ((i < 16 ? '0' : '' + i.toString(16)).toUpperCase()
|
|
603
|
+
array[array.length] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
|
|
604
604
|
}
|
|
605
605
|
|
|
606
606
|
return array;
|
package/lib/parse.js
CHANGED
|
@@ -205,7 +205,7 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesPars
|
|
|
205
205
|
// If there's a remainder, just add whatever is left
|
|
206
206
|
|
|
207
207
|
if (segment) {
|
|
208
|
-
keys[keys.length] = '[' + key.slice(segment.index + ']'
|
|
208
|
+
keys[keys.length] = '[' + key.slice(segment.index) + ']';
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
return parseObject(keys, val, options, valuesParsed);
|
package/lib/utils.js
CHANGED
|
@@ -6,7 +6,7 @@ var isArray = Array.isArray;
|
|
|
6
6
|
var hexTable = (function () {
|
|
7
7
|
var array = [];
|
|
8
8
|
for (var i = 0; i < 256; ++i) {
|
|
9
|
-
array[array.length] = '%' + ((i < 16 ? '0' : '' + i.toString(16)).toUpperCase()
|
|
9
|
+
array[array.length] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
return array;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "qs",
|
|
3
3
|
"description": "A querystring parser that supports nesting and arrays, with a depth limit",
|
|
4
4
|
"homepage": "https://github.com/ljharb/qs",
|
|
5
|
-
"version": "6.7.
|
|
5
|
+
"version": "6.7.5",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/ljharb/qs.git"
|
|
@@ -45,10 +45,11 @@
|
|
|
45
45
|
"qs-iconv": "^1.0.4",
|
|
46
46
|
"safe-publish-latest": "^2.0.0",
|
|
47
47
|
"safer-buffer": "^2.1.2",
|
|
48
|
-
"tape": "^5.4.0"
|
|
48
|
+
"tape": "^5.4.0",
|
|
49
|
+
"npmignore": "^0.3.1"
|
|
49
50
|
},
|
|
50
51
|
"scripts": {
|
|
51
|
-
"prepublishOnly": "safe-publish-latest && npm run dist",
|
|
52
|
+
"prepublishOnly": "safe-publish-latest && npmignore --auto --commentLines=autogenerated && npm run dist",
|
|
52
53
|
"prepublish": "not-in-publish || npm run prepublishOnly",
|
|
53
54
|
"pretest": "npm run --silent readme && npm run --silent lint",
|
|
54
55
|
"test": "npm run --silent tests-only",
|
|
@@ -59,5 +60,13 @@
|
|
|
59
60
|
"lint": "eslint lib/*.js test/*.js",
|
|
60
61
|
"dist": "mkdirp dist && browserify --standalone Qs lib/index.js > dist/qs.js"
|
|
61
62
|
},
|
|
62
|
-
"license": "BSD-3-Clause"
|
|
63
|
+
"license": "BSD-3-Clause",
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"ignore": [
|
|
66
|
+
"!dist/*",
|
|
67
|
+
"bower.json",
|
|
68
|
+
"component.json",
|
|
69
|
+
".github/workflows"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
63
72
|
}
|
package/test/parse.js
CHANGED
|
@@ -84,6 +84,15 @@ test('parse()', function (t) {
|
|
|
84
84
|
st.end();
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
t.test('correctly computes the remainder when depth is exceeded', function (st) {
|
|
88
|
+
st.deepEqual(
|
|
89
|
+
qs.parse('a[b][c][d][e]=f', { depth: 2 }),
|
|
90
|
+
{ a: { b: { c: { '[d][e]': 'f' } } } },
|
|
91
|
+
'the remainder is "[d][e]", not the full original key'
|
|
92
|
+
);
|
|
93
|
+
st.end();
|
|
94
|
+
});
|
|
95
|
+
|
|
87
96
|
t.test('current behavior with depth = 0', function (st) {
|
|
88
97
|
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { depth: 0 }), { a: { '[0]': 'b', '[1]': 'c' } });
|
|
89
98
|
st.deepEqual(qs.parse('a[0][0]=b&a[0][1]=c&a[1]=d&e=2', { depth: 0 }), { a: { '[0][0]': 'b', '[0][1]': 'c', '[1]': 'd' }, e: '2' });
|
package/test/stringify.js
CHANGED
|
@@ -19,6 +19,12 @@ test('stringify()', function (t) {
|
|
|
19
19
|
st.end();
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
t.test('correctly encodes low-byte characters', function (st) {
|
|
23
|
+
st.equal(qs.stringify({ a: String.fromCharCode(1) }), 'a=%01', 'encodes 0x01');
|
|
24
|
+
st.equal(qs.stringify({ a: String.fromCharCode(15) }), 'a=%0F', 'encodes 0x0F');
|
|
25
|
+
st.end();
|
|
26
|
+
});
|
|
27
|
+
|
|
22
28
|
t.test('stringifies falsy values', function (st) {
|
|
23
29
|
st.equal(qs.stringify(undefined), '');
|
|
24
30
|
st.equal(qs.stringify(null), '');
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# Backport Record
|
|
2
|
-
|
|
3
|
-
Commits to backport from main (v6.14.1..v6.14.2, excluding version bump):
|
|
4
|
-
|
|
5
|
-
| # | SHA | Description |
|
|
6
|
-
|---|-----|-------------|
|
|
7
|
-
| 1 | 6744d30 | [Robustness] avoid `.push`, use `void` |
|
|
8
|
-
| 2 | 6bdfaf5 | [readme] replace runkit/travis CI badge with shields.io check-runs badge |
|
|
9
|
-
| 3 | 2a35775 | [meta] fix changelog typo |
|
|
10
|
-
| 4 | 1b9a8b4 | [actions] fix rebase workflow permissions |
|
|
11
|
-
| 5 | fbc5206 | [Fix] `parse`: fix error message |
|
|
12
|
-
| 6 | f6a7abf | [Fix] `parse`: enforce arrayLimit on comma-parsed values |
|
|
13
|
-
| 7 | febb644 | [Fix] `parse`: throw on arrayLimit exceeded with indexed notation |
|
|
14
|
-
| 8 | cfc108f | [Fix] arrayLimit means max count, not max index |
|
|
15
|
-
| 9 | 6addf8c | [Fix] `parse`: mark overflow objects for indexed notation |
|
|
16
|
-
| 10 | 5c308e5 | [readme] clarify parseArrays and arrayLimit documentation |
|
|
17
|
-
| 11 | 294db90 | [readme] document addQueryPrefix does not add ? to empty output |
|
|
18
|
-
|
|
19
|
-
## Results
|
|
20
|
-
|
|
21
|
-
| Version | Base Tag | Base SHA | Result HEAD | Applied | Skipped |
|
|
22
|
-
|---------|----------|----------|-------------|---------|---------|
|
|
23
|
-
| 6.14 | v6.14.2 | bdcf0c7 | bdcf0c7 | all | — |
|
|
24
|
-
| 6.13 | v6.13.1 | f1ee037 | 625fa19 | 1,2,4,10,11 | 3,5,6,7,8,9 |
|
|
25
|
-
| 6.12 | v6.12.3 | f90cc35 | 8a1b294 | 1,2,4,10,11 | 3,5,6,7,8,9 |
|
|
26
|
-
| 6.11 | v6.11.2 | 410bdd3 | 3a5f714 | 1,2,4,10,11 | 3,5,6,7,8,9 |
|
|
27
|
-
| 6.10 | v6.10.5 | 95bc018 | 8189da8 | 1,2,4,10,11 | 3,5,6,7,8,9 |
|
|
28
|
-
| 6.9 | v6.9.7 | 4cd0032 | 6a7a3bf | 1,2,4,10,11 | 3,5,6,7,8,9 |
|
|
29
|
-
| 6.8 | v6.8.3 | 0db5538 | 76d53e9 | 1,2,4,10,11 | 3,5,6,7,8,9 |
|
|
30
|
-
| 6.7 | v6.7.3 | 834389a | 2991d8b | 1,2,4,10,11 | 3,5,6,7,8,9 |
|
|
31
|
-
| 6.6 | v6.6.1 | 4cc653c | 7093153 | 1,2,4,10,11 | 3,5,6,7,8,9 |
|
|
32
|
-
| 6.5 | v6.5.3 | 298bfa5 | 40b77c3 | 1,2,4,10,11 | 3,5,6,7,8,9 |
|
|
33
|
-
| 6.4 | v6.4.1 | 486aa46 | d9ffe2b | 1,2,4,10 | 3,5,6,7,8,9,11 |
|
|
34
|
-
| 6.3 | v6.3.3 | ff235b4 | b4824cb | 1,2,4,10 | 3,5,6,7,8,9,11 |
|
|
35
|
-
| 6.2 | v6.2.4 | 90d9f2b | db7b937 | 1,2,4,10 | 3,5,6,7,8,9,11 |
|
|
36
|
-
| 6.1 | v6.1.2 | 68ca039 | 3245e1f | 1,2,10 | 3,4,5,6,7,8,9,11 |
|
|
37
|
-
| 6.0 | v6.0.4 | 10233c9 | b779be4 | 1,2,10 | 3,4,5,6,7,8,9,11 |
|
|
38
|
-
|
|
39
|
-
## Legend for Applied/Skipped columns
|
|
40
|
-
|
|
41
|
-
1=Robustness, 2=readme badge, 3=changelog typo, 4=actions permissions,
|
|
42
|
-
5=error msg fix, 6=comma arrayLimit, 7=indexed throw, 8=count semantics,
|
|
43
|
-
9=mark overflow, 10=parseArrays/arrayLimit docs, 11=addQueryPrefix docs
|
|
44
|
-
|
|
45
|
-
## Notes
|
|
46
|
-
|
|
47
|
-
- #2 (badge): v6.4+ had runkit badge replaced; v6.0-v6.3 had Travis badge replaced
|
|
48
|
-
- #4 (actions): v6.2-v6.10 had permissions block ADDED (not modified); v6.0-v6.1 have no rebase.yml
|
|
49
|
-
|
|
50
|
-
## Skip Reasons
|
|
51
|
-
|
|
52
|
-
| Commit | Reason for skip |
|
|
53
|
-
|--------|----------------|
|
|
54
|
-
| 3 (2a35775) | v6.14.1-specific changelog — not applicable to other versions |
|
|
55
|
-
| 5 (fbc5206) | Requires `throwOnLimitExceeded` option (v6.14.0+) |
|
|
56
|
-
| 6 (f6a7abf) | Requires overflow system (`combine(a,b,arrayLimit,plainObjects)`) from v6.14.1 |
|
|
57
|
-
| 7 (febb644) | Requires `throwOnLimitExceeded` option (v6.14.0+) |
|
|
58
|
-
| 8 (cfc108f) | Requires overflow system from v6.14.1 |
|
|
59
|
-
| 9 (6addf8c) | Requires overflow system from v6.14.1 |
|
|
60
|
-
| 11 (294db90) | v6.4 and below lack `addQueryPrefix` in README |
|
|
61
|
-
|
|
62
|
-
## Release Tags
|
|
63
|
-
|
|
64
|
-
| Tag | SHA | Changelog Entries |
|
|
65
|
-
|-----|-----|-------------------|
|
|
66
|
-
| v6.13.2 | d8a8ab3 | Robustness, readme #543, readme #418, readme badge, actions |
|
|
67
|
-
| v6.12.4 | a67173e | Robustness, readme #543, readme #418, readme badge, actions |
|
|
68
|
-
| v6.11.3 | 6302f35 | Robustness, readme #543, readme #418, readme badge, actions |
|
|
69
|
-
| v6.10.6 | 1aa4bd9 | Robustness, readme #543, readme #418, readme badge, actions |
|
|
70
|
-
| v6.9.8 | 479d4b1 | Robustness, readme #543, readme #418, readme badge, actions |
|
|
71
|
-
| v6.8.4 | 0f2b1e2 | Robustness, readme #543, readme #418, readme badge, actions |
|
|
72
|
-
| v6.7.4 | d38e43a | Robustness, readme #543, readme #418, readme badge, actions |
|
|
73
|
-
| v6.6.2 | 5e1c72c | Robustness, readme #543, readme #418, readme badge, actions |
|
|
74
|
-
| v6.5.4 | c190488 | Robustness, readme #543, readme #418, readme badge, actions |
|
|
75
|
-
| v6.4.2 | 9b50144 | Robustness, readme #543, readme badge (runkit+travis), actions |
|
|
76
|
-
| v6.3.4 | aa3f9f4 | Robustness, readme #543, readme badge (travis), actions |
|
|
77
|
-
| v6.2.5 | d68f354 | Robustness, readme #543, readme badge (travis), actions |
|
|
78
|
-
| v6.1.3 | cb45ba1 | Robustness, readme #543, readme badge (travis) |
|
|
79
|
-
| v6.0.5 | a73f299 | Robustness, readme #543, readme badge (travis) |
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# GHSA-2856-wc6q-8xwc — Comment to post
|
|
2
|
-
|
|
3
|
-
This is not a vulnerability. The described behavior only modifies properties on the **result object** returned by `qs.parse()` — it does not modify `Object.prototype` or any shared state. Setting `toString` or `hasOwnProperty` on a specific result object is not prototype pollution; it's the expected outcome of parsing a key with that name.
|
|
4
|
-
|
|
5
|
-
The `allowPrototypes` option exists precisely to control whether keys that shadow `Object.prototype` properties are allowed on result objects. When `depth <= 0` and brackets are present, the prototype check is applied to the bracketed form of the key rather than the stripped form — this is a minor inconsistency in the check, but the result is equivalent to what would happen with `allowPrototypes: true`, which is an explicitly supported configuration.
|
|
6
|
-
|
|
7
|
-
Closing as not applicable.
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# GHSA-6rw7-vpxm-498p — Edits to make
|
|
2
|
-
|
|
3
|
-
## Severity
|
|
4
|
-
Change: High → Low
|
|
5
|
-
|
|
6
|
-
## CVSS 3.1
|
|
7
|
-
Old: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` (7.5 High)
|
|
8
|
-
New: `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L` (3.7 Low)
|
|
9
|
-
|
|
10
|
-
Changes:
|
|
11
|
-
- AC: L → H (requires non-default parameterLimit config for real impact)
|
|
12
|
-
- A: H → L (bounded memory increase, not a crash/full DoS under defaults)
|
|
13
|
-
|
|
14
|
-
## Description — replace with:
|
|
15
|
-
|
|
16
|
-
### Summary
|
|
17
|
-
|
|
18
|
-
The `arrayLimit` option in qs did not enforce limits for bracket notation (`a[]=1&a[]=2`), only for indexed notation (`a[0]=1`). This is a consistency bug; `arrayLimit` should apply uniformly across all array notations.
|
|
19
|
-
|
|
20
|
-
**Note:** The default `parameterLimit` of 1000 effectively mitigates the DoS scenario originally described. With default options, bracket notation cannot produce arrays larger than `parameterLimit` regardless of `arrayLimit`, because each `a[]=value` consumes one parameter slot. The severity has been reduced accordingly.
|
|
21
|
-
|
|
22
|
-
### Details
|
|
23
|
-
|
|
24
|
-
The `arrayLimit` option only checked limits for indexed notation (`a[0]=1&a[1]=2`) but did not enforce it for bracket notation (`a[]=1&a[]=2`).
|
|
25
|
-
|
|
26
|
-
**Vulnerable code** (`lib/parse.js:159-162`):
|
|
27
|
-
```javascript
|
|
28
|
-
if (root === '[]' && options.parseArrays) {
|
|
29
|
-
obj = utils.combine([], leaf); // No arrayLimit check
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
**Working code** (`lib/parse.js:175`):
|
|
34
|
-
```javascript
|
|
35
|
-
else if (index <= options.arrayLimit) { // Limit checked here
|
|
36
|
-
obj = [];
|
|
37
|
-
obj[index] = leaf;
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### PoC
|
|
42
|
-
|
|
43
|
-
```javascript
|
|
44
|
-
const qs = require('qs');
|
|
45
|
-
const result = qs.parse('a[]=1&a[]=2&a[]=3&a[]=4&a[]=5&a[]=6', { arrayLimit: 5 });
|
|
46
|
-
console.log(result.a.length); // Output: 6 (should be max 5)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**Note on parameterLimit interaction:** The original advisory's "DoS demonstration" claimed a length of 10,000, but `parameterLimit` (default: 1000) caps parsing to 1,000 parameters. With default options, the actual output is 1,000, not 10,000.
|
|
50
|
-
|
|
51
|
-
### Impact
|
|
52
|
-
|
|
53
|
-
Consistency bug in `arrayLimit` enforcement. With default `parameterLimit`, the practical DoS risk is negligible since `parameterLimit` already caps the total number of parsed parameters (and thus array elements from bracket notation). The risk increases only when `parameterLimit` is explicitly set to a very high value.
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# GHSA-w7fw-mjwx-w883 — Comment to post
|
|
2
|
-
|
|
3
|
-
This is a valid report — thank you. The comma-split code path in `parseArrayValue` returned the split array immediately, bypassing the `arrayLimit` check. Unlike the bracket notation case (GHSA-6rw7-vpxm-498p), this genuinely bypasses `parameterLimit` as well, since a single parameter like `a=x,x,x,...` produces an arbitrarily large array from one `&`-delimited parameter.
|
|
4
|
-
|
|
5
|
-
A fix has been implemented: after the comma split and decode step, if the resulting array exceeds `arrayLimit`, it is converted to an object (consistent with other `arrayLimit` exceedances), and `throwOnLimitExceeded` is respected.
|
|
6
|
-
|
|
7
|
-
A few corrections to the advisory:
|
|
8
|
-
- **Severity should be Medium, not High**: This requires the non-default `comma: true` option to be explicitly enabled. HTTP server request size limits also bound the practical impact.
|
|
9
|
-
- **Vulnerable version range**: Should be `<= 6.14.1` (not `< 6.14.1`), since 6.14.1 is also affected.
|
|
10
|
-
- **Patched version**: Will be set once the fix is released.
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm run lint:*)",
|
|
5
|
-
"Bash(npm test:*)",
|
|
6
|
-
"Bash(npm run tests-only:*)",
|
|
7
|
-
"Bash(npm show:*)",
|
|
8
|
-
"Bash(git log:*)",
|
|
9
|
-
"Bash(git tag:*)",
|
|
10
|
-
"Bash(npm view:*)",
|
|
11
|
-
"Bash(git fetch:*)",
|
|
12
|
-
"Bash(git cat-file:*)",
|
|
13
|
-
"Bash(git show:*)",
|
|
14
|
-
"Bash(git rev-parse:*)",
|
|
15
|
-
"Bash(git rev-list:*)",
|
|
16
|
-
"Bash(gh api:*)",
|
|
17
|
-
"Bash(node /tmp/qs-vuln-test.js:*)",
|
|
18
|
-
"Bash(git -C /Users/ljharb/git/ljharb/qs.git show 3086902 --stat)",
|
|
19
|
-
"Bash(git -C /Users/ljharb/git/ljharb/qs.git diff HEAD -- lib/parse.js lib/utils.js)",
|
|
20
|
-
"Bash(open:*)",
|
|
21
|
-
"WebSearch",
|
|
22
|
-
"WebFetch(domain:www.first.org)",
|
|
23
|
-
"WebFetch(domain:nvd.nist.gov)",
|
|
24
|
-
"WebFetch(domain:raw.githubusercontent.com)",
|
|
25
|
-
"Bash(git -C /Users/ljharb/git/ljharb/qs.git diff origin/main...HEAD)",
|
|
26
|
-
"Bash(for ver in v6.13.1 v6.12.3 v6.11.2 v6.10.5 v6.9.7 v6.8.3 v6.7.3 v6.6.1 v6.5.3 v6.4.1 v6.3.3 v6.2.4 v6.1.2 v6.0.4)",
|
|
27
|
-
"Bash(do echo '=== $ver ===')",
|
|
28
|
-
"Bash(done)",
|
|
29
|
-
"Bash(git checkout:*)",
|
|
30
|
-
"Bash(cd /Users/ljharb/git/ljharb/qs.git git add README.md git commit --no-verify --no-gpg-sign -m \"[readme] replace travis CI badge with shields.io check-runs badge\")",
|
|
31
|
-
"Bash(npm run dist:*)",
|
|
32
|
-
"Bash(for tag in v6.13.2 v6.12.4 v6.11.3 v6.10.6 v6.9.8 v6.8.4 v6.7.4 v6.6.2 v6.5.4 v6.4.2 v6.3.4 v6.2.5 v6.1.3 v6.0.5)",
|
|
33
|
-
"Bash(gh run view:*)"
|
|
34
|
-
]
|
|
35
|
-
}
|
|
36
|
-
}
|