Status: Patched
This vulnerability has been verified as resolved and deployed.
seroval.fromJSON() Promise resolver type confusion invokes attacker-controlled methods (CVE-2026-59940)
Summary
Promise control nodes trusted attacker-controlled values from Seroval's general deserialization reference table
A type confusion issue in seroval.fromJSON() allowed attacker-controlled JSON input to cause PromiseSuccess and PromiseFailure nodes to operate on values from the general deserialization reference table without first verifying that those values were genuine internal promise resolver records. In vanilla fromJSON() mode, the serialized node tree and top-level m marked-reference list can place an attacker-created object in that table. The vulnerable handlers retrieved refs[node.i] and called .s(...) or .f(...) after checking only that the value was truthy.
With plugins enabled, an attacker could deserialize callable values into the object's .s and .f properties and make Promise control nodes invoke them with attacker-controlled data during deserialization. This creates a deserialization side-effect primitive. In downstream server frameworks that register plugins returning callable wrappers, it can become unintended server-side invocation and, depending on exposed application functionality, remote code execution or equivalent server compromise.
The issue affects seroval versions through 1.5.2 and was fixed in [email protected]. A concrete downstream impact scenario was privately validated against TanStack Start before the fix; with [email protected], that reproducer no longer succeeds. The advisory is GHSA-mv8w-475r-vwqw / CVE-2026-59940, CWE-502, with CVSS 3.1 score 9.8.
CVSS Score
Vulnerability Location
Source-to-Sink Analysis
An application passes attacker-controlled Seroval JSON to fromJSON() with one or more deserialization plugins enabled. The payload controls both the serialized node tree and the top-level m list of reference IDs that vanilla mode retains.
When an indexed object ID appears in m, vanilla deserialization stores that ordinary attacker-created value in the shared reference table. Ref ID 0 can therefore hold a plain object whose .s and .f properties are plugin-produced functions.
A registered plugin may legitimately deserialize a tagged node into a callable wrapper. The proof uses a harmless local plugin that returns functions which only append their arguments to an in-memory array.
The vulnerable Promise success handler retrieved refs[node.i] and checked only whether the value was truthy. It did not establish that the ref came from Seroval's internal Promise constructor path before calling .s().
The rejection handler made the same assumption and invoked .f() on the attacker-created ref. Both handlers passed a separately deserialized attacker-controlled value as the call argument.
The fixed deserializer records the node type associated with an internal Promise constructor ref and validates that provenance before either control node invokes a resolver callback. An ordinary object stored in the general ref table has no matching PromiseConstructor type and is rejected first.
Impact Analysis
Critical Impact
Direct impact in seroval is an attacker-controlled deserialization side effect: structured JSON reconstruction can invoke methods supplied through attacker-created objects. In a downstream framework where a plugin reconstructs RPC, server-function, or other privileged callable wrappers, the same primitive can trigger unintended server-side invocation. Depending on the functions exposed by the application, this may lead to remote code execution or equivalent server compromise. The privately coordinated TanStack Start chain is intentionally summarized without publishing its technical exploit material.
Attack Surface
Applications that deserialize Seroval JSON from untrusted clients through fromJSON() on an affected seroval version and enable plugins. The direct package-level primitive requires a plugin capable of reconstructing callable values. Downstream severity depends on what those plugins expose.
Preconditions
The target must use a vulnerable seroval release through 1.5.2, accept attacker-controlled Seroval JSON, and register a plugin that can deserialize a function or callable wrapper. Turning the primitive into server compromise additionally requires a downstream callable that reaches privileged server functionality.
Proof of Concept
Environment Setup
Create an isolated local project and install the verified vulnerable release:
The proof is local-only: it performs no network requests, shell execution, filesystem writes, or destructive side effects after setup.
Target Configuration
Save the following as poc.mjs. The plugin returns harmless functions that record calls in memory, while the payload marks a normal object containing those functions as ref ID 0:
Exploit Delivery
Append the invocation and assertions to poc.mjs, then execute it:
Outcome
The proof isolates the root cause: ref ID 0 contains a normal attacker-created object, not a genuine PromiseConstructorResolver, yet the Promise control nodes invoke both of its callable properties. Repeating the regression check with [email protected] aborts deserialization before either in-memory side effect occurs. The separately coordinated TanStack Start reproducer likewise produces no evidence side effect with 1.5.3.
Expected Response:
On [email protected], fromJSON() returns an array and records two calls during deserialization: one to the attacker-created .s value with the PromiseSuccess argument, and one to .f with the PromiseFailure argument. The final line is:
Run this level of analysis on your repo.
Winfunc traces source-to-sink paths, validates exploitability, and gives your team patch-ready remediation.
