Back to Blog
high SEVERITY6 min read

How Remote Code Execution via serialize-javascript happens in Node.js and how to fix it

The `serialize-javascript` package version 6.0.2 contained a high-severity Remote Code Execution (RCE) vulnerability (GHSA-5c6j-r48x-rmvq) exploitable through crafted `RegExp.flags` and `Date.prototype.toISOString()` payloads. Upgrading to version 7.0.3 eliminates the vulnerable serialization logic and removes the `randombytes` dependency that was part of the attack surface. This fix was applied via a `package.json` override and `package-lock.json` update.

O
By Orbis AppSec
Published August 4, 2026Reviewed August 4, 2026

Answer Summary

GHSA-5c6j-r48x-rmvq is a high-severity Remote Code Execution (RCE) vulnerability in the `serialize-javascript` npm package (versions prior to 7.0.0), exploitable via prototype pollution of `RegExp.flags` and `Date.prototype.toISOString()`. It maps to CWE-94 (Improper Control of Generation of Code). The fix is to upgrade `serialize-javascript` from 6.0.2 to 7.0.3, which rewrites the serialization logic to prevent code injection through these prototype methods.

Vulnerability at a Glance

cweCWE-94 (Improper Control of Generation of Code)
fixUpgrade serialize-javascript from 6.0.2 to 7.0.3 via package.json override
riskAttacker can execute arbitrary code on the server by polluting RegExp or Date prototypes before serialization
languageJavaScript (Node.js)
root causeserialize-javascript 6.0.2 trusts return values of RegExp.flags and Date.prototype.toISOString() during code generation
vulnerabilityRemote Code Execution (RCE) via unsafe serialization

Introduction

In a Docusaurus-based documentation site's dependency tree, a high-severity Remote Code Execution vulnerability lurked inside serialize-javascript version 6.0.2. This package — widely used by webpack, Terser, and other build tools to convert JavaScript objects into safe string representations — contained a critical flaw: an attacker who could pollute RegExp.prototype.flags or Date.prototype.toISOString() could inject arbitrary code into the serialized output, achieving RCE when that output was later evaluated.

The vulnerability tracked as GHSA-5c6j-r48x-rmvq was detected by Trivy in the project's package-lock.json and resolved by upgrading from version 6.0.2 to 7.0.3. While the dependency wasn't confirmed reachable in the application's runtime paths, its presence in the build pipeline represented a concrete exploit primitive that automated attack tooling could chain with other weaknesses.

The Vulnerability Explained

How serialize-javascript Works

The serialize-javascript library converts JavaScript values — including functions, RegExp objects, Dates, Maps, and Sets — into string representations that can be safely embedded in HTML or evaluated later. For example:

const serialize = require('serialize-javascript');
serialize({ regex: /hello/gi, date: new Date() });
// Output: '{"regex":new RegExp("hello", "gi"),"date":new Date("2024-01-15T...")}'

The Attack Vector

In version 6.0.2, the library directly called RegExp.prototype.flags and Date.prototype.toISOString() to construct the serialized output string. The critical issue is that these are accessor properties and prototype methods — they can be overridden via prototype pollution.

Consider this attack scenario:

// Attacker pollutes the RegExp prototype
Object.defineProperty(RegExp.prototype, 'flags', {
  get: function() {
    return 'gi", ""));console.log(process.env);//';
  }
});

// When serialize-javascript processes a RegExp:
const serialize = require('serialize-javascript');
const output = serialize({ re: /innocent/ });
// Output becomes: new RegExp("innocent", "gi", ""));console.log(process.env);//")
// The injected code executes when this string is eval'd or embedded in a script

Similarly, Date.prototype.toISOString() could be polluted:

Date.prototype.toISOString = function() {
  return '");require("child_process").exec("rm -rf /");//';
};

Why This Is High Severity

The serialized output from this library is commonly:
1. Embedded directly in <script> tags by webpack/SSR frameworks
2. Written to build artifacts that are later executed
3. Used in server-side rendering pipelines

If an attacker can achieve prototype pollution (via another vulnerability in the dependency chain, a malicious npm package, or user-controlled JSON parsing), they can escalate it to full RCE through this serialization step.

The Role of randombytes

