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

high

How Quadratic CPU Consumption in !!omap Resolution Happens in js-yaml and How to Fix It

A high-severity denial-of-service vulnerability in js-yaml (GHSA-5p4m-2wfm-xmqj) caused quadratic CPU consumption when resolving `!!omap` (ordered map) YAML tags, affecting both the 3.x and 4.x release lines. Upgrading to js-yaml 4.3.1 or 3.15.1 closes the gap by fixing the algorithmic inefficiency in `!!omap` duplicate-key detection. Any application that parses untrusted YAML input is at risk of resource exhaustion leading to service unavailability.

high

How Octal IP Address Parsing Inconsistency Enables SSRF in Node.js and How to Fix It

A critical parsing inconsistency in the `ip-address` npm package (version 10.2.0) allowed attackers to bypass SSRF protections by exploiting how leading-zero octets are interpreted differently—decimal by the library versus octal by system resolvers. This vulnerability (CVE-2026-69192) was fixed by upgrading to version 10.3.1 using an npm override, ensuring consistent IP address validation across the application.

high

How Command Injection Happens in Node.js Child Process Calls and How to Fix It

A Node.js library was vulnerable to command injection through unsafe use of `execSync()` with shell string interpolation in the `index.js` file. By switching to `execFileSync()` with argument arrays, the fix eliminates the ability for attackers to inject shell metacharacters through file paths. This change demonstrates a critical security hardening pattern for any Node.js code that spawns child processes.

high

How NO_PROXY bypass via crafted URL happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42043) in the axios HTTP client library allowed attackers to bypass NO_PROXY environment variable restrictions using specially crafted URLs. This could route sensitive internal traffic through attacker-controlled proxy servers. The fix upgrades axios from 1.13.6 to 1.18.0, which includes a rewritten proxy resolution mechanism using `proxy-from-env` v2.1.0 and the `https-proxy-agent` package.

high

How Denial of Service via Infinite Loop happens in Node.js dependencies and how to fix it

A high-severity vulnerability in the nanoid package (CVE-2026-67213) allowed attackers to trigger infinite loops through the customAlphabet function, potentially causing complete denial of service. This fix upgrades nanoid from version 3.3.16 to 3.3.17 in the app_store dependency tree, eliminating the DoS risk through a simple version override.

high

How Server-Side Request Forgery (SSRF) happens in Node.js through inconsistent IP address parsing and how to fix it

A high-severity Server-Side Request Forgery (SSRF) vulnerability (CVE-2026-69192) was discovered in the ip-address package version 10.2.0, where inconsistent IP address parsing allowed attackers to bypass trust boundaries and access internal resources. The fix upgrades ip-address from 10.2.0 to 10.3.1 across the dependency tree, with explicit pinning in package.json and strategic version management in bun.lock to prevent both direct and transitive exploitation paths.