VYPR
High severityNVD Advisory· Published Nov 17, 2025· Updated Nov 19, 2025

glob CLI: Command injection via -c/--cmd executes matches with shell:true

CVE-2025-64756

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.

PackageAffected versionsPatched versions
globnpm
>= 11.0.0, < 11.1.011.1.0
globnpm
>= 10.2.0, < 10.5.010.5.0

Affected products

2
  • glob/globllm-create
    Range: >=10.2.0, <10.5.0 <11.1.0
  • isaacs/node-globv5
    Range: >= 10.2.0, < 10.5.0

Patches

2
47473c046b91

bin: Do not expose filenames to shell expansion

https://github.com/isaacs/node-globisaacsNov 13, 2025via ghsa
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: {
    
1e4e297342a0

bin: Do not expose filenames to shell expansion

https://github.com/isaacs/node-globisaacsNov 13, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.