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.
Prevention & Best Practices
1. Audit Your Dependency Tree Regularly
npm audit
npx trivy fs --scanners vuln .
Transitive dependencies like serialize-javascript are easy to miss because they don't appear in your package.json directly.
2. Use Package Overrides for Deep Dependencies
When a vulnerability exists in a transitive dependency and the direct dependency hasn't updated yet, npm overrides (or yarn resolutions) let you force a safe version:
{
"overrides": {
"serialize-javascript": ">=7.0.0"
}
}
3. Minimize Serialization of Untrusted Data
If you don't need executable JavaScript output, prefer JSON.stringify() which produces inert data strings. Only use serialize-javascript when you specifically need to serialize functions, RegExp, or other non-JSON types.
4. Protect Against Prototype Pollution
Since this RCE requires prototype pollution as a precondition:
- Use Object.create(null) for dictionary objects
- Freeze prototypes in security-critical paths: Object.freeze(Object.prototype)
- Validate and sanitize JSON input with libraries like secure-json-parse
5. Pin and Verify Dependencies
Always commit your package-lock.json and use npm ci in CI/CD pipelines. The integrity field in the lock file prevents tampered packages from being installed.
Key Takeaways
serialize-javascript6.0.2 trusts prototype methods during code generation — a design flaw that converts prototype pollution into RCE- The
randombytesdependency was eliminated in 7.0.3, reducing the attack surface and dependency count simultaneously - npm
overridesinpackage.jsonis 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-javascriptto generate code that executes in users' browsers - Integrity hashes in
package-lock.json(thesha512-h+cZ/XX...value) provide tamper detection that wasn't present in the old lock entry
How Orbis AppSec Detected This
- Source: The
serialize-javascriptpackage at version 6.0.2 in the project'spackage-lock.jsondependency tree, pulled in transitively by build tools - Sink: The
serialize()function inserialize-javascript/index.jswhich callsRegExp.prototype.flagsandDate.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-javascriptfrom 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.