Introduction
In a project's dependency tree, a high-severity Remote Code Execution vulnerability lurked inside serialize-javascript version 6.0.2 — a widely-used npm package that serializes JavaScript objects into strings for transport between server and client. The vulnerability, tracked as GHSA-5c6j-r48x-rmvq, could have allowed attackers to execute arbitrary code by poisoning RegExp.flags and Date.prototype.toISOString() — two prototype methods that the library naively trusted during its serialization process.
The vulnerable dependency was identified in package-lock.json, pinned at version 6.0.2 with a dependency on randombytes for generating unique identifiers. While the vulnerability was assessed as "present in dependency tree, not confirmed reachable," it represented an exploit primitive that automated attack tooling could chain with other weaknesses.
The Vulnerability Explained
How serialize-javascript Works
The serialize-javascript package converts JavaScript values — including functions, regular expressions, dates, maps, and sets — into string representations that can be safely embedded in HTML or transmitted across boundaries. It's commonly used by bundlers like webpack (via terser-webpack-plugin) and server-side rendering frameworks.
The Attack Vector
The vulnerability exploits a fundamental design flaw in how version 6.0.2 handled RegExp and Date serialization. When serializing a regular expression, the library called regex.flags to obtain the flags string (e.g., "gi"). When serializing dates, it called date.toISOString().
Here's the critical insight: JavaScript allows prototype methods to be overridden. An attacker who can poison these prototypes before serialization occurs can inject arbitrary code into the serialized output:
// Attacker poisons RegExp.prototype
Object.defineProperty(RegExp.prototype, 'flags', {
get: function() {
return 'gi; console.log("RCE achieved"); //';
}
});
// When serialize-javascript processes a RegExp:
// Expected output: /pattern/gi
// Actual output: /pattern/gi; console.log("RCE achieved"); //
When this serialized string is later evaluated or embedded in a script context, the injected code executes. The same principle applies to Date.prototype.toISOString():
Date.prototype.toISOString = function() {
return '2024-01-01T00:00:00.000Z"; process.exit(1); "';
};
The Vulnerable Dependency Entry
The package-lock.json contained:
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
}
}
The randombytes dependency was used internally for generating unique placeholders during serialization — a mechanism that version 7.0.3 no longer requires.
Real-World Impact
In server-side rendering scenarios, if an attacker can influence the objects being serialized (e.g., through a stored XSS payload that modifies prototypes, or through a supply-chain attack on another dependency), they could achieve:
- Server-side RCE: Execute arbitrary commands on the Node.js server
- Client-side code injection: Inject malicious scripts into HTML pages served to users
- Data exfiltration: Access environment variables, secrets, or database connections
The Fix
Changes Made
The fix involved two files with a clear, focused approach:
1. package.json — Adding a dependency override:
// Before
{
"engines": {
"node": ">=22.0"
}
}
// After
{
"engines": {
"node": ">=22.0"
},
"overrides": {
"serialize-javascript": "7.0.3"
}
}
The overrides field in package.json forces all instances of serialize-javascript in the entire dependency tree — regardless of which package depends on it — to resolve to version 7.0.3. This is critical because serialize-javascript is typically a transitive dependency (pulled in by webpack plugins, testing frameworks, etc.), not a direct dependency.
2. package-lock.json — Updated resolution:
// Before
"node_modules/serialize-javascript": {
"version": "6.0.2",
"dependencies": {
"randombytes": "^2.1.0"
}
}
// After
"node_modules/serialize-javascript": {
"version": "7.0.3",
"engines": {
"node": ">=20.0.0"
}
}
Why This Solves the Problem
Version 7.0.3 of serialize-javascript fundamentally changes how it handles RegExp and Date serialization:
- It no longer calls
RegExp.prototype.flags— Instead, it extracts flags through a safe mechanism that cannot be influenced by prototype poisoning - It no longer calls
Date.prototype.toISOString()— Date serialization uses internal methods that bypass the prototype chain - The
randombytesdependency is removed — The new version uses Node.js built-in crypto APIs (requiring Node.js ≥ 20.0.0), reducing the attack surface and dependency footprint
The removal of randombytes from the lock file (along with its safe-buffer dependency) is a direct consequence of this architectural change in the library.
Prevention & Best Practices
For This Specific Vulnerability Pattern
- Never trust prototype methods on user-influenced objects: When generating code strings, use internal extraction methods rather than calling potentially-overridden prototype methods
- Use npm overrides for transitive dependency fixes: When a vulnerability exists in a transitive dependency and the intermediate package hasn't updated yet,
overrides(npm) orresolutions(yarn) force the correct version - Audit serialization boundaries: Any code that converts objects to executable strings (eval-able output) should be treated as a critical security boundary
General Practices
- Automated dependency scanning: Use tools like Trivy, Snyk, or Dependabot to continuously monitor for known vulnerabilities
- Lock file hygiene: Regularly audit
package-lock.jsonfor outdated or vulnerable transitive dependencies - Prototype freezing: In security-critical contexts, consider
Object.freeze(RegExp.prototype)and similar hardening - Content Security Policy: Implement strict CSP headers to limit the impact of injected scripts
Relevant Standards
- CWE-94: Improper Control of Generation of Code ('Code Injection')
- OWASP: Injection category (A03:2021)
- Node.js Security Best Practices: Avoid
eval()and equivalent patterns with untrusted input
Key Takeaways
serialize-javascript6.0.2 trustedRegExp.flagsandDate.prototype.toISOString()as safe — but these are user-modifiable prototype methods that can inject arbitrary code into serialized output- The
overridesfield inpackage.jsonis essential for fixing transitive dependency vulnerabilities — you can't always wait for intermediate packages to update their dependency ranges - Removing
randombytesas a dependency in 7.0.3 signals a fundamental architectural change — the library now uses Node.js built-in crypto, reducing both attack surface and dependency complexity - Prototype poisoning is a viable RCE vector in serialization libraries — any code that calls prototype methods and embeds results in executable strings must be treated as security-critical
- Vulnerability assessment noted "not confirmed reachable" but the fix was still applied — proactive removal of exploit primitives raises the bar against automated attack tooling
How Orbis AppSec Detected This
- Source: Poisoned prototype methods (
RegExp.prototype.flags,Date.prototype.toISOString()) accessible to any code running in the same JavaScript context - Sink:
serialize-javascriptlibrary's internal serialization functions that call these prototype methods and embed results in code strings output to HTML/scripts - Missing control: No validation or sanitization of values returned by prototype method calls before embedding them in generated code strings; no use of safe internal extraction alternatives
- CWE: CWE-94 (Improper Control of Generation of Code)
- Fix: Upgraded
serialize-javascriptfrom 6.0.2 to 7.0.3 via npm override, which replaces unsafe prototype method calls with internal safe extraction logic
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
The GHSA-5c6j-r48x-rmvq vulnerability in serialize-javascript is a sobering reminder that even widely-trusted utility libraries can harbor critical security flaws. The attack — poisoning JavaScript prototype methods to inject code during serialization — is elegant in its simplicity and devastating in its impact. By upgrading to version 7.0.3 through an npm override, the project eliminates this RCE vector entirely while also reducing its dependency footprint by removing randombytes.
For developers: audit your dependency trees for serialize-javascript versions below 7.0.0, and apply the override pattern shown here if direct upgrades aren't immediately possible. Serialization boundaries are trust boundaries — treat them accordingly.