Oct 27, 2026·10 min read·4 visits
Calling qs.stringify with arrayFormat: 'comma' on arrays containing objects without a callable toString method or null-prototype objects throws an unhandled TypeError: Cannot convert object to primitive value. In Node.js applications that re-serialize untrusted user input, this causes request failures or full process crashes. Upgrading qs to version 6.16.1 resolves the issue.
CVE-2026-110816 (GHSA-6pq8-jhm2-2vcw) is an uncaught exception and improper type conversion vulnerability in the Node.js query string parsing and serialization library qs. When invoking qs.stringify with the option arrayFormat set to 'comma', the library fails to safely handle array elements that cannot be coerced into primitive strings, such as objects with a non-callable toString property or null-prototype objects. An unauthenticated remote attacker can supply crafted input via JSON bodies or URL query parameters that, when re-serialized by the application, throws a synchronous TypeError, resulting in HTTP 500 errors or complete Node.js process termination.
The qs package is a widely deployed query string parsing and serialization library in the Node.js ecosystem. It serves as the foundational query string engine for major web frameworks, including Express 4, Express 5, and the body-parser middleware. Applications frequently use qs.parse to deserialize incoming HTTP query parameters or URL-encoded bodies, and qs.stringify to serialize JavaScript objects back into query strings for upstream API requests, redirects, caching layers, or logging.
CVE-2026-110816 (also tracked as GHSA-6pq8-jhm2-2vcw) is a Denial of Service vulnerability categorized under CWE-248 (Uncaught Exception) and CWE-704 (Incorrect Type Conversion or Cast). The flaw resides in the qs.stringify method when configured with the arrayFormat: 'comma' option. When serializing an array under this format, qs attempts to coerce array elements into primitive strings without verifying that the elements support standard ECMAScript string conversion.
If an attacker injects an object into an array that possesses a non-callable toString property (such as { "toString": "x" }) or is a null-prototype object (Object.create(null)), the underlying JavaScript engine throws a synchronous TypeError: Cannot convert object to primitive value. This vulnerability affects all versions of qs from 6.7.0 through 6.16.0.
Depending on the host application's error handling architecture, this uncaught synchronous exception either terminates the active HTTP request with a 500 Internal Server Error or crashes the entire Node.js runtime process. Applications using the default arrayFormat (indices), or the brackets and repeat formats, are unaffected because those code paths recursively traverse object properties rather than coercing the object directly to a primitive string.
To understand the root cause of CVE-2026-110816, it is necessary to examine how the V8 JavaScript engine executes the ECMAScript ToPrimitive(input, PreferredType: string) abstract operation. When an object is passed to String(obj) or joined via Array.prototype.join(','), the engine first checks for a callable [Symbol.toPrimitive] method. If absent, it looks up the object's toString property. If obj.toString is a callable function, the engine invokes it and returns the resulting primitive string.
However, if obj.toString is overridden with a non-callable value (for example, a string literal "x"), the engine skips toString and falls back to invoking obj.valueOf(). For standard JavaScript objects, Object.prototype.valueOf() returns a reference to the object itself—which is not a primitive value. Because neither method yields a primitive, the ECMAScript specification mandates throwing a runtime TypeError: Cannot convert object to primitive value. The exact same failure occurs on null-prototype objects (Object.create(null)) because neither toString nor valueOf exists on their prototype chain.
In qs versions 6.7.0 (introduced in commit b84acba) through 6.16.0, the arrayFormat: 'comma' execution branch in lib/stringify.js directly invokes obj.join(',') on the target array. Furthermore, starting in version 6.11.1 (commit 4c4b23d), when the encodeValuesOnly: true option is enabled, qs maps over the array elements and passes each item to utils.encode in lib/utils.js prior to joining. Inside utils.encode, any non-string, non-symbol value is coerced via String(str).
Neither obj.join(',') in lib/stringify.js nor String(str) in lib/utils.js guards against objects whose ToPrimitive coercion fails. Furthermore, because Array.prototype.join recursively converts nested arrays via Array.prototype.toString, nested structures such as { "a": [[{ "toString": "x" }]] } trigger the exact same exception path.
In the vulnerable versions of qs, lib/stringify.js handles the comma array format by mapping elements through an encoder (if encodeValuesOnly is true) and immediately joining the array elements with a comma delimiter. In lib/utils.js, the encode function forces non-string primitives and objects through the global String() constructor.
The official remediation in version 6.16.1 (commit a952e7519338ef0cfb25694a2988848f93333727) introduces a defensive helper function named toCoercible inside lib/utils.js. Both lib/stringify.js and lib/utils.js are updated to sanitize inputs through toCoercible before invoking .join(',') or String().
// lib/stringify.js - Patched comma array handling
if (arrayFormat === 'comma' && isArray(obj)) {
if (encodeValuesOnly && typeof encoder === 'function') {
obj = utils.maybeMap(obj, function (v) {
return v == null ? v : encoder(v);
});
}
// FIX: Wrap `obj` in `utils.toCoercible(obj)` before calling `.join(',')`
objKeys = [{ value: obj.length > 0 ? utils.toCoercible(obj).join(',') || null : void undefined }];
}Inside lib/utils.js, the toCoercible function inspects values before native coercion occurs. Primitives and objects implementing [Symbol.toPrimitive] or a valid callable toString method are returned unmodified. If toString is not a function, toCoercible attempts to call valueOf() directly; if valueOf() returns a primitive, it converts that primitive to a string. Otherwise, it falls back to Object.prototype.toString.call(value), which safely evaluates to "[object Object]".
// lib/utils.js - New toCoercible sanitization logic
var hasToPrimitive = typeof Symbol === 'function' && typeof Symbol.toPrimitive === 'symbol';
var arrayJoin = Array.prototype.join;
var arrayToString = Array.prototype.toString;
var objectToString = Object.prototype.toString;
var toCoercible = function toCoercible(value, joining) {
// 1. Primitives and null/undefined do not throw on coercion
if (!value || (typeof value !== 'object' && typeof value !== 'function')) {
return value;
}
// 2. Respect custom Symbol.toPrimitive implementations
if (hasToPrimitive && value[Symbol.toPrimitive] != null) {
return value;
}
// 3. Recursively walk nested arrays while tracking cycles via side-channel
if (isArray(value) && value.toString === arrayToString && value.join === arrayJoin) {
var ancestors = joining || getSideChannel();
if (ancestors.has(value)) {
return ''; // Render cyclical array references as empty string
}
ancestors.set(value, true);
var copy;
for (var i = 0; i < value.length; i += 1) {
var item = value[i];
var coerced = toCoercible(item, ancestors);
// Lazy copy-on-write only if an element required transformation
if (!copy && coerced !== item) {
copy = [];
for (var j = 0; j < i; j += 1) {
copy[j] = value[j];
}
}
if (copy) {
copy[i] = coerced;
}
}
ancestors['delete'](value);
return copy || value;
}
// 4. If toString is a valid function, native coercion will succeed
if (typeof value.toString === 'function') {
return value;
}
// 5. Fallback: try valueOf() if callable, otherwise force Object.prototype.toString
var valueOf = value.valueOf;
if (typeof valueOf === 'function') {
var primitive = valueOf.call(value);
if (!primitive || (typeof primitive !== 'object' && typeof primitive !== 'function')) {
return String(primitive);
}
}
return objectToString.call(value);
};The patch is comprehensive and addresses several subtle edge cases. By implementing recursive array traversal with a getSideChannel() ancestor map, toCoercible prevents infinite recursion on circular array references, matching native Array.prototype.join cycle behavior by returning ''. Additionally, it uses a copy-on-write pattern so arrays containing benign elements incur zero memory allocation overhead. Note that if an application intentionally passes an object with a callable toString or valueOf function that explicitly throws an exception inside its body, that exception will still propagate as expected.
Exploiting CVE-2026-110816 requires no authentication and relies on supplying structured input that deserializes into an array containing an uncoercible object, which the target application subsequently passes to qs.stringify(data, { arrayFormat: 'comma' }). While qs.parse drops prototype properties like toString under its default standalone options, the most common web framework integrations override this behavior by default.
Specifically, Express 4's default query parser, Express 5's extended query parser, and body-parser's urlencoded({ extended: true }) middleware all invoke qs.parse with { allowPrototypes: true }. When allowPrototypes: true is active, an attacker can send a URL query string or form body such as a[0][toString]=x. qs.parse constructs the array [{ toString: 'x' }], shadowing Object.prototype.toString with an own string property.
const qs = require('qs');
// Vector 1: Express 4 / Express 5 extended query parser configuration (allowPrototypes: true)
// Attacker sends HTTP GET: /endpoint?a[0][toString]=x
const parsedQuery = qs.parse('a[0][toString]=x', { allowPrototypes: true });
try {
qs.stringify(parsedQuery, { arrayFormat: 'comma' });
} catch (err) {
// Throws TypeError: Cannot convert object to primitive value
console.error(err);
}
// Vector 2: Standard JSON body parsing (no special qs.parse options required)
// Attacker sends HTTP POST with Content-Type: application/json
const parsedJson = JSON.parse('{"a":[{"toString":"x"}]}');
try {
qs.stringify(parsedJson, { arrayFormat: 'comma' });
} catch (err) {
// Throws TypeError: Cannot convert object to primitive value
console.error(err);
}
// Vector 3: Null-prototype objects generated via plainObjects: true
// Attacker sends HTTP GET: /endpoint?a[][b]=c
const plainParsed = qs.parse('a[][b]=c', { plainObjects: true });
try {
qs.stringify(plainParsed, { arrayFormat: 'comma', encodeValuesOnly: true });
} catch (err) {
// Throws TypeError: Cannot convert object to primitive value inside utils.encode
console.error(err);
}As shown in the reproduction vectors above, an attacker does not even need allowPrototypes: true or plainObjects: true if the endpoint accepts JSON payloads. Standard JSON.parse('{"a":[{"toString":"x"}]}') natively assigns toString as an own property on the resulting object. When the server forwards or normalizes this payload using qs.stringify with arrayFormat: 'comma', the exception triggers immediately.
CVE-2026-110816 carries a CVSS v3.1 base score of 5.3 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L) and a CVSS v4.0 vector of 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. The vulnerability does not allow confidentiality breaches or memory corruption, as the impact is strictly confined to service availability.
The operational severity of the availability impact depends heavily on the execution context within the Node.js application. When qs.stringify is called synchronously inside a standard Express route handler, the framework's built-in error boundary catches the TypeError and responds to the client with an HTTP 500 error. In this scenario, the Node.js process survives and continues serving other clients, limiting the impact to per-request failures.
> [!WARNING]
> In Express 4 applications, unhandled synchronous exceptions thrown inside async route handlers or unawaited Promise chains are not caught by the default router error boundary. This results in an unhandledRejection event, which terminates the entire Node.js process in modern Node.js runtimes.
Similarly, if qs.stringify is invoked inside a bare http.createServer request listener, an asynchronous timer callback (setTimeout / setImmediate), or background worker thread without an explicit try...catch block, the uncaught exception immediately crashes the Node.js process. An unauthenticated attacker can repeatedly send a single lightweight HTTP request to keep the application offline in a continuous crash loop.
Threat intelligence metrics currently indicate an EPSS score of 0.00368 (30.52nd percentile), and the vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.
The primary remediation is to upgrade the qs dependency to version 6.16.1 or later. Version 6.16.1 incorporates the toCoercible check in lib/utils.js and lib/stringify.js, ensuring that objects lacking a callable toString method or prototype chain are safely serialized as "[object Object]" without throwing runtime exceptions.
# Upgrade qs to the patched release
npm install qs@^6.16.1If qs is pulled in as a transitive dependency through frameworks such as Express or body-parser, administrators should use npm ls qs to inspect the dependency tree and apply an overrides entry in package.json (or resolutions in Yarn) to force resolution to >=6.16.1.
For environments where immediate package upgrades are not feasible, development teams can apply several workarounds:
arrayFormat: 'comma'. Switching to the default indices format (a[0]=1&a[1]=2), brackets (a[]=1&a[]=2), or repeat (a=1&a=2) bypasses the vulnerable array joining code path entirely.filter function to qs.stringify to inspect and sanitize non-coercible objects prior to serialization:const safeStringify = (payload) => {
return qs.stringify(payload, {
arrayFormat: 'comma',
filter: (prefix, value) => {
if (typeof value === 'object' && value !== null && typeof value.toString !== 'function') {
return Object.prototype.toString.call(value);
}
return value;
}
});
};allowPrototypes in query parsers where prototype property access is not explicitly required, and ensure all qs.stringify calls inside asynchronous handlers are wrapped in try...catch blocks.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
qs ljharb | >= 6.7.0 < 6.16.1 | 6.16.1 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | CWE-248 (Uncaught Exception) / CWE-704 (Incorrect Type Conversion) |
| Affected Component | qs (npm package) >= 6.7.0, < 6.16.1 |
| Attack Vector | Network (Unauthenticated HTTP requests with crafted query or JSON parameters) |
| CVSS v3.1 Score | 5.3 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L) |
| EPSS Score | 0.37% (30.52nd percentile) |
| Security Impact | Denial of Service (HTTP 500 errors or Node.js process crash) |
| Exploit Status | Proof-of-Concept publicly available in advisory |
| CISA KEV Status | Not Listed |
The software does not catch or properly handle an exception that can occur during normal or exceptional operation, which may lead to a denial of service or unexpected program termination.
A high-severity target blocklist omission in the hydra-core Python package (versions 1.3.6 through 1.3.7 and 1.4.0.dev9 through 1.4.0rc2) allows arbitrary code execution when instantiating untrusted configuration dictionaries. Because the execution policy failed to restrict the PyYAML (yaml.) namespace, an attacker can specify functions such as yaml.unsafe_load as the instantiation target and supply a crafted YAML stream containing Python object construction tags to execute arbitrary system commands within the host process.
Authlib versions prior to 1.9.0 contain a Server-Side Request Forgery (SSRF) vulnerability in the RFC 9101 JWT-Secured Authorization Request (JAR) server implementation. The JWTAuthenticationRequest class passes an unauthenticated, client-controlled request_uri parameter directly to the abstract get_request_object() method before applying any destination validation or client registration policies. Because the official documentation previously recommended implementing this hook as an unrestricted HTTP fetch, deployments following those guidelines allow remote attackers possessing a valid public client_id to trigger outbound network requests to internal services and infer fetch outcomes via error response variations.
CVE-2026-109956 (GHSA-j456-xh4h-cpf2) is a path traversal vulnerability and security sandbox bypass in the Python Natural Language Toolkit (NLTK) versions 3.10.3 and earlier. The WekaClassifier module fails to validate caller-controlled model file paths before passing them to the Weka Java Virtual Machine (JVM) via command-line arguments. When operating under the nltk.pathsec security sandbox, this omission permits attackers who control model path parameters to read, probe, or overwrite arbitrary files outside authorized NLTK data directories.
CVE-2026-35002 is an arbitrary code execution vulnerability in the Agno agent platform (formerly Phidata) affecting all versions prior to 2.3.24. The flaw resides in the model execution logic (libs/agno/agno/models/base.py) and tool serialization logic (libs/agno/agno/tools/function.py), where the application directly passes the field_type string parameter into Python's built-in eval() function to resolve string type names into Python data types. An attacker who can influence FunctionCall arguments via prompt injection or supply untrusted dictionary structures for deserialization can execute arbitrary Python expressions within the context of the host process.
CVE-2026-115895 is a high-severity code injection vulnerability (CWE-94) in the happy-dom npm package prior to version 21.0.4. The vulnerability exists within the ECMAScriptModuleCompiler component, which transforms ECMAScript module syntax into executable Node.js functions when JavaScript evaluation is enabled. Due to an incomplete fix for CVE-2026-33943, the compiler failed to validate local named import aliases against legal JavaScript identifier rules. Attackers supplying crafted module imports—specifically utilizing destructuring default initializers alongside JSON module attributes—can inject arbitrary JavaScript expressions directly into the host Node.js runtime context.
An improper input validation and authorization bypass vulnerability exists in the LangGraph storage layer affecting langgraph, langgraph-checkpoint-postgres, and langgraph-checkpoint-sqlite. Because relational store implementations serialize namespace tuples into dot-delimited strings without validating labels on read, search, delete, and batch operations, an attacker who controls a namespace label can inject periods (.) to traverse namespace boundaries and access, modify, or delete data belonging to other namespaces.