How Prototype Pollution Happens in JavaScript Dependencies and How to Fix It
The Incident
In a repository that processes geospatial data — pulling together dependencies like xlsx, geojson-vt, fflate, and iconv-lite — Trivy's dependency scanner flagged a high-severity advisory against js-yaml version 5.2.1. The advisory, GHSA-pm4m-ph32-ghv5, describes a scenario where a single crafted YAML string can drive the js-yaml parser into exponential time complexity, effectively freezing the Node.js event loop for as long as the attacker desires.
The vulnerable entry in package-lock.json looked like this:
"node_modules/js-yaml": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
"integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw=="
}
And the declared dependency in package.json:
"js-yaml": "^5.2.1"
A one-version patch — bumping to 5.2.2 — was all it took to close the door.
The Vulnerability Explained
What Makes Flow Collections Dangerous in js-yaml 5.2.1?
YAML supports two main styles for collections (arrays and objects): block style and flow style. Flow collections use inline {} and [] notation, making them compact and convenient — but also more complex to parse.
In js-yaml 5.2.1, the parser responsible for processing flow collections contained a logic path whose time complexity was not strictly linear. For certain deeply or repeatedly nested flow structures, the parser would revisit and re-evaluate portions of the input in a pattern that caused exponential growth in CPU time relative to input size. This is structurally similar to a ReDoS (Regular Expression Denial of Service) attack, but applied to the YAML parser's state machine rather than a regex engine.
An attacker who can supply YAML input to your application — even a small payload of a few hundred bytes — can craft a flow collection that causes js-yaml.load() or js-yaml.safeLoad() to consume 100% CPU for seconds, minutes, or indefinitely. Because Node.js runs JavaScript on a single-threaded event loop, a blocked parser blocks everything: HTTP responses, database callbacks, health checks, and all other in-flight requests.
What Does an Attack Look Like?
Consider a Node.js service that accepts YAML configuration uploads, or an API endpoint that parses YAML-formatted request bodies. If the service calls:
const yaml = require('js-yaml');
const parsed = yaml.load(req.body.config); // req.body.config is attacker-controlled
An attacker sends a POST request with a body containing a specially crafted flow collection:
{a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: {a: }}}}}}}}}}}}}}
(The actual proof-of-concept payloads for this CVE are more precisely tuned, but the principle is the same: nested or repeated flow structures that exploit the parser's backtracking behavior.)
The parser enters its exponential path. The event loop stalls. Every other request to the service times out. The application is effectively down — a full denial of service achieved with a single HTTP request.
Why This Repository Is Specifically at Risk
This project lists xlsx (SheetJS) as a direct dependency alongside js-yaml. SheetJS itself processes spreadsheet files that can embed YAML-formatted metadata or configuration. A pipeline that accepts user-uploaded spreadsheets and also uses js-yaml for configuration parsing presents two distinct surfaces where untrusted YAML could reach the vulnerable parser. The combination of file-processing dependencies and a YAML parser in the same package.json is precisely the kind of context where this vulnerability has real-world bite.
The Fix
What Changed
The fix is a targeted version bump in two files:
package.json — the human-maintained dependency manifest:
- "js-yaml": "^5.2.1",
+ "js-yaml": "^5.2.2",
package-lock.json — the machine-generated lockfile that pins exact resolved versions:
"node_modules/js-yaml": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
- "integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz",
+ "integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==",
Why Both Files Matter
Updating only package.json is insufficient in a CI/CD environment that uses npm ci (which installs strictly from the lockfile). If package-lock.json still pins 5.2.1, every clean install — including every Docker build, every deployment pipeline run, and every developer npm ci — will install the vulnerable version. Updating both files guarantees that the patched version is installed consistently everywhere.
The new integrity hash (sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==) also serves as a supply-chain safeguard: npm will refuse to install a package whose content doesn't match this hash, protecting against a compromised registry entry.
What js-yaml 5.2.2 Actually Fixes
The js-yaml 5.2.2 patch corrects the flow collection parser so that it processes each token in bounded, predictable time — the parsing complexity returns to O(n) with respect to input length. Valid YAML inputs are parsed identically; only the adversarial edge cases that triggered backtracking are now handled without exponential blowup. No API changes were made, so the upgrade is a drop-in replacement.
Prevention & Best Practices
1. Lock Your Dependencies and Audit Them Regularly
A package-lock.json is only as safe as its last audit. Integrate automated dependency scanning into your CI pipeline:
npm audit
# or with a dedicated scanner:
trivy fs --scanners vuln .
Configure your pipeline to fail on high-severity findings so vulnerable versions never reach production.
2. Use Exact or Tight Version Ranges for Security-Sensitive Libraries
The original constraint ^5.2.1 allows any 5.x.x >= 5.2.1, which means it would have picked up 5.2.2 on a fresh npm install — but not on npm ci with a stale lockfile. For libraries that parse untrusted input, consider pinning to an exact version and updating deliberately:
"js-yaml": "5.2.2"
3. Never Parse Untrusted YAML Without a Timeout or Isolation Boundary
Even with a patched library, defense in depth is valuable. If your application parses user-supplied YAML, wrap the call in a worker thread with a timeout:
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
// In the worker:
const yaml = require('js-yaml');
parentPort.postMessage(yaml.load(workerData.input));
// In the main thread, enforce a timeout on the worker
This ensures that even a future zero-day in the parser cannot block your event loop.
4. Validate Input Size Before Parsing
Enforce a maximum size on any YAML input before handing it to the parser:
const MAX_YAML_BYTES = 64 * 1024; // 64 KB
if (Buffer.byteLength(input, 'utf8') > MAX_YAML_BYTES) {
throw new Error('YAML input exceeds maximum allowed size');
}
const parsed = yaml.load(input);
This does not eliminate the vulnerability, but it significantly raises the cost of an attack.
5. Reference Standards
- OWASP: Denial of Service Cheat Sheet
- CWE-400: Uncontrolled Resource Consumption
- CWE-1333: Inefficient Regular Expression Complexity (the structural analogue for parser complexity attacks)
- OWASP A06:2021: Vulnerable and Outdated Components — this entire class of issue is addressed by keeping dependencies current
Key Takeaways
- The
js-yamlflow collection parser in version 5.2.1 has exponential time complexity on adversarial inputs — a single crafted YAML string can freeze a Node.js process indefinitely. - Updating
package.jsonalone is not enough —package-lock.jsonmust also be updated to ensurenpm ciinstalls the patched version in CI/CD and production environments. - The integrity hash change in
package-lock.json(fromsha512-zfLtN...tosha512-dayzU...) is a supply-chain safeguard, not cosmetic — it ensures npm validates the exact bytes of the installed package. - This project's combination of
xlsxandjs-yamlcreates two potential surfaces for untrusted YAML to reach the parser — file uploads and configuration APIs — making the patch especially important here. - Algorithmic complexity attacks are not stopped by firewalls or WAFs — they require patching the vulnerable parsing logic itself.
How Orbis AppSec Detected This
- Source: Untrusted YAML content entering the application via any code path that calls
js-yaml'sload()orsafeLoad()functions — including user-uploaded files processed by thexlsxpipeline or direct YAML configuration endpoints. - Sink: The js-yaml flow collection parser internals in
node_modules/js-yamlversion5.2.1, reachable viayaml.load(untrustedInput)anywhere in the codebase. - Missing control: No upper bound on parsing time or input complexity; the parser's flow collection handling lacked protection against adversarial backtracking inputs.
- CWE: CWE-400 (Uncontrolled Resource Consumption) / CWE-1333 (Inefficient Regular Expression Complexity).
- Fix: Upgraded
js-yamlfrom5.2.1to5.2.2in bothpackage.jsonandpackage-lock.json, replacing the vulnerable parser with one that processes flow collections in bounded linear time.
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 js-yaml GHSA-pm4m-ph32-ghv5 vulnerability is a sharp reminder that denial-of-service risk doesn't always come from network-level attacks or authentication bypasses — sometimes it lives in a parser's handling of a single data type. In this case, YAML flow collections in js-yaml 5.2.1 contained a latent algorithmic complexity flaw that could be triggered by any attacker who could get a crafted string to yaml.load(). The fix is minimal — a one-version bump to 5.2.2 — but the discipline required to catch it (automated scanning, locked dependencies, and prompt patching) is what separates secure software from vulnerable software.
Keep your lockfiles current, scan your dependencies in CI, and treat your YAML parser as a potential attack surface whenever it touches untrusted data.
References
- CWE-400: Uncontrolled Resource Consumption
- CWE-1333: Inefficient Regular Expression Complexity
- OWASP Denial of Service Cheat Sheet
- OWASP A06:2021 – Vulnerable and Outdated Components
- js-yaml npm package (official)
- GHSA-pm4m-ph32-ghv5 Advisory
- Semgrep rules for vulnerable dependency detection
- fix: upgrade js-yaml to 5.2.2 (GHSA-pm4m-ph32-ghv5)