glob CLI: Command injection via -c/--cmd executes matches with shell:true
Description
Glob matches files using patterns the shell uses. Starting in version 10.2.0 and prior to versions 10.5.0 and 11.1.0, 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 are 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. This issue has been patched in versions 10.5.0 and 11.1.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A command injection vulnerability in the glob CLI's -c/--cmd option allows arbitrary code execution when processing files with malicious names.
Vulnerability
Overview The glob CLI (versions 10.2.0 through 10.4.x and 11.0.x) contains a command injection vulnerability in the -c/--cmd option. When glob matches files against a pattern, the matched filenames are passed to a shell command with shell: true, meaning shell metacharacters embedded in filenames are interpreted by the shell rather than passed as literal arguments [1][2]. This flaw exists because the CLI directly concatenates file paths into a shell command string instead of using safe positional argument passing.
Exploitation
An attacker can exploit this by creating a file with a name containing shell metacharacters (e.g., $(malicious_command)). When a victim or CI pipeline runs glob -c and the malicious file is matched, the shell expands the metacharacters and executes the attacker-controlled command. No additional authentication is required beyond the ability to write a file that will be matched by the glob pattern used in the vulnerable command [2]. The attack surface includes developer machines and CI/CD environments where untrusted file uploads or repository contents are processed.
Impact
Successful exploitation allows an attacker to execute arbitrary commands with the privileges of the user or CI account running the glob command. This can lead to code execution, data exfiltration, or lateral movement in environments that rely on glob to process file listings from untrusted sources. The vulnerability is particularly dangerous in CI pipelines where a malicious file in a repository checkout could trigger automated builds or tests [2].
Mitigation
Patches are available in glob versions 10.5.0 and 11.1.0. The fix, visible in commits [3] and [4], introduces a safer --cmd-arg (-g) option for passing positional arguments and deprecates the unsafe -c/--cmd behavior. Users should upgrade to the patched versions immediately. As a workaround, avoid passing untrusted filenames directly to the -c/--cmd option and use the --cmd-arg option to separate command arguments from file paths.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
globnpm | >= 11.0.0, < 11.1.0 | 11.1.0 |
globnpm | >= 10.2.0, < 10.5.0 | 10.5.0 |
Affected products
2- isaacs/node-globv5Range: >= 10.2.0, < 10.5.0
Patches
247473c046b91bin: Do not expose filenames to shell expansion
4 files changed · +345 −35
changelog.md+12 −0 modified@@ -1,5 +1,17 @@ # changeglob +## 11.1 + +[GHSA-5j98-mcp5-4vw2](https://github.com/isaacs/node-glob/security/advisories/GHSA-5j98-mcp5-4vw2) + +- Add the `--shell` option for the command line, with a warning + that this is unsafe. (It will be removed in v12.) +- Add the `--cmd-arg`/`-g` as a way to *safely* add positional + arguments to the command provided to the CLI tool. +- Detect commands with space or quote characters on known shells, + and pass positional arguments to them safely, avoiding + `shell:true` execution. + ## 11.0 - Drop support for node before v20
src/bin.mts+137 −34 modified@@ -3,7 +3,7 @@ import { foregroundChild } from 'foreground-child' import { existsSync } from 'fs' import { jack } from 'jackspeak' import { loadPackageJson } from 'package-json-from-dist' -import { join } from 'path' +import { basename, join } from 'path' import { globStream } from './index.js' const { version } = loadPackageJson(import.meta.url, '../package.json') @@ -35,6 +35,50 @@ const j = jack({ this pattern`, }, }) + .flag({ + shell: { + default: false, + description: `Interpret the command as a shell command by passing it + to the shell, with all matched filesystem paths appended, + **even if this cannot be done safely**. + + This is **not** unsafe (and usually unnecessary) when using + the known Unix shells sh, bash, zsh, and fish, as these can + all be executed in such a way as to pass positional + arguments safely. + + **Note**: THIS IS UNSAFE IF THE FILE PATHS ARE UNTRUSTED, + because a path like \`'some/path/\\$\\(cmd)'\` will be + executed by the shell. + + If you do have positional arguments that you wish to pass to + the command ahead of the glob pattern matches, use the + \`--cmd-arg\`/\`-g\` option instead. + + The next major release of glob will fully remove the ability + to use this option unsafely.`, + }, + }) + .optList({ + 'cmd-arg': { + short: 'g', + hint: 'arg', + default: [], + description: `Pass the provided values to the supplied command, ahead of + the glob matches. + + For example, the command: + + glob -c echo -g"hello" -g"world" *.txt + + might output: + + hello world a.txt b.txt + + This is a safer (and future-proof) alternative than putting + positional arguments in the \`-c\`/\`--cmd\` option.`, + }, + }) .flag({ all: { short: 'A', @@ -227,54 +271,113 @@ const j = jack({ try { const { positionals, values } = j.parse() - if (values.version) { + const { + cmd, + shell, + all, + default: def, + version: showVersion, + help, + absolute, + cwd, + dot, + + 'dot-relative': dotRelative, + follow, + ignore, + 'match-base': matchBase, + 'max-depth': maxDepth, + mark, + nobrace, + nocase, + nodir, + noext, + noglobstar, + platform, + realpath, + root, + stat, + debug, + posix, + 'cmd-arg': cmdArg, + } = values + if (showVersion) { console.log(version) process.exit(0) } - if (values.help) { + if (help) { console.log(j.usage()) process.exit(0) } - if (positionals.length === 0 && !values.default) - throw 'No patterns provided' - if (positionals.length === 0 && values.default) - positionals.push(values.default) + //const { shell, help } = values + if (positionals.length === 0 && !def) throw 'No patterns provided' + if (positionals.length === 0 && def) positionals.push(def) const patterns = - values.all ? positionals : positionals.filter(p => !existsSync(p)) + all ? positionals : positionals.filter(p => !existsSync(p)) const matches = - values.all ? - [] - : positionals.filter(p => existsSync(p)).map(p => join(p)) + all ? [] : positionals.filter(p => existsSync(p)).map(p => join(p)) + const stream = globStream(patterns, { - absolute: values.absolute, - cwd: values.cwd, - dot: values.dot, - dotRelative: values['dot-relative'], - follow: values.follow, - ignore: values.ignore, - mark: values.mark, - matchBase: values['match-base'], - maxDepth: values['max-depth'], - nobrace: values.nobrace, - nocase: values.nocase, - nodir: values.nodir, - noext: values.noext, - noglobstar: values.noglobstar, - platform: values.platform as undefined | NodeJS.Platform, - realpath: values.realpath, - root: values.root, - stat: values.stat, - debug: values.debug, - posix: values.posix, + absolute, + cwd, + dot, + dotRelative, + follow, + ignore, + mark, + matchBase, + maxDepth, + nobrace, + nocase, + nodir, + noext, + noglobstar, + platform: platform as undefined | NodeJS.Platform, + realpath, + root, + stat, + debug, + posix, }) - const cmd = values.cmd if (!cmd) { matches.forEach(m => console.log(m)) stream.on('data', f => console.log(f)) } else { - stream.on('data', f => matches.push(f)) - stream.on('end', () => foregroundChild(cmd, matches, { shell: true })) + cmdArg.push(...matches) + stream.on('data', f => cmdArg.push(f)) + // Attempt to support commands that contain spaces and otherwise require + // shell interpretation, but do NOT shell-interpret the arguments, to avoid + // injections via filenames. This affordance can only be done on known Unix + // shells, unfortunately. + // + // 'bash', ['-c', cmd + ' "$@"', 'bash', ...matches] + // 'zsh', ['-c', cmd + ' "$@"', 'zsh', ...matches] + // 'fish', ['-c', cmd + ' "$argv"', ...matches] + const { SHELL = 'unknown' } = process.env + const shellBase = basename(SHELL) + const knownShells = ['sh', 'ksh', 'zsh', 'bash', 'fish'] + if ( + (shell || /[ "']/.test(cmd)) && + knownShells.includes(shellBase) + ) { + const cmdWithArgs = `${cmd} "\$${shellBase === 'fish' ? 'argv' : '@'}"` + if (shellBase !== 'fish') { + cmdArg.unshift(SHELL) + } + cmdArg.unshift('-c', cmdWithArgs) + stream.on('end', () => foregroundChild(SHELL, cmdArg)) + } else { + if (shell) { + process.emitWarning( + 'The --shell option is unsafe, and will be removed. To pass ' + + 'positional arguments to the subprocess, use -g/--cmd-arg instead.', + 'DeprecationWarning', + 'GLOB_SHELL', + ) + } + stream.on('end', () => foregroundChild(cmd, cmdArg, { shell })) + } } } catch (e) { console.error(j.usage())
tap-snapshots/test/bin.ts.test.cjs+39 −0 modified@@ -31,6 +31,45 @@ Object { If no positional arguments are provided, glob will use this pattern + --shell Interpret the command as a shell command by passing it + to the shell, with all matched filesystem paths + appended, + **even if this cannot be done safely**. + + This is **not** unsafe (and usually unnecessary) when + using the known Unix shells sh, bash, zsh, and fish, as + these can all be executed in such a way as to pass + positional arguments safely. + + **Note**: THIS IS UNSAFE IF THE FILE PATHS ARE + UNTRUSTED, because a path like \`'some/path/\\\\$\\\\(cmd)'\` + will be executed by the shell. + + If you do have positional arguments that you wish to + pass to the command ahead of the glob pattern matches, + use the \`--cmd-arg\`/\`-g\` option instead. + + The next major release of glob will fully remove the + ability to use this option unsafely. + + -g<arg> --cmd-arg=<arg> + Pass the provided values to the supplied command, ahead + of the glob matches. + + For example, the command: + + glob -c echo -g"hello" -g"world" *.txt + + might output: + + hello world a.txt b.txt + + This is a safer (and future-proof) alternative than + putting positional arguments in the \`-c\`/\`--cmd\` + option. + + Can be set multiple times + -A --all By default, the glob cli command will not expand any arguments that are an exact match to a file on disk.
test/bin.ts+157 −1 modified@@ -1,4 +1,4 @@ -import { spawn, SpawnOptions } from 'child_process' +import { spawn, type SpawnOptions } from 'child_process' import { readFileSync } from 'fs' import { sep } from 'path' import t from 'tap' @@ -11,6 +11,30 @@ const { version } = JSON.parse( ) const bin = fileURLToPath(new URL('../dist/esm/bin.mjs', import.meta.url)) +const foregroundChildCalls: [ + string, + string[], + undefined | SpawnOptions, +][] = [] +let mockForegroundChildAwaiting: undefined | Promise<void> = undefined +let resolveMockForegroundChildAwaiting: undefined | (() => void) = + undefined +const expectForegroundChild = () => + new Promise<void>(res => (resolveMockForegroundChildAwaiting = res)) +const mockForegroundChild = { + foregroundChild: async ( + cmd: string, + args: string[], + options?: SpawnOptions, + ) => { + resolveMockForegroundChildAwaiting?.() + resolveMockForegroundChildAwaiting = undefined + mockForegroundChildAwaiting = undefined + foregroundChildCalls.push([cmd, args, options]) + }, +} +t.beforeEach(() => (foregroundChildCalls.length = 0)) + t.cleanSnapshot = s => s.split(version).join('{VERSION}') interface Result { @@ -61,6 +85,8 @@ t.test('version', async t => { t.matchSnapshot(await run(['--version']), '--version shows version') }) +// Note: this test works without --shell because we only run it on bash. +// exercises the "safely add cmd args to shell cmd" path. t.test('finds matches for a pattern', async t => { const cwd = t.testdir({ a: { @@ -88,6 +114,136 @@ t.test('finds matches for a pattern', async t => { ) }) +t.test('append positional args safely to shell in fish', async t => { + const cwd = t.testdir({ + a: { + 'x.y': '', + 'x.a': '', + b: { + 'z.y': '', + 'z.a': '', + }, + }, + }) + const { SHELL } = process.env + t.teardown(() => (process.env.SHELL = SHELL)) + process.env.SHELL = '/usr/local/bin/fish' + const p = expectForegroundChild() + t.chdir(cwd) + const c = `node -p "process.argv.map(s=>s.toUpperCase())"` + t.intercept(process, 'argv', { + value: [process.argv[0], 'glob', '**/*.y', '-c', c], + }) + + await t.mockImport('../dist/esm/bin.mjs', { + 'foreground-child': mockForegroundChild, + }) + await p + t.strictSame(foregroundChildCalls, [ + [ + '/usr/local/bin/fish', + [ + '-c', + 'node -p "process.argv.map(s=>s.toUpperCase())" "$argv"', + 'a/x.y', + 'a/b/z.y', + ], + undefined, + ], + ]) +}) + +t.test('UNSAFE positional args with --shell', async t => { + const cwd = t.testdir({ + a: { + 'x.y': '', + 'x.a': '', + b: { + 'z.y': '', + 'z.a': '', + }, + }, + }) + const { SHELL } = process.env + t.teardown(() => (process.env.SHELL = SHELL)) + process.env.SHELL = '/some/unknown/thing' + + const p = expectForegroundChild() + t.chdir(cwd) + const c = `node -p "process.argv.map(s=>s.toUpperCase())"` + t.intercept(process, 'argv', { + value: [process.argv[0], 'glob', '--shell', '**/*.y', '-c', c], + }) + const warnings: [string, string, string][] = [] + t.intercept(process, 'emitWarning', { + value: (a: string, b: string, c: string) => warnings.push([a, b, c]), + }) + + await t.mockImport('../dist/esm/bin.mjs', { + 'foreground-child': mockForegroundChild, + }) + await p + t.strictSame(foregroundChildCalls, [ + [c, ['a/x.y', 'a/b/z.y'], { shell: true }], + ]) + t.strictSame(warnings, [ + [ + 'The --shell option is unsafe, and will be removed. To pass positional arguments to the subprocess, use -g/--cmd-arg instead.', + 'DeprecationWarning', + 'GLOB_SHELL', + ], + ]) +}) + +t.test('safe positional args with --cmd-arg/-g', async t => { + const cwd = t.testdir({ + a: { + 'x.y': '', + 'x.a': '', + b: { + 'z.y': '', + 'z.a': '', + }, + }, + }) + const { SHELL } = process.env + t.teardown(() => (process.env.SHELL = SHELL)) + process.env.SHELL = '/some/unknown/thing' + + const p = expectForegroundChild() + t.chdir(cwd) + const c = 'node' + t.intercept(process, 'argv', { + value: [ + process.argv[0], + 'glob', + '**/*.y', + '-c', + c, + '-g-p', + '--cmd-arg', + 'process.argv.map(s=>s.toUpperCase())', + ], + }) + const warnings: [string, string, string][] = [] + t.intercept(process, 'emitWarning', { + value: (a: string, b: string, c: string) => warnings.push([a, b, c]), + }) + + await t.mockImport('../dist/esm/bin.mjs', { + 'foreground-child': mockForegroundChild, + }) + await p + t.strictSame(foregroundChildCalls, [ + [ + c, + ['-p', 'process.argv.map(s=>s.toUpperCase())', 'a/x.y', 'a/b/z.y'], + { shell: false }, + ], + ]) + t.strictSame(warnings, []) +}) + t.test('prioritizes exact match if exists, unless --all', async t => { const cwd = t.testdir({ routes: {
1e4e297342a0bin: Do not expose filenames to shell expansion
5 files changed · +388 −40
changelog.md+14 −0 modified@@ -1,5 +1,19 @@ # changeglob +## 10.5 + +Backport fix for +[GHSA-5j98-mcp5-4vw2](https://github.com/isaacs/node-glob/security/advisories/GHSA-5j98-mcp5-4vw2) +to v10 branch. + +- Add the `--shell` option for the command line, with a warning + that this is unsafe. (It will be removed in v12.) +- Add the `--cmd-arg`/`-g` as a way to _safely_ add positional + arguments to the command provided to the CLI tool. +- Detect commands with space or quote characters on known shells, + and pass positional arguments to them safely, avoiding + `shell:true` execution. + ## 10.4 - Add `includeChildMatches: false` option
package.json+0 −2 modified@@ -21,12 +21,10 @@ "./package.json": "./package.json", ".": { "import": { - "source": "./src/index.ts", "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { - "source": "./src/index.ts", "types": "./dist/commonjs/index.d.ts", "default": "./dist/commonjs/index.js" }
src/bin.mts+145 −36 modified@@ -3,7 +3,7 @@ import { foregroundChild } from 'foreground-child' import { existsSync } from 'fs' import { jack } from 'jackspeak' import { loadPackageJson } from 'package-json-from-dist' -import { join } from 'path' +import { basename, join } from 'path' import { globStream } from './index.js' const { version } = loadPackageJson(import.meta.url, '../package.json') @@ -35,6 +35,50 @@ const j = jack({ this pattern`, }, }) + .flag({ + shell: { + default: false, + description: `Interpret the command as a shell command by passing it + to the shell, with all matched filesystem paths appended, + **even if this cannot be done safely**. + + This is **not** unsafe (and usually unnecessary) when using + the known Unix shells sh, bash, zsh, and fish, as these can + all be executed in such a way as to pass positional + arguments safely. + + **Note**: THIS IS UNSAFE IF THE FILE PATHS ARE UNTRUSTED, + because a path like \`'some/path/\\$\\(cmd)'\` will be + executed by the shell. + + If you do have positional arguments that you wish to pass to + the command ahead of the glob pattern matches, use the + \`--cmd-arg\`/\`-g\` option instead. + + The next major release of glob will fully remove the ability + to use this option unsafely.`, + }, + }) + .optList({ + 'cmd-arg': { + short: 'g', + hint: 'arg', + default: [], + description: `Pass the provided values to the supplied command, ahead of + the glob matches. + + For example, the command: + + glob -c echo -g"hello" -g"world" *.txt + + might output: + + hello world a.txt b.txt + + This is a safer (and future-proof) alternative than putting + positional arguments in the \`-c\`/\`--cmd\` option.`, + }, + }) .flag({ all: { short: 'A', @@ -78,7 +122,7 @@ const j = jack({ description: `Always resolve to posix style paths, using '/' as the directory separator, even on Windows. Drive letter absolute matches on Windows will be expanded to their - full resolved UNC maths, eg instead of 'C:\\foo\\bar', + full resolved UNC paths, eg instead of 'C:\\foo\\bar', it will expand to '//?/C:/foo/bar'. `, }, @@ -215,8 +259,10 @@ const j = jack({ description: `Output a huge amount of noisy debug information about patterns as they are parsed and used to match files.`, }, - }) - .flag({ + version: { + short: 'V', + description: `Output the version (${version})`, + }, help: { short: 'h', description: 'Show this usage information', @@ -225,50 +271,113 @@ const j = jack({ try { const { positionals, values } = j.parse() - if (values.help) { + const { + cmd, + shell, + all, + default: def, + version: showVersion, + help, + absolute, + cwd, + dot, + + 'dot-relative': dotRelative, + follow, + ignore, + 'match-base': matchBase, + 'max-depth': maxDepth, + mark, + nobrace, + nocase, + nodir, + noext, + noglobstar, + platform, + realpath, + root, + stat, + debug, + posix, + 'cmd-arg': cmdArg, + } = values + if (showVersion) { + console.log(version) + process.exit(0) + } + if (help) { console.log(j.usage()) process.exit(0) } - if (positionals.length === 0 && !values.default) - throw 'No patterns provided' - if (positionals.length === 0 && values.default) - positionals.push(values.default) + //const { shell, help } = values + if (positionals.length === 0 && !def) throw 'No patterns provided' + if (positionals.length === 0 && def) positionals.push(def) const patterns = - values.all ? positionals : positionals.filter(p => !existsSync(p)) + all ? positionals : positionals.filter(p => !existsSync(p)) const matches = - values.all ? - [] - : positionals.filter(p => existsSync(p)).map(p => join(p)) + all ? [] : positionals.filter(p => existsSync(p)).map(p => join(p)) + const stream = globStream(patterns, { - absolute: values.absolute, - cwd: values.cwd, - dot: values.dot, - dotRelative: values['dot-relative'], - follow: values.follow, - ignore: values.ignore, - mark: values.mark, - matchBase: values['match-base'], - maxDepth: values['max-depth'], - nobrace: values.nobrace, - nocase: values.nocase, - nodir: values.nodir, - noext: values.noext, - noglobstar: values.noglobstar, - platform: values.platform as undefined | NodeJS.Platform, - realpath: values.realpath, - root: values.root, - stat: values.stat, - debug: values.debug, - posix: values.posix, + absolute, + cwd, + dot, + dotRelative, + follow, + ignore, + mark, + matchBase, + maxDepth, + nobrace, + nocase, + nodir, + noext, + noglobstar, + platform: platform as undefined | NodeJS.Platform, + realpath, + root, + stat, + debug, + posix, }) - const cmd = values.cmd if (!cmd) { matches.forEach(m => console.log(m)) stream.on('data', f => console.log(f)) } else { - stream.on('data', f => matches.push(f)) - stream.on('end', () => foregroundChild(cmd, matches, { shell: true })) + cmdArg.push(...matches) + stream.on('data', f => cmdArg.push(f)) + // Attempt to support commands that contain spaces and otherwise require + // shell interpretation, but do NOT shell-interpret the arguments, to avoid + // injections via filenames. This affordance can only be done on known Unix + // shells, unfortunately. + // + // 'bash', ['-c', cmd + ' "$@"', 'bash', ...matches] + // 'zsh', ['-c', cmd + ' "$@"', 'zsh', ...matches] + // 'fish', ['-c', cmd + ' "$argv"', ...matches] + const { SHELL = 'unknown' } = process.env + const shellBase = basename(SHELL) + const knownShells = ['sh', 'ksh', 'zsh', 'bash', 'fish'] + if ( + (shell || /[ "']/.test(cmd)) && + knownShells.includes(shellBase) + ) { + const cmdWithArgs = `${cmd} "\$${shellBase === 'fish' ? 'argv' : '@'}"` + if (shellBase !== 'fish') { + cmdArg.unshift(SHELL) + } + cmdArg.unshift('-c', cmdWithArgs) + stream.on('end', () => foregroundChild(SHELL, cmdArg)) + } else { + if (shell) { + process.emitWarning( + 'The --shell option is unsafe, and will be removed. To pass ' + + 'positional arguments to the subprocess, use -g/--cmd-arg instead.', + 'DeprecationWarning', + 'GLOB_SHELL', + ) + } + stream.on('end', () => foregroundChild(cmd, cmdArg, { shell })) + } } } catch (e) { console.error(j.usage())
tap-snapshots/test/bin.ts.test.cjs+67 −1 modified@@ -31,6 +31,45 @@ Object { If no positional arguments are provided, glob will use this pattern + --shell Interpret the command as a shell command by passing it + to the shell, with all matched filesystem paths + appended, + **even if this cannot be done safely**. + + This is **not** unsafe (and usually unnecessary) when + using the known Unix shells sh, bash, zsh, and fish, as + these can all be executed in such a way as to pass + positional arguments safely. + + **Note**: THIS IS UNSAFE IF THE FILE PATHS ARE + UNTRUSTED, because a path like \`'some/path/\\\\$\\\\(cmd)'\` + will be executed by the shell. + + If you do have positional arguments that you wish to + pass to the command ahead of the glob pattern matches, + use the \`--cmd-arg\`/\`-g\` option instead. + + The next major release of glob will fully remove the + ability to use this option unsafely. + + -g<arg> --cmd-arg=<arg> + Pass the provided values to the supplied command, ahead + of the glob matches. + + For example, the command: + + glob -c echo -g"hello" -g"world" *.txt + + might output: + + hello world a.txt b.txt + + This is a safer (and future-proof) alternative than + putting positional arguments in the \`-c\`/\`--cmd\` + option. + + Can be set multiple times + -A --all By default, the glob cli command will not expand any arguments that are an exact match to a file on disk. @@ -60,7 +99,7 @@ Object { -x --posix Always resolve to posix style paths, using '/' as the directory separator, even on Windows. Drive letter absolute matches on Windows will be expanded to their - full resolved UNC maths, eg instead of 'C:\\\\foo\\\\bar', it + full resolved UNC paths, eg instead of 'C:\\\\foo\\\\bar', it will expand to '//?/C:/foo/bar'. -f --follow Follow symlinked directories when expanding '**' @@ -143,8 +182,35 @@ Object { -v --debug Output a huge amount of noisy debug information about patterns as they are parsed and used to match files. + -V --version Output the version ({VERSION}) -h --help Show this usage information ), } ` + +exports[`test/bin.ts > TAP > version > --version shows version 1`] = ` +Object { + "args": Array [ + "--version", + ], + "code": 0, + "options": Object {}, + "signal": null, + "stderr": "", + "stdout": "{VERSION}\\n", +} +` + +exports[`test/bin.ts > TAP > version > -V shows version 1`] = ` +Object { + "args": Array [ + "-V", + ], + "code": 0, + "options": Object {}, + "signal": null, + "stderr": "", + "stdout": "{VERSION}\\n", +} +`
test/bin.ts+162 −1 modified@@ -1,4 +1,4 @@ -import { spawn, SpawnOptions } from 'child_process' +import { spawn, type SpawnOptions } from 'child_process' import { readFileSync } from 'fs' import { sep } from 'path' import t from 'tap' @@ -11,6 +11,30 @@ const { version } = JSON.parse( ) const bin = fileURLToPath(new URL('../dist/esm/bin.mjs', import.meta.url)) +const foregroundChildCalls: [ + string, + string[], + undefined | SpawnOptions, +][] = [] +let mockForegroundChildAwaiting: undefined | Promise<void> = undefined +let resolveMockForegroundChildAwaiting: undefined | (() => void) = + undefined +const expectForegroundChild = () => + new Promise<void>(res => (resolveMockForegroundChildAwaiting = res)) +const mockForegroundChild = { + foregroundChild: async ( + cmd: string, + args: string[], + options?: SpawnOptions, + ) => { + resolveMockForegroundChildAwaiting?.() + resolveMockForegroundChildAwaiting = undefined + mockForegroundChildAwaiting = undefined + foregroundChildCalls.push([cmd, args, options]) + }, +} +t.beforeEach(() => (foregroundChildCalls.length = 0)) + t.cleanSnapshot = s => s.split(version).join('{VERSION}') interface Result { @@ -56,6 +80,13 @@ t.test('usage', async t => { t.match(badp.stderr, 'Invalid value provided for --platform: "glorb"\n') }) +t.test('version', async t => { + t.matchSnapshot(await run(['-V']), '-V shows version') + t.matchSnapshot(await run(['--version']), '--version shows version') +}) + +// Note: this test works without --shell because we only run it on bash. +// exercises the "safely add cmd args to shell cmd" path. t.test('finds matches for a pattern', async t => { const cwd = t.testdir({ a: { @@ -83,6 +114,136 @@ t.test('finds matches for a pattern', async t => { ) }) +t.test('append positional args safely to shell in fish', async t => { + const cwd = t.testdir({ + a: { + 'x.y': '', + 'x.a': '', + b: { + 'z.y': '', + 'z.a': '', + }, + }, + }) + const { SHELL } = process.env + t.teardown(() => (process.env.SHELL = SHELL)) + process.env.SHELL = '/usr/local/bin/fish' + const p = expectForegroundChild() + t.chdir(cwd) + const c = `node -p "process.argv.map(s=>s.toUpperCase())"` + t.intercept(process, 'argv', { + value: [process.argv[0], 'glob', '**/*.y', '-c', c], + }) + + await t.mockImport('../dist/esm/bin.mjs', { + 'foreground-child': mockForegroundChild, + }) + await p + t.strictSame(foregroundChildCalls, [ + [ + '/usr/local/bin/fish', + [ + '-c', + 'node -p "process.argv.map(s=>s.toUpperCase())" "$argv"', + 'a/x.y', + 'a/b/z.y', + ], + undefined, + ], + ]) +}) + +t.test('UNSAFE positional args with --shell', async t => { + const cwd = t.testdir({ + a: { + 'x.y': '', + 'x.a': '', + b: { + 'z.y': '', + 'z.a': '', + }, + }, + }) + const { SHELL } = process.env + t.teardown(() => (process.env.SHELL = SHELL)) + process.env.SHELL = '/some/unknown/thing' + + const p = expectForegroundChild() + t.chdir(cwd) + const c = `node -p "process.argv.map(s=>s.toUpperCase())"` + t.intercept(process, 'argv', { + value: [process.argv[0], 'glob', '--shell', '**/*.y', '-c', c], + }) + const warnings: [string, string, string][] = [] + t.intercept(process, 'emitWarning', { + value: (a: string, b: string, c: string) => warnings.push([a, b, c]), + }) + + await t.mockImport('../dist/esm/bin.mjs', { + 'foreground-child': mockForegroundChild, + }) + await p + t.strictSame(foregroundChildCalls, [ + [c, ['a/x.y', 'a/b/z.y'], { shell: true }], + ]) + t.strictSame(warnings, [ + [ + 'The --shell option is unsafe, and will be removed. To pass positional arguments to the subprocess, use -g/--cmd-arg instead.', + 'DeprecationWarning', + 'GLOB_SHELL', + ], + ]) +}) + +t.test('safe positional args with --cmd-arg/-g', async t => { + const cwd = t.testdir({ + a: { + 'x.y': '', + 'x.a': '', + b: { + 'z.y': '', + 'z.a': '', + }, + }, + }) + const { SHELL } = process.env + t.teardown(() => (process.env.SHELL = SHELL)) + process.env.SHELL = '/some/unknown/thing' + + const p = expectForegroundChild() + t.chdir(cwd) + const c = 'node' + t.intercept(process, 'argv', { + value: [ + process.argv[0], + 'glob', + '**/*.y', + '-c', + c, + '-g-p', + '--cmd-arg', + 'process.argv.map(s=>s.toUpperCase())', + ], + }) + const warnings: [string, string, string][] = [] + t.intercept(process, 'emitWarning', { + value: (a: string, b: string, c: string) => warnings.push([a, b, c]), + }) + + await t.mockImport('../dist/esm/bin.mjs', { + 'foreground-child': mockForegroundChild, + }) + await p + t.strictSame(foregroundChildCalls, [ + [ + c, + ['-p', 'process.argv.map(s=>s.toUpperCase())', 'a/x.y', 'a/b/z.y'], + { shell: false }, + ], + ]) + t.strictSame(warnings, []) +}) + t.test('prioritizes exact match if exists, unless --all', async t => { const cwd = t.testdir({ routes: {
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-5j98-mcp5-4vw2ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-64756ghsaADVISORY
- github.com/isaacs/node-glob/commit/1e4e297342a09f2aa0ced87fcd4a70ddc325d75fghsax_refsource_MISCWEB
- github.com/isaacs/node-glob/commit/47473c046b91c67269df7a66eab782a6c2716146ghsax_refsource_MISCWEB
- github.com/isaacs/node-glob/security/advisories/GHSA-5j98-mcp5-4vw2ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.