Skip to content

Commit 1e4e297

Browse files
committed
bin: Do not expose filenames to shell expansion
Backport of 47473c0 from main to v10 branch fix: #636
1 parent 1f0c1ca commit 1e4e297

File tree

5 files changed

+388
-40
lines changed

5 files changed

+388
-40
lines changed

changelog.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# changeglob
22

3+
## 10.5
4+
5+
Backport fix for
6+
[GHSA-5j98-mcp5-4vw2](https://github.com/isaacs/node-glob/security/advisories/GHSA-5j98-mcp5-4vw2)
7+
to v10 branch.
8+
9+
- Add the `--shell` option for the command line, with a warning
10+
that this is unsafe. (It will be removed in v12.)
11+
- Add the `--cmd-arg`/`-g` as a way to _safely_ add positional
12+
arguments to the command provided to the CLI tool.
13+
- Detect commands with space or quote characters on known shells,
14+
and pass positional arguments to them safely, avoiding
15+
`shell:true` execution.
16+
317
## 10.4
418

519
- Add `includeChildMatches: false` option

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@
2121
"./package.json": "./package.json",
2222
".": {
2323
"import": {
24-
"source": "./src/index.ts",
2524
"types": "./dist/esm/index.d.ts",
2625
"default": "./dist/esm/index.js"
2726
},
2827
"require": {
29-
"source": "./src/index.ts",
3028
"types": "./dist/commonjs/index.d.ts",
3129
"default": "./dist/commonjs/index.js"
3230
}

src/bin.mts

Lines changed: 145 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { foregroundChild } from 'foreground-child'
33
import { existsSync } from 'fs'
44
import { jack } from 'jackspeak'
55
import { loadPackageJson } from 'package-json-from-dist'
6-
import { join } from 'path'
6+
import { basename, join } from 'path'
77
import { globStream } from './index.js'
88

99
const { version } = loadPackageJson(import.meta.url, '../package.json')
@@ -35,6 +35,50 @@ const j = jack({
3535
this pattern`,
3636
},
3737
})
38+
.flag({
39+
shell: {
40+
default: false,
41+
description: `Interpret the command as a shell command by passing it
42+
to the shell, with all matched filesystem paths appended,
43+
**even if this cannot be done safely**.
44+
45+
This is **not** unsafe (and usually unnecessary) when using
46+
the known Unix shells sh, bash, zsh, and fish, as these can
47+
all be executed in such a way as to pass positional
48+
arguments safely.
49+
50+
**Note**: THIS IS UNSAFE IF THE FILE PATHS ARE UNTRUSTED,
51+
because a path like \`'some/path/\\$\\(cmd)'\` will be
52+
executed by the shell.
53+
54+
If you do have positional arguments that you wish to pass to
55+
the command ahead of the glob pattern matches, use the
56+
\`--cmd-arg\`/\`-g\` option instead.
57+
58+
The next major release of glob will fully remove the ability
59+
to use this option unsafely.`,
60+
},
61+
})
62+
.optList({
63+
'cmd-arg': {
64+
short: 'g',
65+
hint: 'arg',
66+
default: [],
67+
description: `Pass the provided values to the supplied command, ahead of
68+
the glob matches.
69+
70+
For example, the command:
71+
72+
glob -c echo -g"hello" -g"world" *.txt
73+
74+
might output:
75+
76+
hello world a.txt b.txt
77+
78+
This is a safer (and future-proof) alternative than putting
79+
positional arguments in the \`-c\`/\`--cmd\` option.`,
80+
},
81+
})
3882
.flag({
3983
all: {
4084
short: 'A',
@@ -78,7 +122,7 @@ const j = jack({
78122
description: `Always resolve to posix style paths, using '/' as the
79123
directory separator, even on Windows. Drive letter
80124
absolute matches on Windows will be expanded to their
81-
full resolved UNC maths, eg instead of 'C:\\foo\\bar',
125+
full resolved UNC paths, eg instead of 'C:\\foo\\bar',
82126
it will expand to '//?/C:/foo/bar'.
83127
`,
84128
},
@@ -215,8 +259,10 @@ const j = jack({
215259
description: `Output a huge amount of noisy debug information about
216260
patterns as they are parsed and used to match files.`,
217261
},
218-
})
219-
.flag({
262+
version: {
263+
short: 'V',
264+
description: `Output the version (${version})`,
265+
},
220266
help: {
221267
short: 'h',
222268
description: 'Show this usage information',
@@ -225,50 +271,113 @@ const j = jack({
225271

226272
try {
227273
const { positionals, values } = j.parse()
228-
if (values.help) {
274+
const {
275+
cmd,
276+
shell,
277+
all,
278+
default: def,
279+
version: showVersion,
280+
help,
281+
absolute,
282+
cwd,
283+
dot,
284+
285+
'dot-relative': dotRelative,
286+
follow,
287+
ignore,
288+
'match-base': matchBase,
289+
'max-depth': maxDepth,
290+
mark,
291+
nobrace,
292+
nocase,
293+
nodir,
294+
noext,
295+
noglobstar,
296+
platform,
297+
realpath,
298+
root,
299+
stat,
300+
debug,
301+
posix,
302+
'cmd-arg': cmdArg,
303+
} = values
304+
if (showVersion) {
305+
console.log(version)
306+
process.exit(0)
307+
}
308+
if (help) {
229309
console.log(j.usage())
230310
process.exit(0)
231311
}
232-
if (positionals.length === 0 && !values.default)
233-
throw 'No patterns provided'
234-
if (positionals.length === 0 && values.default)
235-
positionals.push(values.default)
312+
//const { shell, help } = values
313+
if (positionals.length === 0 && !def) throw 'No patterns provided'
314+
if (positionals.length === 0 && def) positionals.push(def)
236315
const patterns =
237-
values.all ? positionals : positionals.filter(p => !existsSync(p))
316+
all ? positionals : positionals.filter(p => !existsSync(p))
238317
const matches =
239-
values.all ?
240-
[]
241-
: positionals.filter(p => existsSync(p)).map(p => join(p))
318+
all ? [] : positionals.filter(p => existsSync(p)).map(p => join(p))
319+
242320
const stream = globStream(patterns, {
243-
absolute: values.absolute,
244-
cwd: values.cwd,
245-
dot: values.dot,
246-
dotRelative: values['dot-relative'],
247-
follow: values.follow,
248-
ignore: values.ignore,
249-
mark: values.mark,
250-
matchBase: values['match-base'],
251-
maxDepth: values['max-depth'],
252-
nobrace: values.nobrace,
253-
nocase: values.nocase,
254-
nodir: values.nodir,
255-
noext: values.noext,
256-
noglobstar: values.noglobstar,
257-
platform: values.platform as undefined | NodeJS.Platform,
258-
realpath: values.realpath,
259-
root: values.root,
260-
stat: values.stat,
261-
debug: values.debug,
262-
posix: values.posix,
321+
absolute,
322+
cwd,
323+
dot,
324+
dotRelative,
325+
follow,
326+
ignore,
327+
mark,
328+
matchBase,
329+
maxDepth,
330+
nobrace,
331+
nocase,
332+
nodir,
333+
noext,
334+
noglobstar,
335+
platform: platform as undefined | NodeJS.Platform,
336+
realpath,
337+
root,
338+
stat,
339+
debug,
340+
posix,
263341
})
264342

265-
const cmd = values.cmd
266343
if (!cmd) {
267344
matches.forEach(m => console.log(m))
268345
stream.on('data', f => console.log(f))
269346
} else {
270-
stream.on('data', f => matches.push(f))
271-
stream.on('end', () => foregroundChild(cmd, matches, { shell: true }))
347+
cmdArg.push(...matches)
348+
stream.on('data', f => cmdArg.push(f))
349+
// Attempt to support commands that contain spaces and otherwise require
350+
// shell interpretation, but do NOT shell-interpret the arguments, to avoid
351+
// injections via filenames. This affordance can only be done on known Unix
352+
// shells, unfortunately.
353+
//
354+
// 'bash', ['-c', cmd + ' "$@"', 'bash', ...matches]
355+
// 'zsh', ['-c', cmd + ' "$@"', 'zsh', ...matches]
356+
// 'fish', ['-c', cmd + ' "$argv"', ...matches]
357+
const { SHELL = 'unknown' } = process.env
358+
const shellBase = basename(SHELL)
359+
const knownShells = ['sh', 'ksh', 'zsh', 'bash', 'fish']
360+
if (
361+
(shell || /[ "']/.test(cmd)) &&
362+
knownShells.includes(shellBase)
363+
) {
364+
const cmdWithArgs = `${cmd} "\$${shellBase === 'fish' ? 'argv' : '@'}"`
365+
if (shellBase !== 'fish') {
366+
cmdArg.unshift(SHELL)
367+
}
368+
cmdArg.unshift('-c', cmdWithArgs)
369+
stream.on('end', () => foregroundChild(SHELL, cmdArg))
370+
} else {
371+
if (shell) {
372+
process.emitWarning(
373+
'The --shell option is unsafe, and will be removed. To pass ' +
374+
'positional arguments to the subprocess, use -g/--cmd-arg instead.',
375+
'DeprecationWarning',
376+
'GLOB_SHELL',
377+
)
378+
}
379+
stream.on('end', () => foregroundChild(cmd, cmdArg, { shell }))
380+
}
272381
}
273382
} catch (e) {
274383
console.error(j.usage())

tap-snapshots/test/bin.ts.test.cjs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,45 @@ Object {
3131
If no positional arguments are provided, glob will use
3232
this pattern
3333
34+
--shell Interpret the command as a shell command by passing it
35+
to the shell, with all matched filesystem paths
36+
appended,
37+
**even if this cannot be done safely**.
38+
39+
This is **not** unsafe (and usually unnecessary) when
40+
using the known Unix shells sh, bash, zsh, and fish, as
41+
these can all be executed in such a way as to pass
42+
positional arguments safely.
43+
44+
**Note**: THIS IS UNSAFE IF THE FILE PATHS ARE
45+
UNTRUSTED, because a path like \`'some/path/\\\\$\\\\(cmd)'\`
46+
will be executed by the shell.
47+
48+
If you do have positional arguments that you wish to
49+
pass to the command ahead of the glob pattern matches,
50+
use the \`--cmd-arg\`/\`-g\` option instead.
51+
52+
The next major release of glob will fully remove the
53+
ability to use this option unsafely.
54+
55+
-g<arg> --cmd-arg=<arg>
56+
Pass the provided values to the supplied command, ahead
57+
of the glob matches.
58+
59+
For example, the command:
60+
61+
glob -c echo -g"hello" -g"world" *.txt
62+
63+
might output:
64+
65+
hello world a.txt b.txt
66+
67+
This is a safer (and future-proof) alternative than
68+
putting positional arguments in the \`-c\`/\`--cmd\`
69+
option.
70+
71+
Can be set multiple times
72+
3473
-A --all By default, the glob cli command will not expand any
3574
arguments that are an exact match to a file on disk.
3675
@@ -60,7 +99,7 @@ Object {
6099
-x --posix Always resolve to posix style paths, using '/' as the
61100
directory separator, even on Windows. Drive letter
62101
absolute matches on Windows will be expanded to their
63-
full resolved UNC maths, eg instead of 'C:\\\\foo\\\\bar', it
102+
full resolved UNC paths, eg instead of 'C:\\\\foo\\\\bar', it
64103
will expand to '//?/C:/foo/bar'.
65104
66105
-f --follow Follow symlinked directories when expanding '**'
@@ -143,8 +182,35 @@ Object {
143182
-v --debug Output a huge amount of noisy debug information about
144183
patterns as they are parsed and used to match files.
145184
185+
-V --version Output the version ({VERSION})
146186
-h --help Show this usage information
147187
148188
),
149189
}
150190
`
191+
192+
exports[`test/bin.ts > TAP > version > --version shows version 1`] = `
193+
Object {
194+
"args": Array [
195+
"--version",
196+
],
197+
"code": 0,
198+
"options": Object {},
199+
"signal": null,
200+
"stderr": "",
201+
"stdout": "{VERSION}\\n",
202+
}
203+
`
204+
205+
exports[`test/bin.ts > TAP > version > -V shows version 1`] = `
206+
Object {
207+
"args": Array [
208+
"-V",
209+
],
210+
"code": 0,
211+
"options": Object {},
212+
"signal": null,
213+
"stderr": "",
214+
"stdout": "{VERSION}\\n",
215+
}
216+
`

0 commit comments

Comments
 (0)