Back to Blog
high SEVERITY9 min read

How Regular Expression Denial of Service happens in Node.js picomatch extglob patterns and how to fix it

A Regular Expression Denial of Service (ReDoS) vulnerability in picomatch versions prior to 2.3.2, 3.0.2, and 4.0.4 allowed attackers to craft malicious extglob patterns that triggered catastrophic backtracking in the regex engine, potentially freezing Node.js applications. The fix, tracked as CVE-2026-33671, involved upgrading picomatch to patched versions and pinning the dependency explicitly in `package.json` to ensure the safe version is resolved across the dependency tree.

O
By Orbis AppSec
Published July 9, 2026Reviewed July 9, 2026

Answer Summary

CVE-2026-33671 is a Regular Expression Denial of Service (ReDoS) vulnerability in the picomatch glob-matching library for Node.js, classified under CWE-1333 (Inefficient Regular Expression Complexity). Versions prior to 2.3.2, 3.0.2, and 4.0.4 contained extglob pattern parsing logic that could be exploited with specially crafted input strings to cause catastrophic regex backtracking, effectively hanging the event loop. The fix upgrades picomatch to the patched versions and explicitly pins `picomatch: "2.3.2"` in `package.json` to override vulnerable transitive dependency resolutions.

Vulnerability at a Glance

cweCWE-1333
fixUpgrade picomatch to 2.3.2 / 3.0.2 / 4.0.4 and pin the dependency in package.json
riskAttacker-controlled input can freeze the Node.js event loop, causing a denial of service
languageJavaScript / Node.js
root causeInefficient regex generated from extglob patterns in picomatch allows catastrophic backtracking
vulnerabilityRegular Expression Denial of Service (ReDoS) via extglob patterns

How Regular Expression Denial of Service happens in Node.js picomatch extglob patterns and how to fix it


Summary

A Regular Expression Denial of Service (ReDoS) vulnerability in picomatch — one of the most widely used glob-matching libraries in the Node.js ecosystem — allowed attackers to craft malicious extglob patterns that triggered catastrophic backtracking in the JavaScript regex engine. Tracked as CVE-2026-33671 with a HIGH severity rating, this flaw affected picomatch versions prior to 2.3.2, 3.0.2, and 4.0.4. The fix pins picomatch explicitly in package.json and upgrades all resolved versions in pnpm-lock.yaml to eliminate the vulnerable code paths.


Introduction

