In 2017, security researcher Ajin Abraham disclosed a remote code execution flaw in node-serialize, a popular npm package used to convert JavaScript objects—including functions—into strings and back again. The vulnerability, tracked as CVE-2017-5941, let attackers smuggle executable code inside what looked like ordinary serialized session data. Because node-serialize's unserialize() function used eval() to reconstruct function objects, a single crafted cookie value was enough to run arbitrary shell commands on the server.
Nearly a decade later, the underlying pattern—trusting serialized data enough to reconstruct executable code from it—still shows up in Node.js codebases, session middleware, caching layers, and internal RPC formats. This post breaks down how the node-serialize exploit actually works, why insecure deserialization keeps resurfacing in JavaScript applications, and what concrete steps eliminate the risk in your own supply chain.
What Is Insecure Deserialization in JavaScript?
Insecure deserialization happens when an application reconstructs objects—including functions or class instances—from untrusted input without validating what it's rebuilding. Unlike Java or PHP, JavaScript's native JSON.parse() is actually one of the safer deserializers in mainstream use: it only ever produces plain objects, arrays, strings, numbers, booleans, and null. It cannot instantiate a function or execute code on its own.
The danger appears when developers reach for libraries that extend serialization beyond JSON's safe subset—specifically, formats that can represent functions as data. node-serialize was built to do exactly that: it serialized JavaScript functions by wrapping their source code in a string with a special marker, then reconstructed them at deserialization time using eval(). That single design decision—using eval() to turn a string back into executable code—is the entire vulnerability class in miniature. Any attacker who controls the serialized string controls what gets executed.
How Does the node-serialize RCE Exploit (CVE-2017-5941) Work?
The exploit works because node-serialize's unserialize() function matches any string prefixed with _$$ND_FUNC$$_ and passes it directly to eval(). A benign serialized function looks like this:
{"rce":"_$$ND_FUNC$$_function(){return 1}()"}
Abraham's proof-of-concept simply replaced the function body with a Node.js child_process call:
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('cat /etc/passwd > /tmp/out.txt')}()"}
When unserialize() hit that string, it stripped the marker, handed the remainder to eval(), and the immediately-invoked function ran with the same privileges as the Node.js process. In the original disclosure, the attack vector was a cookie-based session store: an Express app used node-serialize to persist session objects in a cookie, so an attacker only had to set a crafted cookie value and send one request to achieve full remote code execution—no authentication required. The package's maintainers never shipped a patch; the advisory's guidance was simply to stop using it, and npm has since flagged versions up through 0.0.4 as vulnerable in its advisory database.
Why Do Node.js Applications Still Use Unsafe Deserialization Patterns?
They still do it because "serialize a function and send it somewhere" is a convenient shortcut, and convenience outlives its original context. node-serialize itself was designed for legitimate use cases—passing callback logic between processes, persisting closures in a cache, or moving config objects that included default functions. Teams adopt it (or write an equivalent using eval() or new Function()) for an internal, trusted use case, then months later the same serialization path gets wired up to accept a query parameter, a webhook payload, or a cookie from an end user without anyone revisiting the original trust assumptions.
This is also why insecure deserialization keeps ranking in OWASP's Top 10 (A08:2021 – Software and Data Integrity Failures folds it in) across every major language, not just JavaScript. The npm ecosystem compounds the problem: node-serialize has been downloaded hundreds of thousands of times historically, and forks and copy-pasted snippets of its unserialize() logic have propagated into internal tooling that never shows up in a package.json audit at all, because the vulnerable code was copied rather than imported.
What Are Real-World Attack Scenarios for node-serialize?
The most common scenario is session state stored client-side and deserialized server-side without integrity checks. If a Node.js app signs a cookie with a weak or leaked secret—or doesn't sign it at all—an attacker can forge a session value containing the _$$ND_FUNC$$_ payload and trigger RCE on every request that touches that cookie. Security researchers have documented this exact chain in CTF writeups and in real Express/Connect middleware misconfigurations, and it maps directly onto the pattern behind other deserialization-driven breaches, such as the 2015 Java deserialization attacks against Jenkins and WebLogic servers (CVE-2015-4852) that used Apache Commons Collections gadget chains for the same end result: untrusted bytes in, arbitrary code execution out.
A second scenario involves message queues and inter-service communication. If two internal services pass "rich" payloads (including function references) over Redis, RabbitMQ, or a WebSocket channel using node-serialize instead of plain JSON, compromising or spoofing one service becomes a direct path to code execution on the other—turning a single weak internal endpoint into a lateral-movement primitive across your entire backend.
How Can Developers Prevent Insecure Deserialization in Node.js?
The fix starts with removing node-serialize (and any home-grown equivalent) from anywhere it touches untrusted input. Concretely:
- Use
JSON.parse()/JSON.stringify()for anything that crosses a trust boundary. JSON cannot represent functions, so the entire exploit class is structurally impossible. - Validate deserialized data against a schema using a library like
ajvorzodbefore your application logic touches it, rather than trusting shape and type implicitly. - Never call
eval(),new Function(), orvm.runInThisContext()on data derived from user input, even indirectly through a "serialization" library's internals. - If you must serialize rich objects, use a one-way library like
serialize-javascript, which is explicitly designed to safely embed JavaScript values into HTML/script contexts for output—not to reconstruct executable functions from untrusted strings on input. Read the library's threat model before assuming "serialize" and "deserialize" are safe inverses of each other. - Sign and encrypt session cookies (e.g., with
cookie-session's signing or a server-side session store) so that even a JSON-only payload can't be tampered with, and rotate secrets that may have leaked. - Run
npm auditand a software composition analysis (SCA) tool in CI so that a dependency on node-serialize—or any package carrying a known deserialization advisory—fails the build instead of shipping to production.
How Safeguard Helps
Safeguard's software supply chain security platform is built to catch exactly this class of risk before it reaches production. Our composition analysis continuously scans dependency manifests and lockfiles for packages like node-serialize that carry known CVEs (including CVE-2017-5941), flags transitive dependencies pulling it in indirectly, and blocks builds when a policy threshold is crossed—so a forgotten require('node-serialize') three layers deep in your dependency tree doesn't sit unnoticed until an attacker finds it first.
Beyond dependency scanning, Safeguard's static analysis engine detects dangerous sink patterns directly in your code—calls to eval(), new Function(), and vm module usage fed by request bodies, cookies, or query parameters—surfacing the exact line where untrusted input meets a deserialization sink, whether the vulnerable code was imported from npm or copy-pasted into an internal utility file. Combined with provenance tracking across your CI/CD pipeline, Safeguard gives teams a continuous, auditable record of what's actually running in production versus what's been reviewed—turning insecure deserialization from a recurring incident-response surprise into a finding your pipeline catches automatically, every time.