Deserialization that evaluates the payload

ID

javascript.insecure_deserialization

Severity

low

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

Resource

Misconfiguration

Language

JavaScript

Tags

ASVS50:v1.5.2, CWE:502, NIST.SP.800-53, OWASP:2025:A08, PCI-DSS:6.5.1

Description

A payload is read with a deserializer that rebuilds the functions embedded in it and runs them. The payload is code rather than data, so whoever writes it executes it.

Rationale

javascript.code_injection_with_deserialization reports deserialization when a data path can be followed from an external input to the call. That needs the input to be visible: an unserialize() in a codec or a helper module, where the caller lives elsewhere, has no source to follow and is reported by nothing.

Its severity is low because the finding says nothing about whether an attacker-controlled payload actually reaches the call — when that can be shown, javascript.code_injection_with_deserialization reports it as critical, and this finding is dropped as a duplicate.

A short sink list, on purpose

JavaScript has no equivalent of pickle or ObjectInputStream in its standard library. JSON.parse names no types and evaluates nothing, so unlike Java, Python or PHP there is no default to report. What is left is a handful of packages that deliberately round-trip functions, and each was checked against the version npm currently serves:

Sink Checked

node-serialize.unserialize

Verified: a payload carrying _ND_FUNC_function(){..}() runs it — straight RCE.

funcster.deepDeserialize

Still exported by the current 0.0.5, and rebuilds functions through a generated module.

serialize-to-js.deserialize

Removed in the 3.x line, which exports a bare function. Kept precisely because of that: a call naming it can only be against the 1.x/2.x that still had it, where it was eval-based.

That last entry is the inverse of the usual version trap. Elsewhere a sink has to be dropped because a release made the default safe; here the release removed the API outright, so matching it can only ever hit the vulnerable old major.

Examples

Reported:

const serialize = require('node-serialize');

function restore(blob) {
  return serialize.unserialize(blob);      // reported
}

Not reported:

function restore(blob) {
  return JSON.parse(blob);
}

Remediation

Use JSON.parse and validate the result against the shape you expect. Where functions genuinely need to cross a boundary, send a name the receiver resolves against a fixed table rather than a body it evaluates:

const handlers = { resize, crop };
const { handler, args } = JSON.parse(blob);
return handlers[handler]?.(...args);

Limitations

Matched on the resolved package-qualified name, so a deserializer reached through a re-export or a dynamically computed property is not seen.

© 2026 Xygeni. All rights reserved