DEV Community

CVE Reports
CVE Reports

Posted on Originally published at cvereports.com

CVE-2026-116580: CVE-2026-116580: Unvalidated Method Invocation via Identifier Reuse in Seroval Deserializer

CVE-2026-116580: Unvalidated Method Invocation via Identifier Reuse in Seroval Deserializer

Vulnerability ID: CVE-2026-116580
CVSS Score: 7.3
Published: 2026-10-28

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.

TL;DR

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.


⚠️ Exploit Status: POC

Technical Details

  • 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

Affected Systems

  • seroval
  • seroval-plugins
  • seroval: < 1.6.8 (Fixed in: 1.6.8)
  • seroval-plugins: < 1.6.8 (Fixed in: 1.6.8)

Code Analysis

Commit: 3edc78b

fix: use internal classes for stream and sequence identity

Commit: 50926b3

fix: check iterator source values instead of node ids

diff --git a/.changeset/iterator-source-identity.md b/.changeset/iterator-source-identity.md
new file mode 100644
index 00000000..ba2f0324
--- /dev/null
+++ b/.changeset/iterator-source-identity.md
@@ -0,0 +1,5 @@
+---
+"seroval": patch
+---
+
+Check that an iterator or async iterator's source is a genuine sequence or stream during `fromJSON` deserialization. Previously the check used the type recorded for the source's id, and an id missing from `m` could be reused to pass a plain object as the source.
diff --git a/packages/seroval/src/core/context/deserializer.ts b/packages/seroval/src/core/context/deserializer.ts
index 8be0e719..2282ca56 100644
--- a/packages/seroval/src/core/context/deserializer.ts
+++ b/packages/seroval/src/core/context/deserializer.ts
@@ -26,11 +26,12 @@ import { SerovalMode } from '../plugin';
 import { getReference } from '../reference';
 import {
   createSequence,
+  isSequence,
   type Sequence,
   sequenceToIterator,
 } from '../sequence';
 import type { Stream } from '../stream';
-import { createStream, streamToAsyncIterable } from '../stream';
+import { createStream, isStream, streamToAsyncIterable } from '../stream';
 import { deserializeString } from '../string';
 import type {
   SerovalAggregateErrorNode,
@@ -736,15 +737,13 @@ function deserializeIteratorFactoryInstance(
 ): unknown {
   deserialize(ctx, depth, node.a[0]);
   const source = deserialize(ctx, depth, node.a[1]);
-  // `node.a[1]` is any node the input picked. Validate the source *node type*,
-  // not the runtime value's shape: a `__SEROVAL_SEQUENCE__` brand is forgeable
-  // by a plugin, but only a genuine Sequence node carries the Sequence mark,
-  // and `guardIndexedValue` forbids reusing its id for another value.
-  validateNodeType(ctx, node, node.a[1].i, SerovalNodeType.Sequence);
-  if (!source) {
+  // `node.a[1]` is any node the input picked. Check the value itself rather
+  // than the type recorded for its id: in vanilla mode an unmarked id can be
+  // reused, so a plain object could sit behind an id marked as a Sequence.
+  if (!isSequence(source)) {
     throw new SerovalMalformedNodeError(node.a[1]);
   }
-  return sequenceToIterator(source as Sequence);
+  return sequenceToIterator(source);
 }

 function deserializeAsyncIteratorFactoryInstance(
@@ -754,16 +753,14 @@ function deserializeAsyncIteratorFactoryInstance(
 ): unknown {
   deserialize(ctx, depth, node.a[0]);
   const source = deserialize(ctx, depth, node.a[1]);
-  // `node.a[1]` is any node the input picked. Validate the source *node type*,
-  // not the runtime value's shape: a `__SEROVAL_STREAM__` brand is forgeable by
-  // a plugin, but only a genuine StreamConstructor node carries the mark, and
-  // `guardIndexedValue` forbids reusing its id for another value. This stops a
-  // plugin-returned fake stream from having its `on` invoked below.
-  validateNodeType(ctx, node, node.a[1].i, SerovalNodeType.StreamConstructor);
-  if (!source) {
+  // `node.a[1]` is any node the input picked. Check the value itself rather
+  // than the type recorded for its id: in vanilla mode an unmarked id can be
+  // reused, so another value could sit behind an id marked as a stream. This
+  // stops a fake stream from having its `on` invoked below.
+  if (!isStream(source)) {
     throw new SerovalMalformedNodeError(node.a[1]);
   }
-  return streamToAsyncIterable(source as Stream<unknown>);
+  return streamToAsyncIterable(source);
 }
Enter fullscreen mode Exit fullscreen mode

Mitigation Strategies

  • Upgrade seroval and seroval-plugins to version 1.6.8 or higher to enforce runtime instanceof checks on Stream and Sequence instances.
  • Avoid passing untrusted or unauthenticated network payloads directly to Seroval's fromJSON deserializer.
  • Audit custom Seroval plugins to ensure their deserialize hooks do not return objects exposing sensitive .on() methods or callable interfaces.
  • Pre-filter incoming JSON AST structures to block node types 28 (IteratorFactoryInstance) and 30 (AsyncIteratorFactoryInstance) if iterator deserialization is not required.

Remediation Steps:

  1. Identify all direct and transitive dependencies on 'seroval' and 'seroval-plugins' across project lockfiles.
  2. Run 'npm install seroval@^1.6.8' (or the equivalent yarn/pnpm command) or configure package.json overrides to force resolution to >= 1.6.8.
  3. Verify that unit and integration tests pass and that malformed AST inputs throw SerovalMalformedNodeError as expected.

References


Read the full report for CVE-2026-116580 on our website for more details including interactive diagrams and full exploit analysis.

Top comments (0)