The Problem Hidden in Your Lock File
Most developers never think twice about yarn.lock. It's auto-generated, committed once, and largely ignored. But inside that file, a single pinned version of a utility package called braces was quietly carrying a high-severity denial-of-service vulnerability—CVE-2024-4068.
braces is one of those invisible workhorses of the Node.js ecosystem. It handles brace expansion: turning shorthand patterns like {a,b,c} or {1..100} into their full list of strings. It underpins micromatch, glob, fast-glob, and dozens of other tools your build pipeline almost certainly depends on. In version 3.0.2, it had a critical flaw: it placed no upper bound on the number of characters or expansion steps it would process.
This post walks through exactly what that means, how an attacker could exploit it, and the precise changes made to close the hole.
The Vulnerability Explained
What braces Does (and Where It Goes Wrong)
Brace expansion takes a pattern and produces an array of strings:
const braces = require('braces');
// Normal use
braces('{a,b,c}'); // ['a', 'b', 'c']
braces('{1..5}'); // ['1', '2', '3', '4', '5']
// The dangerous case in braces 3.0.2
braces('{1..10000000}'); // Attempts to generate 10,000,000 strings
// No limit enforced — event loop blocked
The vulnerability is not in a regex per se, but in the algorithmic complexity of the expansion itself. When braces processes a range like {1..10000000}, it delegates to fill-range to generate every integer in that range. In fill-range 7.0.1, this is done without any guard on the total number of values produced. The result: a single function call can consume gigabytes of memory and 100% of one CPU core for an extended period.
Because Node.js runs JavaScript on a single-threaded event loop, blocking that loop—even briefly—denies service to every other concurrent request. A sufficiently large range can block it for seconds, minutes, or indefinitely.
The Vulnerable Dependency Chain
The yarn.lock snapshot before the fix shows the problem clearly:
# BEFORE (vulnerable)
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
Two packages are involved:
braces3.0.2 — the entry point that parses brace-expansion patternsfill-range7.0.1 — the librarybracescalls to generate numeric ranges, with no iteration limit
A Concrete Attack Scenario
Imagine an application that accepts a glob pattern from a user to filter files, or a build tool that processes user-supplied template strings. Any code path that passes untrusted input into braces() (directly or through micromatch, glob, or similar) is exploitable:
// Hypothetical vulnerable endpoint
app.post('/search', (req, res) => {
const pattern = req.body.pattern; // User-controlled input
const matches = micromatch(fileList, pattern); // Internally calls braces()
res.json(matches);
});
// Attacker sends:
// POST /search
// { "pattern": "{1..99999999}" }
// → Server event loop blocked, all other requests time out
The attacker doesn't need authentication. A single HTTP request with a malicious pattern is enough to take down the service.
Real-world impact for this application: Even though the scanner assessed the vulnerability as "present in dependency tree, not confirmed reachable," the risk is real any time user-influenced strings flow through the dependency chain that includes braces. The attack surface is broad because braces is a transitive dependency of many common tools.
The Fix
What Changed and Why
The fix required updates to two packages and two files:
1. yarn.lock — Upgrading Both braces and fill-range
# BEFORE
-braces@^3.0.2, braces@~3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
- integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
- dependencies:
- fill-range "^7.0.1"
# AFTER
+braces@3.0.3, braces@^3.0.2, braces@~3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
# BEFORE
-fill-range@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#..."
- integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
# AFTER
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#..."
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
braces 3.0.3 now requires fill-range ^7.1.1 (up from ^7.0.1). The new fill-range 7.1.1 introduces the actual defensive logic: it checks the size of the range before attempting to generate values and throws an error (or returns safely) if the expansion would exceed a safe threshold. This breaks the attack at the point where unbounded work would begin.
2. package.json — The Resolution Pin
Simply updating yarn.lock is not enough. Yarn resolves versions based on package.json constraints from all packages in the tree. Without a resolution pin, a future yarn install could silently re-resolve braces back to 3.0.2 if any transitive dependency still specifies braces@~3.0.2.
The fix adds a resolutions field to package.json:
# package.json
"volta": {
"node": "24.11.0",
"yarn": "1.22.22"
+ },
+ "resolutions": {
+ "braces": "3.0.3"
}
The resolutions field is a Yarn 1.x feature that forces all packages in the dependency tree—regardless of what version they request—to receive exactly braces@3.0.3. This is the correct defense-in-depth approach: even if a transitive dependency is never updated to request ^3.0.3, the resolution override ensures the patched version is always used.
Before vs. After: The Security Boundary
| Before (3.0.2) | After (3.0.3) | |
|---|---|---|
Input {1..100} |
Expands normally | Expands normally |
Input {1..10000000} |
Blocks event loop | Throws / returns safely |
fill-range version |
7.0.1 (no limit) | 7.1.1 (enforces limit) |
Pinned in package.json |
No | Yes (via resolutions) |
The fix is backward compatible: valid, reasonably-sized brace expressions continue to work exactly as before. Only pathologically large inputs are now rejected.
Prevention & Best Practices
1. Run SCA (Software Composition Analysis) on Every Commit
This vulnerability was caught by Trivy scanning yarn.lock. SCA tools compare your locked dependency versions against CVE databases and flag known-vulnerable packages. Integrate Trivy, Snyk, or npm audit / yarn audit into your CI pipeline so new vulnerabilities are caught before they reach production.
# Quick check with yarn
yarn audit --level high
# Or with Trivy
trivy fs --scanners vuln yarn.lock
2. Use resolutions (Yarn) or overrides (npm) for Transitive Dependencies
When a vulnerability is in a transitive dependency you don't control directly, use your package manager's override mechanism:
// Yarn 1.x
"resolutions": {
"braces": "3.0.3"
}
// npm 8.3+ / package.json
"overrides": {
"braces": "3.0.3"
}
This guarantees the patched version is used everywhere in the tree, not just where you have a direct dependency.
3. Validate and Cap User-Supplied Glob/Pattern Strings
Never pass raw user input into pattern-expansion libraries without validation:
// Dangerous
const results = micromatch(files, req.body.pattern);
// Safer
const MAX_PATTERN_LENGTH = 200;
const pattern = String(req.body.pattern || '');
if (pattern.length > MAX_PATTERN_LENGTH) {
return res.status(400).json({ error: 'Pattern too long' });
}
const results = micromatch(files, pattern);
Defense in depth: validate at the application layer and rely on the library's own guards.
4. Understand Your Transitive Dependency Tree
Run yarn why braces or npm explain braces to see every package that depends on braces. This tells you your actual attack surface and which packages would need updating if a new vulnerability were found.
$ yarn why braces
# => fast-glob > micromatch > braces
# => chokidar > anymatch > micromatch > braces
5. Relevant Security Standards
- CWE-1333: Inefficient Regular Expression Complexity — the canonical classification for ReDoS and algorithmic complexity attacks
- CWE-400: Uncontrolled Resource Consumption — applies when there is no cap on the work performed per input
- OWASP: Denial of Service Cheat Sheet — covers resource exhaustion attack patterns
Key Takeaways
braces3.0.2 andfill-range7.0.1 must both be upgraded — the vulnerability spans two packages in a chain, and patching only one is insufficient.- A
resolutionspin inpackage.jsonis required for Yarn 1.x projects to prevent futureyarn installruns from silently re-resolving back to the vulnerable version. - Brace-expansion patterns are a non-obvious attack surface: unlike SQL injection or XSS, this attack vector is easy to miss in code review because
bracesis almost always a transitive, invisible dependency. - The event-loop threading model of Node.js amplifies this risk: a single blocked call denies service to all concurrent users, making ReDoS particularly dangerous in server-side JavaScript.
- Trivy's SCA scanning of
yarn.lockcaught this without any source-code analysis — demonstrating that dependency scanning alone, applied to lock files, is a high-value, low-cost security control.
How Orbis AppSec Detected This
- Source: The
bracespackage receives expansion patterns that may be influenced by user input flowing through libraries such asmicromatchorglobin the application's dependency tree. - Sink: The
fill-rangefunction called internally bybraces@3.0.2(resolved inyarn.lockat line ~587) performs unbounded numeric range expansion with no iteration or character limit. - Missing control: Neither
braces3.0.2 norfill-range7.0.1 enforced any maximum on the number of values to generate or the total characters to process, leaving the expansion loop open to resource exhaustion. - CWE: CWE-1333 (Inefficient Regular Expression Complexity) / CWE-400 (Uncontrolled Resource Consumption)
- Fix: Upgraded
bracesto 3.0.3 andfill-rangeto 7.1.1 (which add internal input-size guards), and pinned the resolution inpackage.jsonto ensure all transitive dependents receive the patched version.
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
CVE-2024-4068 is a reminder that denial-of-service vulnerabilities don't require exotic techniques—sometimes a single string with a large numeric range is enough to take down a Node.js service. The braces package is embedded so deeply in the JavaScript tooling ecosystem that most projects are exposed without knowing it.
The fix is surgical and safe: upgrading to braces 3.0.3 and fill-range 7.1.1 adds the missing input-size guards while leaving all valid patterns unaffected. Pinning the version via resolutions in package.json ensures the fix holds across future installs. And integrating SCA scanning into your CI pipeline means you'll catch the next vulnerable transitive dependency before it ever ships.
Lock files are not just bookkeeping—they are a security artifact. Treat them accordingly.