The pnpm-lock.yaml file in this repository was resolving picomatch@2.3.1 as a transitive dependency — a version containing a regex engine flaw that could be weaponized with a single crafted string. Picomatch is responsible for converting glob patterns (like **/*.js or !(foo|bar)) into regular expressions that Node.js can evaluate. The vulnerability lives specifically in its extglob handling: the extended glob syntax that supports patterns like +(foo), *(bar), and !(baz).

When picomatch processes these extglob patterns, it constructs a regular expression internally. In versions prior to the fix, certain combinations of nested or repeated extglob quantifiers produced regex structures susceptible to catastrophic backtracking — a condition where the regex engine's backtracking algorithm explores an exponentially growing number of possible match paths for a carefully constructed non-matching string.

For developers using build tools, bundlers, test runners, or file watchers (virtually any modern Node.js toolchain), picomatch is almost certainly somewhere in your dependency tree. That makes this a particularly broad-impact vulnerability.


The Vulnerability Explained

What Are Extglob Patterns?

Extglob (extended globbing) patterns are a superset of standard glob syntax. They support quantifier-like constructs:

Pattern Meaning
?(pattern) Match zero or one occurrence
*(pattern) Match zero or more occurrences
+(pattern) Match one or more occurrences
@(pattern) Match exactly one occurrence
!(pattern) Match anything except the pattern

These are powerful for file matching, but converting them to regular expressions is non-trivial. The bug in picomatch 2.3.1 arises from how it generates the internal regex for these patterns.

The Root Cause: Catastrophic Backtracking

Consider a simplified version of what picomatch 2.3.1 might generate for a nested extglob like +(a+(b)):

// Conceptual regex generated by vulnerable picomatch for nested extglob
/^(?:a(?:b)+)+$/

This kind of nested quantifier structure — (?:...)+ inside another (?:...)+ — is the classic recipe for catastrophic backtracking. When the regex engine tries to match a string like "aaaaaaaaaaaaaaaaaaaac" (many as followed by a non-matching character), it explores all possible ways to partition the as between the outer and inner groups before concluding there's no match. The number of paths explored grows exponentially with the length of the input.

The Vulnerable Lockfile Entry

Before the fix, pnpm-lock.yaml resolved picomatch to the vulnerable version:

# BEFORE (vulnerable)
picomatch@2.3.1:
  resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
  engines: {node: '>=8.6'}

And the vulnerable version 4.0.2 was also present:

# BEFORE (also vulnerable)
picomatch@4.0.2:
  resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
  engines: {node: '>=12'}

Attack Scenario

Imagine a web application that accepts user-supplied glob patterns for filtering files or assets — for example, a CI/CD dashboard, a file browser, or a build configuration API. The application passes user input through picomatch to evaluate matches:

const picomatch = require('picomatch'); // v2.3.1 — vulnerable

app.post('/filter-files', (req, res) => {
  const userPattern = req.body.pattern; // attacker-controlled
  const isMatch = picomatch(userPattern); // triggers regex compilation
  const results = files.filter(f => isMatch(f));
  res.json(results);
});

An attacker sends a POST request with a crafted extglob pattern:

POST /filter-files
{ "pattern": "+(a+(b+(c+(d+(e+(f+(g))))))))" }

When picomatch compiles this nested extglob into a regex and attempts to match it against even a short non-matching string, the JavaScript event loop blocks completely. Since Node.js is single-threaded, this freezes the entire server — no other requests are processed until the regex evaluation completes (which could take minutes or longer).

Even without direct user input, a compromised or malicious package in the supply chain could inject such a pattern into any code path that calls picomatch.


The Fix

The fix involved two coordinated changes across package.json and pnpm-lock.yaml.

1. Pinning picomatch in package.json

The most important change was explicitly adding picomatch as a direct dependency in package.json, overriding whatever version transitive dependencies would have resolved:

# package.json — BEFORE
"dependencies": {
  "shx": "^0.4.0"
}

# package.json — AFTER
"dependencies": {
+ "picomatch": "2.3.2",
  "shx": "^0.4.0"
}

By pinning "picomatch": "2.3.2" (an exact version, no ^ or ~), the project guarantees that the patched version is always used regardless of what upstream packages request. This is a critical technique for overriding vulnerable transitive dependencies in the npm/pnpm ecosystem.

2. Updating pnpm-lock.yaml

The lockfile was updated to reflect the new resolution, replacing the vulnerable 2.3.1 entry with 2.3.2 and removing the vulnerable 4.0.2 entry:

# pnpm-lock.yaml — BEFORE
-  picomatch@2.3.1:
-    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
-    engines: {node: '>=8.6'}
-
-  picomatch@4.0.2:
-    resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
-    engines: {node: '>=12'}

# pnpm-lock.yaml — AFTER
+  picomatch@2.3.2:
+    resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
+    engines: {node: '>=8.6'}

   picomatch@4.0.3:
     resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
     engines: {node: '>=12'}

Note that picomatch@4.0.3 (a patched version in the 4.x line) is retained in the lockfile for packages that specifically require the v4 API, while the v2 line is now resolved to the safe 2.3.2.

What Changed Inside picomatch 2.3.2?

The picomatch maintainers restructured the regex generation for extglob patterns to avoid producing nested quantifier structures that are susceptible to backtracking. The fix ensures that the compiled regex for patterns like +(a+(b)) uses atomic grouping or possessive quantifiers where available, or restructures the alternation logic to eliminate ambiguity that the backtracking engine would otherwise explore exhaustively.


Prevention & Best Practices

1. Audit Your Lockfile for Transitive Vulnerabilities

The vulnerability was detected in pnpm-lock.yaml, not in a direct dependency. Run dependency scanners regularly against your lockfile — not just package.json:

# Using Trivy (detected this CVE)
trivy fs --scanners vuln .

# Using npm audit (also catches many CVEs)
pnpm audit

# Using Snyk
snyk test

2. Pin Vulnerable Transitive Dependencies

When a transitive dependency has a known CVE and the direct dependent hasn't released a fix yet, pin the safe version explicitly:

// package.json
"dependencies": {
  "picomatch": "2.3.2"  // override transitive resolution
}

In pnpm, you can also use overrides in package.json for a cleaner approach:

"pnpm": {
  "overrides": {
    "picomatch": ">=2.3.2"
  }
}

3. Validate and Limit Glob Pattern Input

If your application accepts user-supplied glob or extglob patterns, apply these controls:

// Limit pattern length to prevent long backtracking inputs
const MAX_PATTERN_LENGTH = 200;
if (userPattern.length > MAX_PATTERN_LENGTH) {
  throw new Error('Pattern too long');
}

// Optionally, use a timeout wrapper with worker threads for untrusted patterns

4. Consider Linear-Time Alternatives for Untrusted Input

For applications that must evaluate regex or glob patterns from untrusted sources, consider using RE2 — a linear-time regex engine that eliminates catastrophic backtracking by design:

const RE2 = require('re2');
// RE2 guarantees O(n) matching time regardless of pattern complexity

5. Enable Automated Dependency Scanning in CI

Add a scanning step to your CI pipeline so vulnerable versions are caught before they reach production:

# .github/workflows/security.yml
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    scan-ref: '.'
    severity: 'HIGH,CRITICAL'
    exit-code: '1'

Security Standards Reference

  • CWE-1333: Inefficient Regular Expression Complexity — directly applicable to this vulnerability
  • OWASP Input Validation Cheat Sheet: Recommends validating and sanitizing all input, including pattern strings
  • OWASP Dependency Check: Recommends scanning all transitive dependencies, not just direct ones

Key Takeaways

  • Extglob patterns in picomatch 2.3.1 and 4.0.2 produce regexes with nested quantifiers — the specific structure (?:...(?:...)+)+ that causes catastrophic backtracking with crafted non-matching strings.
  • Transitive dependencies are attack surface too — this CVE was hiding in pnpm-lock.yaml as a resolved transitive dep, invisible to a simple package.json review.
  • Pinning "picomatch": "2.3.2" as a direct dependency is the correct pnpm/npm pattern for forcing a safe version across the entire dependency tree when upstream packages haven't updated yet.
  • Removing picomatch@4.0.2 from the lockfile was equally important — having two vulnerable versions resolved simultaneously doubled the attack surface.
  • ReDoS is a real denial-of-service vector in Node.js because the single-threaded event loop means one blocked regex evaluation freezes the entire process for all concurrent users.

How Orbis AppSec Detected This

  • Source: The vulnerable picomatch version (2.3.1) entered the project as a transitive dependency resolved in pnpm-lock.yaml, where it was available to any code path that invoked glob pattern matching with extglob syntax.
  • Sink: picomatch's internal makeRe() / extglob compilation function, which constructs and caches a JavaScript RegExp object from the user-supplied pattern string — the point at which a crafted extglob triggers catastrophic backtracking in the V8 regex engine.
  • Missing control: No version constraint in package.json forced a safe minimum version of picomatch, allowing the package manager to resolve the vulnerable 2.3.1 without any warning in the source-controlled dependency manifest.
  • CWE: CWE-1333 — Inefficient Regular Expression Complexity
  • Fix: Added "picomatch": "2.3.2" as an explicit direct dependency in package.json and updated pnpm-lock.yaml to resolve all picomatch references to patched versions (2.3.2 and 4.0.3), removing both 2.3.1 and 4.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-33671 is a textbook example of how a single subtle flaw in regex generation — nested quantifiers inside extglob pattern compilation — can become a denial-of-service weapon in production Node.js applications. What makes it particularly insidious is that picomatch is a deeply embedded transitive dependency: it powers glob matching in Webpack, Rollup, Vite, Jest, ESLint, and dozens of other foundational tools. A vulnerable version silently lurking in your lockfile is all an attacker needs.

The fix is straightforward — upgrade to picomatch 2.3.2, 3.0.2, or 4.0.4 depending on your version line, and pin the version explicitly so your package manager can't silently downgrade it. But the broader lesson is about defense in depth for dependencies: scan your lockfiles, not just your manifests; use overrides to force safe versions; and integrate automated vulnerability scanning into every CI run.

Regex-based denial of service is often dismissed as theoretical, but in a Node.js event loop, one catastrophic backtrack is all it takes to take down a service for every user simultaneously.


References

Frequently Asked Questions

What is a ReDoS vulnerability?

A ReDoS (Regular Expression Denial of Service) occurs when a regex engine takes exponential time to evaluate certain inputs due to catastrophic backtracking, allowing an attacker to cause a denial of service with a small crafted string.

How do you prevent ReDoS in Node.js?

Use regex libraries or glob matchers that are patched against backtracking issues, apply input length limits, use linear-time regex engines (like RE2), and keep dependencies up to date with tools like Trivy or Dependabot.

What CWE is ReDoS?

ReDoS is classified as CWE-1333: Inefficient Regular Expression Complexity.

Is input validation enough to prevent ReDoS?

Not always. Input validation helps by limiting length and character sets, but the most reliable fix is using a regex implementation that doesn't exhibit catastrophic backtracking — which in this case means upgrading picomatch to the patched version.

Can static analysis detect ReDoS?

Yes. Tools like Semgrep, ESLint with security plugins, and dedicated ReDoS analyzers (e.g., vuln-regex-detector) can flag potentially vulnerable regex patterns. Dependency scanners like Trivy detected this specific CVE in the lockfile.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2657

Related Articles

critical

How argument injection happens in Node.js Copilot tool bridge and how to fix it

A high-severity argument injection vulnerability was discovered in the Copilot tool bridge (`bridge.ts`) where user-controlled `request.args` were passed directly to `tool.execute()` without any validation or sanitization. The fix introduces Zod schema validation at line 108, ensuring that tool arguments are parsed against a declared `inputSchema` before execution. This prevents malformed or malicious payloads — including prototype pollution attempts — from reaching the underlying tool implement

high

How CORS credential reflection happens in Hono middleware and how to fix it

A high-severity CORS misconfiguration in Hono's middleware (CVE-2026-54290) allowed any origin to be reflected with credentials when the `origin` option defaulted to wildcard. This vulnerability in the studio frontend could enable attackers to steal authenticated user data through cross-origin requests. The fix upgrades Hono from 4.12.21 to 4.12.25, which properly handles CORS origin validation.

high

How Denial of Service happens in Node.js devalue and how to fix it

A high-severity Denial of Service vulnerability (CVE-2026-22774) was discovered in the `devalue` package used by the exo-dashboard SvelteKit application. Attackers could craft malicious input to trigger excessive resource consumption in the devalue deserialization library, potentially taking down the web service. The fix upgrades `devalue` from version 5.5.0 to 5.6.2 in both `package.json` and `package-lock.json`.

high

How DoS via sparse array deserialization happens in Svelte devalue and how to fix it

A high-severity vulnerability (CVE-2026-42570) was discovered in the devalue library version 5.7.1, used by the Astro-powered website. This vulnerability allowed attackers to trigger denial-of-service conditions through maliciously crafted sparse arrays during deserialization. The fix involved upgrading devalue from 5.7.1 to 5.8.1, which implements proper safeguards against sparse array exploitation.

high

Prototype Pollution in defu's Defaults Argument via `__proto__` Key (CVE-2026-35209)

CVE-2026-35209 is a high-severity prototype pollution vulnerability in the `defu` JavaScript library (versions prior to 6.1.5) that allows attackers to inject arbitrary properties onto `Object.prototype` by passing a `__proto__` key in the defaults argument. The vulnerability was present in the `blog-site` project's dependency tree and was resolved by upgrading `defu` to 6.1.5 and adding an explicit `overrides` entry to prevent transitive re-introduction of the vulnerable version.

high

How path traversal happens in Ruby YARD server and how to fix it

A high-severity path traversal vulnerability (CVE-2026-41493) in YARD versions prior to 0.9.42 allowed attackers to read arbitrary files from servers running `yard server`. This fix upgrades the yard gem from 0.9.26 to 0.9.42 in the Gemfile and Gemfile.lock, closing a dangerous information disclosure vector that could expose configuration files, credentials, and source code.