How Regular Expression Denial of Service Happens in JavaScript and How to Fix It
The Incident: A Glob Library Hiding a Denial-of-Service Trap
The client/package-lock.json in this project pinned picomatch at version 4.0.3 — a version that contains a confirmed Regular Expression Denial of Service (ReDoS) vulnerability tracked as CVE-2026-33671. Trivy's dependency scanner flagged the package during a routine security scan, and an automated fix was generated to upgrade the library and prevent any user-influenced glob pattern from being able to freeze the Node.js event loop.
This post walks through exactly what went wrong, how the attack works at the regex level, and what the fix does to close the door.
The Vulnerability Explained
What Is picomatch?
picomatch is one of the most widely used glob-matching libraries in the JavaScript ecosystem. It converts glob patterns like **/*.js or +(foo|bar) into compiled JavaScript regular expressions and then tests strings against them. It is a transitive dependency of tools like Vite, Rollup, chokidar, and many others — meaning it quietly lives inside almost every modern frontend build toolchain.
Extglob Patterns and the ReDoS Root Cause
The vulnerability lives in picomatch's extglob handling — the +(...), *(...), ?(...), @(...), and !(...) pattern syntax inherited from ksh/bash. When picomatch compiles an extglob pattern into a JavaScript regex, certain nested or repeated extglob structures produce a compiled regex with exponential backtracking complexity.
Consider a pattern like:
+(a+)+b
When compiled naively, the resulting regex contains nested quantifiers over overlapping character classes. If the input string is a long sequence of a characters that does not end in b, the regex engine must explore an exponentially growing number of possible ways to partition the as across the outer and inner + quantifiers before concluding there is no match. This is the classic catastrophic backtracking scenario.
In picomatch 4.0.3, the vulnerable code path is inside the extglob compiler — the function that translates +(...) and friends into raw regex syntax. The generated pattern strings were not checked or rewritten to eliminate ambiguous quantifier nesting.
The Vulnerable Dependency Entry
Before the fix, client/package-lock.json resolved picomatch to:
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"engines": {
"node": ">=12"
}
}
Any code that eventually calls picomatch(pattern) with a user-influenced pattern string — or passes user data through a library that internally uses picomatch for file filtering — is potentially reachable by this attack.
Real-World Attack Scenario
Imagine a build-tool API endpoint or a file-watching feature that accepts a glob pattern from the user (e.g., a "watch files matching this pattern" configuration field). An attacker submits:
+(a+(b+(c+(d+(e+(f+)))))+)
picomatch 4.0.3 compiles this into a deeply nested regex. When the application then tests even a moderately long string against the compiled pattern, the JavaScript regex engine enters catastrophic backtracking. Because Node.js runs JavaScript on a single-threaded event loop, the entire server — including all concurrent requests — is blocked for the duration of the backtrack storm. A single HTTP request containing a crafted pattern is enough to cause application-wide denial of service.
The Fix
Two-File Change, One Clear Goal
The fix touches exactly two files: client/package-lock.json and client/package.json. Here is what each change does.
1. client/package-lock.json — Upgrade the Resolved Version
Before:
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="
}
After:
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="
}
The new integrity hash (sha512-QP88...) cryptographically binds the installed package to the exact patched release, preventing supply-chain substitution.
2. client/package.json — Pin via overrides
Before:
"overrides": {
"nanoid": "3.3.18"
}
After:
"overrides": {
"nanoid": "3.3.18",
"picomatch": "4.0.4"
}
This is the critical second step. Without the overrides entry, any transitive dependency that declares a loose picomatch range (e.g., "picomatch": "^2.0.0") could still resolve to a vulnerable version deeper in the dependency tree. The overrides field in npm forces every node in the dependency graph that requires picomatch to receive version 4.0.4, regardless of what semver range they declare.
Why the Fix Works
Picomatch 4.0.4 rewrites the extglob-to-regex compiler to produce patterns that avoid ambiguous quantifier nesting. The fix applies atomic groups or possessive quantifiers where available, and restructures the generated regex alternations so that the engine can fail fast without exploring exponential match paths. Valid glob patterns continue to match exactly as before — the change only affects how the regex is internally structured, not what it accepts.
Prevention & Best Practices
1. Use overrides / resolutions for Security-Critical Transitive Dependencies
When a vulnerability is in a deeply transitive dependency, simply upgrading your direct dependencies may not be enough. npm's overrides field (and Yarn's resolutions) let you force a specific version across the entire tree:
"overrides": {
"picomatch": "4.0.4"
}
Always verify the override took effect by running npm ls picomatch and confirming no stale copies remain.
2. Never Pass Unsanitized User Input to Glob Matchers
Treat glob patterns from user input the same way you treat SQL queries: validate them before use. Consider:
- Allowlisting a set of safe pattern characters (e.g.,
[a-zA-Z0-9_\-\/\*\?\.]) - Rejecting patterns that contain extglob syntax (
+(,*(,!(, etc.) unless your application explicitly requires it - Enforcing a maximum pattern length
3. Integrate Dependency Scanning into CI
Tools like Trivy, npm audit, Snyk, and OWASP Dependency-Check can catch known-vulnerable versions before they reach production. Add a step to your CI pipeline:
# Example: fail the build on high-severity findings
trivy fs --exit-code 1 --severity HIGH,CRITICAL .
4. Monitor for ReDoS-Prone Regex Patterns
For custom regex in your own code, use tools like:
- vuln-regex-detector — static analysis for catastrophic backtracking
- safe-regex — npm package that flags dangerous patterns
- Semgrep — rules for detecting ReDoS-prone patterns in source code
5. Relevant Standards
- CWE-1333: Inefficient Regular Expression Complexity
- CWE-400: Uncontrolled Resource Consumption
- OWASP: Denial of Service Cheat Sheet
Key Takeaways
- Extglob patterns in picomatch 4.0.3 compile to regexes with exponential backtracking — a single crafted pattern string is enough to block the entire Node.js event loop.
- Upgrading
package-lock.jsonalone is not sufficient — without the"picomatch": "4.0.4"entry inoverrides, transitive dependents can still resolve to a vulnerable copy. - The
integrityhash change fromsha512-5gTmg...tosha512-QP88...is your cryptographic proof that the patched binary is installed, not just a version number bump. - ReDoS is a single-threaded event loop killer — unlike memory exhaustion, a ReDoS attack requires no persistence, no authentication, and produces no noisy error logs until the server is already unresponsive.
- Glob patterns from user input are an under-appreciated attack surface — any feature that lets users specify file patterns, watch paths, or filter expressions should validate or sandbox those patterns before passing them to libraries like picomatch.
How Orbis AppSec Detected This
- Source: User-influenced glob pattern strings passed into picomatch's pattern compiler (e.g., via build-tool configuration endpoints or file-watcher APIs that accept patterns from external input).
- Sink: picomatch's internal extglob-to-regex compilation function, invoked whenever
picomatch(pattern)is called with an extglob-containing string — resolved tonode_modules/picomatchversion 4.0.3 inclient/package-lock.json. - Missing control: No version constraint or
overridespin prevented the vulnerable 4.0.3 release from being installed; no input validation blocked extglob patterns from reaching the compiler. - CWE: CWE-1333 — Inefficient Regular Expression Complexity.
- Fix: picomatch was upgraded to 4.0.4 in
client/package-lock.jsonand pinned via"picomatch": "4.0.4"in theoverridessection ofclient/package.jsonto ensure all transitive copies resolve to 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-2026-33671 is a reminder that denial-of-service vulnerabilities don't require memory corruption or authentication bypass — a carefully constructed string fed to an unsuspecting regex engine is enough. picomatch's extglob compiler in versions before 4.0.4 / 3.0.2 / 2.3.2 produced internally unsafe regex patterns that could be triggered by any code path accepting user-influenced glob strings.
The fix is clean and minimal: two files changed, one version number bumped, one overrides entry added. But the lesson is broader — transitive dependencies deserve the same security scrutiny as your own code, and dependency overrides are a powerful tool for enforcing patched versions across an entire dependency graph.
Keep your lock files up to date, integrate scanner tooling into CI, and treat user-supplied pattern strings with the same suspicion you'd give to SQL input.