Introduction
The package-lock.json file in a Node.js project is often treated as a boring implementation detail — a machine-generated lockfile that nobody reads. But it is precisely because nobody reads it that dangerous vulnerability patterns can hide inside it for months. In this repository, the Trivy scanner surfaced two separate pinned copies of brace-expansion@2.1.2 buried deep in the nested dependency trees of @sentry/bundler-plugin-core and @typescript-eslint/typescript-estree. Both copies were vulnerable to CVE-2026-14257, a high-severity denial-of-service flaw that allows an attacker to exhaust server CPU by feeding a malicious brace pattern to the library.
This post walks through exactly where those copies lived, why they were dangerous, how the fix removes them, and what you can do to prevent similar issues in your own projects.
The Vulnerability Explained
What is brace-expansion?
brace-expansion is a tiny but widely-used npm package that implements POSIX-style brace expansion — the same feature your shell uses when you type cp file.{js,ts,json}. It is a direct dependency of minimatch, which is in turn used by virtually every glob-matching library in the Node.js ecosystem.
The CVE-2026-14257 Flaw
In brace-expansion versions through 5.0.7, the expansion algorithm does not place any upper bound on the number of strings it will generate from a single input pattern. Consider a pattern like:
{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}
Each {a,b} doubles the output. Twenty of them produce 2²⁰ = 1,048,576 strings. Thirty produce over a billion. The library will happily attempt to allocate and return all of them, saturating the event loop and exhausting heap memory.
This is a classic CWE-400: Uncontrolled Resource Consumption pattern — the library trusts that its caller will only pass it reasonable input, but makes no defensive check itself.
Where the Vulnerable Code Lived in This Repository
The tricky part here is that the top-level project might not directly depend on brace-expansion. The vulnerability existed in two nested locations inside package-lock.json:
Location 1 — @sentry/bundler-plugin-core's private minimatch copy:
"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",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
}
Location 2 — @typescript-eslint/typescript-estree's private minimatch copy:
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+K..."
}
Both entries pin brace-expansion at exactly 2.1.2 — a version that predates the security fix. Because package-lock.json is authoritative, npm ci will always install this exact version regardless of what the parent package's package.json says.
Attack Scenario
Imagine a CI/CD pipeline or a developer tooling server that:
- Accepts a user-supplied file glob pattern (e.g., through a build configuration API or a lint-on-save editor plugin).
- Passes that pattern to
minimatchor a glob library that internally callsbrace-expansion.
An attacker who can influence that pattern — even indirectly, through a crafted .eslintrc file in a pull request or a malicious Sentry source-map configuration — could submit:
{0..9}{0..9}{0..9}{0..9}{0..9}{0..9}{0..9}{0..9}
This single pattern expands to 10⁸ = 100 million strings. The Node.js process will attempt to build that array, spike to 100% CPU, and become unresponsive. In a shared CI environment, this could block all pipelines.
The Fix
What Changed
The fix is surgical: it removes the two pinned nested brace-expansion@2.1.2 entries (and their associated balanced-match copies) from package-lock.json. With those entries gone, npm's dependency resolution algorithm is free to satisfy the brace-expansion requirement using a patched version from higher up in the tree.
Before (vulnerable — two blocks removed):
- "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch/node_modules/balanced-match": {
- "version": "1.0.2",
- ...
- },
- "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",
- "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/balanced-match": {
- "version": "1.0.2",
- ...
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
- "integrity": "sha512-w5JZcKgdhDOgOwm8H+K...",
- ...
- },
After (safe): These blocks are simply absent. npm now resolves brace-expansion for these paths to one of the patched releases: 1.1.17, 2.1.3, 3.0.3, or 5.0.8 — all of which include the fix that limits expansion output size.
Why Removing the Nested Entry Is the Right Fix
npm's lockfile nests a private copy of a package when it cannot satisfy a version range using an already-installed ancestor. By removing the nested pin, we allow npm to hoist the dependency resolution upward and reuse a patched version that already satisfies the semver range. The consuming code in minimatch doesn't care which patch version of brace-expansion it gets — it only requires ^2.0.0 or similar — so using 2.1.3 instead of 2.1.2 is a fully backward-compatible substitution.
Optionally Enforcing the Fix with npm Overrides
For extra safety, you can add a top-level overrides block to package.json to force all transitive consumers to use the patched version, even if future installs re-introduce a nested copy:
{
"overrides": {
"brace-expansion": "^2.1.3"
}
}
This acts as a belt-and-suspenders measure alongside the lockfile fix.
Prevention & Best Practices
1. Run a Vulnerability Scanner in CI
Tools like Trivy, npm audit, Snyk, and Socket inspect package-lock.json (not just package.json) and will flag vulnerable nested copies that a simple npm outdated would miss. Add one of these to your CI pipeline as a required check.
# Example GitHub Actions step
- name: Run Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'HIGH,CRITICAL'
exit-code: '1'
2. Use npm Overrides for Transitive Pinning
When a vulnerable package is deep in the dependency tree and you can't wait for upstream maintainers to release a fix, use overrides (npm ≥8) or resolutions (Yarn) to force a safe version across the entire tree.
3. Audit Your Lockfile Regularly
package-lock.json can grow stale. Run npm audit and npm dedupe periodically. The npm dedupe command collapses duplicate nested copies where semver ranges permit, reducing both attack surface and bundle size.
4. Validate Glob Inputs at the Application Layer
If your application accepts user-supplied glob or file-path patterns, validate them before passing to minimatch or similar libraries:
// Reject patterns with excessive brace nesting
function isSafeGlob(pattern) {
const braceCount = (pattern.match(/\{/g) || []).length;
if (braceCount > 10) throw new Error('Pattern too complex');
return pattern;
}
This defense-in-depth measure protects you even if the underlying library has an unpatched vulnerability.
5. Reference Security Standards
- OWASP A06:2021 — Vulnerable and Outdated Components directly addresses this scenario: transitive dependencies with known CVEs.
- CWE-400: Uncontrolled Resource Consumption is the root-cause classification for this type of DoS.
- NIST NVD entry for CVE-2026-14257 provides the official severity score and affected version range.
Key Takeaways
- Nested
package-lock.jsonentries can pin vulnerable versions invisibly. The twobrace-expansion@2.1.2copies were hidden four levels deep under@sentry/bundler-plugin-coreand@typescript-eslint/typescript-estree— invisible tonpm outdatedand easy to miss in manual review. - Removing a pinned nested entry is often safer than patching it in place. Deleting the block lets npm's resolver find the best compatible patched version automatically, rather than requiring you to manually craft the correct integrity hash.
- Transitive dev dependencies are still attack surface. Both vulnerable copies lived under tooling packages (
@sentry/bundler-plugin-core,@typescript-eslint/typescript-estree). If these tools run in environments that process untrusted input — like CI pipelines that lint contributor PRs — the DoS risk is real. - brace-expansion's
^2.1.2semver range allowed a safe in-place upgrade to2.1.3without any API changes, demonstrating why semantic versioning patch releases exist. - Trivy's filesystem scan mode (
trivy fs .) catches lockfile vulnerabilities that SAST tools focused on source code would miss entirely.
How Orbis AppSec Detected This
- Source: The vulnerable
brace-expansion@2.1.2package version, as recorded in two nested entries withinpackage-lock.json, constitutes the tainted dependency. - Sink: Any call path in
@sentry/bundler-plugin-coreor@typescript-eslint/typescript-estreethat invokesminimatch()with externally influenced pattern strings ultimately reaches the unguarded expansion loop insidebrace-expansion/index.js. - Missing control: No upper bound on the number of expanded strings; no input length or complexity validation before invoking the expander.
- CWE: CWE-400 — Uncontrolled Resource Consumption.
- Fix: Removed the two pinned
brace-expansion@2.1.2nested entries (and their associatedbalanced-match@1.0.2copies) frompackage-lock.json, allowing npm to resolve both paths to a patched release (≥2.1.3).
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-14257 is a reminder that the real attack surface of a modern Node.js application is not just the code you write — it is the entire transitive dependency graph locked in package-lock.json. A single two-line package entry, nested four levels deep under a Sentry bundler plugin, was enough to expose the application to CPU exhaustion attacks. The fix required removing fewer than 30 lines from the lockfile, but finding those lines required a dedicated scanner that understands nested dependency resolution.
Make vulnerability scanning of your lockfile a first-class citizen in your CI pipeline. Treat package-lock.json as security-relevant configuration, not just build plumbing. And when a scanner flags a nested copy of a package, don't dismiss it as "not directly reachable" — trace the call path, understand the risk, and remove the pin.