Headline
GHSA-6rw7-vpxm-498p: qs's arrayLimit bypass in its bracket notation allows DoS via memory exhaustion
Summary
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) orarrayLimit: 100(test 2)- Use bracket notation:
a[]=value(not indexeda[0]=value)
Impact
Denial of Service via memory exhaustion. Affects applications using qs.parse() with user-controlled input and arrayLimit for protection.
Attack scenario:
- Attacker sends HTTP request:
GET /api/search?filters[]=x&filters[]=x&...&filters[]=x(100,000+ times) - Application parses with
qs.parse(query, { arrayLimit: 100 }) - qs ignores limit, parses all 100,000 elements into array
- Server memory exhausted → application crashes or becomes unresponsive
- 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).
Summary
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:
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:
- Attacker sends HTTP request: GET /api/search?filters[]=x&filters[]=x&…&filters[]=x (100,000+ times)
- Application parses with qs.parse(query, { arrayLimit: 100 })
- qs ignores limit, parses all 100,000 elements into array
- Server memory exhausted → application crashes or becomes unresponsive
- 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).
References
- GHSA-6rw7-vpxm-498p
- https://nvd.nist.gov/vuln/detail/CVE-2025-15284
- ljharb/qs@3086902