Version 6.0.2 depended on randombytes (which itself depends on safe-buffer) to generate random placeholders during serialization. Version 7.0.3 eliminates this dependency entirely, using Node.js built-in crypto.randomUUID() instead — reducing the attack surface and the dependency tree simultaneously.

The Fix

The fix involves two coordinated changes across package.json and package-lock.json:

1. Adding a Package Override (package.json)

// Before
"overrides": {
    "@cmfcmf/docusaurus-search-local": {
      "@docusaurus/core": "^3.5.2",
      "cheerio": "1.0.0-rc.12"
    }
}

// After
"overrides": {
    "@cmfcmf/docusaurus-search-local": {
      "@docusaurus/core": "^3.5.2",
      "cheerio": "1.0.0-rc.12"
    },
    "serialize-javascript": "7.0.3"
}

The overrides field in package.json forces all instances of serialize-javascript in the dependency tree to resolve to 7.0.3, regardless of what version ranges transitive dependencies specify. This is critical because serialize-javascript is typically pulled in by webpack plugins, Terser, and other build tools — not directly by the application.

2. Updating the Lock File (package-lock.json)

// Before
"node_modules/serialize-javascript": {
  "version": "6.0.2",
  "license": "BSD-3-Clause",
  "dependencies": {
    "randombytes": "^2.1.0"
  }
}

// After
"node_modules/serialize-javascript": {
  "version": "7.0.3",
  "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.3.tgz",
  "integrity": "sha512-h+cZ/XXarqDgCjo+YSyQU/ulDEESGGf8AMK9pPNmhNSl/FzPl6L8pMp1leca5z6NuG6tvV/auC8/43tmovowww==",
  "license": "BSD-3-Clause",
  "engines": {
    "node": ">=20.0.0"
  }
}

Key improvements in 7.0.3:
- Removed randombytes dependency: The node_modules/randombytes entry is deleted entirely, reducing supply chain risk
- Added integrity hash: The integrity field with SHA-512 ensures the exact expected package is installed
- Node.js 20+ requirement: Leverages built-in crypto.randomUUID() and modern security features
- Hardened serialization: The new version doesn't trust prototype method return values for code generation

Why Both Files Changed

The package.json override ensures the constraint persists across npm install runs and affects all transitive consumers. The package-lock.json change reflects the resolved state, ensuring deterministic installs with the exact patched version and its integrity hash.

Key Takeaways

  • serialize-javascript 6.0.2 trusts prototype methods during code generation — a design flaw that converts prototype pollution into RCE
  • The randombytes dependency was eliminated in 7.0.3, reducing the attack surface and dependency count simultaneously
  • npm overrides in package.json is the correct mechanism to force transitive dependency upgrades when direct parents haven't updated
  • Build-time dependencies can be just as dangerous as runtime ones — webpack and Terser use serialize-javascript to generate code that executes in users' browsers
  • Integrity hashes in package-lock.json (the sha512-h+cZ/XX... value) provide tamper detection that wasn't present in the old lock entry

How Orbis AppSec Detected This

  • Source: The serialize-javascript package at version 6.0.2 in the project's package-lock.json dependency tree, pulled in transitively by build tools
  • Sink: The serialize() function in serialize-javascript/index.js which calls RegExp.prototype.flags and Date.prototype.toISOString() to construct executable JavaScript strings
  • Missing control: No sanitization or validation of prototype method return values before embedding them in generated code strings
  • CWE: CWE-94 (Improper Control of Generation of Code / Code Injection)
  • Fix: Upgraded serialize-javascript from 6.0.2 to 7.0.3 via npm override, which rewrites serialization logic to not trust pollutable prototype accessors

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

GHSA-5c6j-r48x-rmvq demonstrates how a seemingly innocuous utility library — one that serializes JavaScript objects to strings — can become an RCE vector when it trusts prototype methods during code generation. The fix was straightforward: upgrade serialize-javascript from 6.0.2 to 7.0.3 using an npm override. But the lesson is deeper: any library that generates executable code from object properties must treat those properties as potentially attacker-controlled. In a world of prototype pollution vulnerabilities and increasingly automated exploit chains, proactively removing these primitives from your dependency tree is essential defense-in-depth.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1389

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.