How Denial of Service Happens in Node.js Dependency Trees and How to Fix It
The package-lock.json file in any non-trivial Node.js project is a sprawling graph of direct and transitive dependencies — and buried inside that graph, a single vulnerable package version can expose your entire application to attack. This post walks through exactly how CVE-2026-13149 works in the brace-expansion package, how it was hiding inside the @sentry/bundler-plugin-core subtree, and what the fix looks like at the lock-file level.
The Vulnerability Explained
What Is brace-expansion and Why Does It Matter?
brace-expansion is a small but widely-used npm package that implements POSIX brace expansion for glob patterns — the same syntax you use when you write src/{components,pages}/**/*.ts in a build tool. It is a dependency of minimatch, which is in turn a dependency of glob, which is used by virtually every build tool, linter, and bundler in the Node.js ecosystem.
Because it sits so deep in the dependency tree, most developers never think about it. That invisibility is exactly what makes CVE-2026-13149 dangerous.
The Root Cause: Exponential-Time Complexity
The vulnerability is classified under CWE-1333 (Inefficient Regular Expression Complexity), but it is not strictly a regex issue — it is an algorithmic complexity problem in how brace patterns are parsed and expanded.
Consider a brace expression like:
{a,b}{c,d}{e,f}{g,h}{i,j}{k,l}{n,m}{o,p}{q,r}{s,t}
Expanding this legitimately produces 2¹⁰ = 1,024 combinations. That is fine. But in the vulnerable versions of brace-expansion, certain malformed or deeply nested inputs cause the expansion to grow exponentially in processing time, not just in output size. An attacker can craft an input that takes milliseconds to type but seconds — or minutes — to process.
In a Node.js application, the event loop is single-threaded. If a synchronous call to brace-expansion blocks for even a few seconds, the entire server becomes unresponsive to all other requests. This is a classic Denial of Service via algorithmic complexity, sometimes called a "Billion Laughs"-style attack.
The Vulnerable Code Path in This Repository
The Trivy scanner identified that the project was resolving brace-expansion version 5.0.6 through the following path:
@sentry/bundler-plugin-core
└── glob@13.0.6
└── minimatch
└── brace-expansion@5.0.6 ← VULNERABLE
└── balanced-match@4.0.4
The package-lock.json contained a dedicated nested resolution for this path:
"node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU...",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
}
This explicit nested entry meant that even if the top-level brace-expansion was patched, this subtree would continue using version 5.0.6 — the vulnerable one.
Attack Scenario
Imagine a scenario where your Next.js application uses a Server Action that accepts a file glob pattern from the user (e.g., to preview matching files in a project template). Under the hood, that pattern is passed to a glob call, which internally uses minimatch, which uses brace-expansion. An attacker submits a crafted pattern like:
{{{{{{{{{{{{{{{{{{{{a,b},c},d},e},f},g},h},i},j},k},l},m},n},o},p},q},r},s},t},u}
The vulnerable brace-expansion@5.0.6 begins expanding this and never finishes in any reasonable time. The Node.js event loop freezes. Every other request to the server — including health checks — times out. The application is effectively down.
The Fix
What Changed in package-lock.json
The fix makes two coordinated changes to the lock file:
Removed — the top-level nested brace-expansion@5.0.6 and its companion balanced-match@4.0.4 under @sentry/bundler-plugin-core:
- "node_modules/@sentry/bundler-plugin-core/node_modules/balanced-match": {
- "version": "4.0.4",
- ...
- },
- "node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": {
- "version": "5.0.6",
- ...
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- },
Added — a more deeply scoped resolution under minimatch specifically, pinning to the patched brace-expansion@2.1.2 with balanced-match@1.0.2:
+ "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch/node_modules/brace-expansion": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
+ ...
+ },
Why This Specific Structure?
The new layout scopes the patched brace-expansion@2.1.2 directly under minimatch's own node_modules directory. This is more precise than the previous approach: instead of overriding brace-expansion for all of @sentry/bundler-plugin-core, it targets exactly the minimatch package that was consuming the vulnerable version. The balanced-match companion is also downgraded from 4.0.4 to 1.0.2, which is the version compatible with the 2.x series of brace-expansion.
The result is a dependency tree where:
@sentry/bundler-plugin-core
└── glob@13.0.6
└── minimatch
├── node_modules/balanced-match@1.0.2 ← PATCHED
└── node_modules/brace-expansion@2.1.2 ← PATCHED
Version 2.1.2 of brace-expansion includes a fix that bounds the expansion algorithm to polynomial time, eliminating the exponential blowup for malformed inputs.
Prevention & Best Practices
1. Run npm audit in CI
Add npm audit --audit-level=high as a required step in your CI pipeline. This catches known-vulnerable transitive dependencies before they reach production.
npm audit --audit-level=high
2. Use Lock-File Overrides for Deep Transitive Fixes
When a vulnerable package is buried deep in a dependency tree and you cannot wait for upstream maintainers to update, use npm's overrides field in package.json:
{
"overrides": {
"brace-expansion": ">=2.1.2"
}
}
This forces all resolutions of brace-expansion — regardless of depth — to use a version satisfying the constraint.
3. Scan with Trivy or Snyk Regularly
Trivy (the scanner that caught this issue) can be run locally or in CI:
trivy fs --scanners vuln .
It inspects package-lock.json and flags vulnerable transitive dependencies by CVE ID.
4. Avoid Passing Untrusted Input to Glob Functions
Even with patched libraries, treat glob patterns from user input as untrusted. Validate or sanitize them before passing to glob, minimatch, or any brace-expansion-consuming function:
// Bad: passing raw user input
const files = await glob(req.body.pattern);
// Better: validate against an allowlist of safe characters
const SAFE_GLOB = /^[a-zA-Z0-9/_\-.*{}?,\[\]]+$/;
if (!SAFE_GLOB.test(req.body.pattern)) {
throw new Error('Invalid glob pattern');
}
const files = await glob(req.body.pattern);
5. Security Standards
- OWASP A06:2021 – Vulnerable and Outdated Components: This vulnerability is a textbook example of why dependency hygiene matters.
- CWE-1333: Inefficient Regular Expression Complexity — applicable to any algorithmic complexity attack, not just regex.
Key Takeaways
brace-expansion@5.0.6inside@sentry/bundler-plugin-core's subtree was the specific vulnerable instance — a reminder that the same package can appear multiple times in a lock file at different versions, and each instance must be audited.- Removing the broad
@sentry/bundler-plugin-core/node_modules/brace-expansionoverride and replacing it with a scopedminimatch-level override is a more surgical and maintainable fix than blanket version pinning. - Transitive dependencies are attack surface. A package you never import directly can still be the vector for a production outage.
balanced-matchversion matters too — thebrace-expansion@2.xseries requiresbalanced-match@1.0.2, not4.0.4, and getting this pairing wrong would break the fix.- Algorithmic complexity attacks require no authentication. Any endpoint that processes user-supplied strings through a vulnerable code path is exposed.
How Orbis AppSec Detected This
- Source: Untrusted glob or file-pattern strings entering the application (e.g., via HTTP request bodies processed by Next.js Server Actions or build-tool APIs).
- Sink: The
brace-expansionexpansion algorithm invoked transitively throughminimatch→glob→@sentry/bundler-plugin-core, as resolved bynode_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion@5.0.6. - Missing control: No upper bound on expansion complexity; the vulnerable version lacked protection against exponential-time inputs.
- CWE: CWE-1333 — Inefficient Regular Expression Complexity.
- Fix: Replaced the
brace-expansion@5.0.6nested resolution under@sentry/bundler-plugin-corewith a more precisely scopedbrace-expansion@2.1.2entry underminimatch's own node_modules, paired with the compatiblebalanced-match@1.0.2.
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-2026-13149 is a sharp reminder that your application's security posture is only as strong as its deepest transitive dependency. The brace-expansion package is invisible to most developers — it appears nowhere in their own code — yet a single vulnerable version nested inside @sentry/bundler-plugin-core was enough to expose the entire application to a Denial of Service attack.
The fix is precise: remove the broad nested override for brace-expansion@5.0.6, introduce a scoped resolution for brace-expansion@2.1.2 directly under minimatch, and pair it with the correct balanced-match@1.0.2. This surgical approach ensures the vulnerable code path is eliminated without disrupting other parts of the dependency graph.
Make dependency scanning a first-class citizen of your CI pipeline. Run npm audit, integrate Trivy, and use overrides in package.json when you need to force a safe version across the entire tree. The cost of prevention is a few minutes of configuration; the cost of a production DoS is measured in downtime, reputation, and revenue.