Skip to content

Conversation

@backstage-goalie
Copy link
Contributor

This PR contains the following updates:

Package Change Age Confidence
qs 6.14.06.14.1 age confidence

qs's arrayLimit bypass in its bracket notation allows DoS via memory exhaustion

CVE-2025-15284 / GHSA-6rw7-vpxm-498p

More information

Details

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) 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).

Severity

  • CVSS Score: 8.7 / 10 (High)
  • Vector String: 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

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Release Notes

ljharb/qs (qs)

v6.14.1

Compare Source

  • [Fix] ensure arrayLength applies to [] notation as well
  • [Fix] parse: when a custom decoder returns null for a key, ignore that key
  • [Refactor] parse: extract key segment splitting helper
  • [meta] add threat model
  • [actions] add workflow permissions
  • [Tests] stringify: increase coverage
  • [Dev Deps] update eslint, @ljharb/eslint-config, npmignore, es-value-fixtures, for-each, object-inspect

Configuration

📅 Schedule: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about these updates again.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Renovate Bot.

@backstage-goalie
Copy link
Contributor Author

backstage-goalie bot commented Dec 31, 2025

Changed Packages

Package Name Package Path Changeset Bump Current Version
@backstage-community/plugin-rbac-backend workspaces/rbac/plugins/rbac-backend patch v7.6.0

@backstage-service backstage-service force-pushed the renovate/npm-qs-vulnerability branch from 79b5e08 to c076290 Compare December 31, 2025 06:42
@backstage-goalie backstage-goalie bot force-pushed the renovate/npm-qs-vulnerability branch from c076290 to ddba5d2 Compare December 31, 2025 13:52
@backstage-service backstage-service force-pushed the renovate/npm-qs-vulnerability branch from ddba5d2 to e851f27 Compare December 31, 2025 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants