Introduction
In the Audex desktop music player repository (audex-player), Orbis AppSec's Trivy scanner flagged a high-severity vulnerability in package-lock.json: the project's dependency on js-yaml@4.1.1 exposed the application to GHSA-5p4m-2wfm-xmqj, a quadratic CPU consumption attack through crafted !!omap (ordered map) YAML sequences.
This isn't a theoretical concern. Audex is an Electron-based application — meaning it runs a full Node.js runtime on the user's desktop. If any part of the application parses YAML from an untrusted source (configuration files, playlist metadata, plugin manifests), an attacker could craft a payload that locks up the application's main thread, rendering the music player completely unresponsive. The vulnerability existed in js-yaml's !!omap type resolver, where duplicate-key validation used an O(n²) algorithm that could be weaponized with a relatively small input document.
The Vulnerability Explained
What Is !!omap in YAML?
YAML's !!omap tag represents an ordered mapping — essentially an array of key-value pairs where order matters and duplicate keys are forbidden. When js-yaml encounters !!omap during parsing, it must validate that no keys are duplicated. Here's where the problem lies.
The Quadratic Blowup
In js-yaml versions prior to 4.3.1 (and 3.15.1 in the 3.x line), the !!omap type resolver checked for duplicate keys using a nested comparison loop. For each new key encountered, the resolver compared it against every previously seen key. This is classic O(n²) behavior:
- 100 keys → ~10,000 comparisons
- 1,000 keys → ~1,000,000 comparisons
- 10,000 keys → ~100,000,000 comparisons
An attacker doesn't need a massive payload. A YAML document with just a few thousand unique !!omap entries — perhaps 50–100 KB of text — can consume seconds to minutes of CPU time, effectively freezing the Node.js event loop.
Attack Scenario Against Audex
Consider this attack scenario specific to the Audex player:
# Malicious playlist metadata file
!!omap
- key_0001: "track data"
- key_0002: "track data"
- key_0003: "track data"
# ... thousands more unique keys ...
- key_9999: "track data"
If Audex loads a playlist file, configuration, or any YAML-formatted data that passes through js-yaml.load(), this payload would cause the Electron main process to hang. The user sees a frozen window, and on some operating systems, the OS may prompt to kill the unresponsive application. This is a local denial of service — and in scenarios where YAML is fetched from a remote source (e.g., shared playlists), it becomes a remote denial of service.
The Vulnerable Dependency in package-lock.json
The pinned version before the fix:
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="
}
Version 4.1.1 contains the vulnerable !!omap resolver. The CVE-2026-59870 fix was applied upstream but had not been backported to the versions Audex was using, leaving the application exposed.
The Fix
The fix is a targeted dependency upgrade across two files: package.json and package-lock.json.
Before (Vulnerable)
// package-lock.json
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
}
}
After (Patched)
// package-lock.json
"node_modules/js-yaml": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
}
}
What Changed Internally in js-yaml 4.3.1
The patched version replaces the O(n²) nested-loop duplicate detection in the !!omap resolver with a hash-set-based approach (O(n) amortized). Instead of comparing each new key against all previous keys in a linear scan, the resolver now inserts keys into a Set or object-based lookup, making duplicate detection constant-time per key.
Additional Changes in the PR
The PR also includes minor formatting normalizations in package.json:
- Unicode character encoding: The em dash (
—) in the description field was encoded as\u2014, and the copyright symbol (©) as\u00a9. These are cosmetic normalizations that occur duringnpm installregeneration. - Array formatting: The
linux.targetarray was reformatted from inline["AppImage", "deb"]to a multi-line format. This is standard npm lockfile regeneration behavior.
These changes are side effects of running npm install with the updated dependency and do not affect application behavior.
Prevention & Best Practices
1. Pin and Audit Dependencies Regularly
# Run npm audit regularly
npm audit
# Use Trivy for comprehensive scanning
trivy fs --scanners vuln .
The js-yaml vulnerability sat in package-lock.json as a transitive or direct dependency. Regular auditing catches these before they're exploited.
2. Set Input Size Limits on YAML Parsing
Even with patched libraries, defense-in-depth matters:
const yaml = require('js-yaml');
const MAX_YAML_SIZE = 1024 * 1024; // 1 MB limit
function safeParseYaml(input) {
if (input.length > MAX_YAML_SIZE) {
throw new Error('YAML input exceeds maximum allowed size');
}
return yaml.load(input);
}
3. Use Schema Restrictions
If you don't need !!omap or other advanced YAML types, restrict the schema:
// Only allow core YAML types — no !!omap, !!pairs, etc.
const result = yaml.load(input, { schema: yaml.CORE_SCHEMA });
4. Implement Parsing Timeouts for Electron Apps
In Electron applications, parse untrusted data in a worker thread with a timeout:
const { Worker } = require('worker_threads');
function parseYamlWithTimeout(input, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
const worker = new Worker('./yaml-worker.js', { workerData: input });
const timer = setTimeout(() => {
worker.terminate();
reject(new Error('YAML parsing timed out'));
}, timeoutMs);
worker.on('message', (result) => { clearTimeout(timer); resolve(result); });
worker.on('error', (err) => { clearTimeout(timer); reject(err); });
});
}
5. Automate Dependency Updates
Use tools like Dependabot, Renovate, or Orbis AppSec to automatically detect and patch vulnerable dependencies before they reach production.
Key Takeaways
- js-yaml's
!!omapresolver prior to 4.3.1 used O(n²) duplicate-key checking, making it trivial to craft a small YAML document that consumes disproportionate CPU time — a classic algorithmic complexity attack. - Electron applications are especially vulnerable to event-loop-blocking attacks because a frozen main thread means a frozen UI, turning a parsing bug into a full application denial of service.
- The
package-lock.jsonpinned js-yaml at 4.1.1, which did not include the CVE-2026-59870 backport — demonstrating why lockfile auditing is essential, not justpackage.jsonreview. - Schema restriction (
yaml.CORE_SCHEMA) would have mitigated this vulnerability even without the upgrade, since!!omapis not part of the core YAML schema. - A 50 KB YAML payload could freeze an application for minutes — input size alone is not a reliable defense against algorithmic complexity attacks; the algorithm itself must be efficient.
How Orbis AppSec Detected This
- Source: YAML content parsed by the
js-yamllibrary (version 4.1.1) as declared inpackage-lock.jsonof the Audex player project - Sink: The
!!omaptype resolver internal tojs-yaml@4.1.1, which performs O(n²) duplicate-key validation duringyaml.load()calls - Missing control: No upgrade to the patched js-yaml version (4.3.1/3.15.1) that replaces the quadratic algorithm with linear-time duplicate detection; no input size limits or schema restrictions on YAML parsing
- CWE: CWE-400 (Uncontrolled Resource Consumption)
- Fix: Upgraded js-yaml from 4.1.1 to 4.3.1 in both
package.jsonandpackage-lock.json, replacing the vulnerable!!omapresolver with the patched version that uses O(n) duplicate-key detection
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 !!omap quadratic CPU consumption vulnerability (GHSA-5p4m-2wfm-xmqj) is a textbook example of how algorithmic complexity bugs in widely-used parsing libraries can create real denial-of-service risks — especially in Electron applications where the main thread is sacred. The fix was straightforward: upgrade js-yaml from 4.1.1 to 4.3.1. But the lesson runs deeper. Dependency lockfiles deserve the same security scrutiny as application code, schema restrictions should be applied to all parsers handling untrusted input, and automated scanning tools are essential for catching vulnerabilities that hide in transitive dependency trees.
Don't wait for an attacker to freeze your application with a 50 KB YAML file. Audit your dependencies today.