-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
child_process: escape args[] for shell #29576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ba9d9f3
fe443d2
0d8de7d
242ecd9
d75fe07
a01ac85
0ccc509
240fddf
a0c791a
551417b
68018aa
d66746c
19c2125
1e54c83
5821e0a
cdae7ff
770d59a
46d190b
804a346
0dfba45
c0a6aff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -27,6 +27,7 @@ const { | |||||||||||||||||||||
| ArrayPrototypeIncludes, | ||||||||||||||||||||||
| ArrayPrototypeJoin, | ||||||||||||||||||||||
| ArrayPrototypeLastIndexOf, | ||||||||||||||||||||||
| ArrayPrototypeMap, | ||||||||||||||||||||||
| ArrayPrototypePush, | ||||||||||||||||||||||
| ArrayPrototypeSlice, | ||||||||||||||||||||||
| ArrayPrototypeSort, | ||||||||||||||||||||||
|
|
@@ -37,8 +38,12 @@ const { | |||||||||||||||||||||
| ObjectAssign, | ||||||||||||||||||||||
| ObjectDefineProperty, | ||||||||||||||||||||||
| ObjectPrototypeHasOwnProperty, | ||||||||||||||||||||||
| RegExp, | ||||||||||||||||||||||
| RegExpPrototypeTest, | ||||||||||||||||||||||
| SafeSet, | ||||||||||||||||||||||
| String, | ||||||||||||||||||||||
| StringPrototypeEndsWith, | ||||||||||||||||||||||
| StringPrototypeReplace, | ||||||||||||||||||||||
| StringPrototypeSlice, | ||||||||||||||||||||||
| StringPrototypeToUpperCase, | ||||||||||||||||||||||
| } = primordials; | ||||||||||||||||||||||
|
|
@@ -480,6 +485,15 @@ function normalizeSpawnArguments(file, args, options) { | |||||||||||||||||||||
| ['boolean', 'string'], options.shell); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Validate shellEscape, if present. | ||||||||||||||||||||||
| if (options.shellEscape != null && | ||||||||||||||||||||||
| typeof options.shellEscape !== 'boolean' && | ||||||||||||||||||||||
| typeof options.shellEscape !== 'function') { | ||||||||||||||||||||||
| throw new ERR_INVALID_ARG_TYPE('options.shellEscape', | ||||||||||||||||||||||
| ['boolean', 'function'], | ||||||||||||||||||||||
| options.shellEscape); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Validate argv0, if present. | ||||||||||||||||||||||
| if (options.argv0 != null) { | ||||||||||||||||||||||
| validateString(options.argv0, 'options.argv0'); | ||||||||||||||||||||||
|
|
@@ -502,29 +516,54 @@ function normalizeSpawnArguments(file, args, options) { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (options.shell) { | ||||||||||||||||||||||
| const command = ArrayPrototypeJoin([file, ...args], ' '); | ||||||||||||||||||||||
| // Set the shell, switches, and commands. | ||||||||||||||||||||||
| if (process.platform === 'win32') { | ||||||||||||||||||||||
| if (typeof options.shell === 'string') | ||||||||||||||||||||||
| file = options.shell; | ||||||||||||||||||||||
| else | ||||||||||||||||||||||
| file = process.env.comspec || 'cmd.exe'; | ||||||||||||||||||||||
| // '/d /s /c' is used only for cmd.exe. | ||||||||||||||||||||||
| if (RegExpPrototypeTest(/^(?:.*\\)?cmd(?:\.exe)?$/i, file)) { | ||||||||||||||||||||||
| args = ['/d', '/s', '/c', `"${command}"`]; | ||||||||||||||||||||||
| windowsVerbatimArguments = true; | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| args = ['-c', command]; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| const command = file; | ||||||||||||||||||||||
| const commandArgs = ArrayPrototypeMap(args, String); | ||||||||||||||||||||||
| args = ['-c']; | ||||||||||||||||||||||
| let quote = quotePosixShArg; | ||||||||||||||||||||||
| let isCmd = false; | ||||||||||||||||||||||
| const testWindowsExe = | ||||||||||||||||||||||
| process.platform === 'win32' ? | ||||||||||||||||||||||
| (cmd, file) => | ||||||||||||||||||||||
| RegExpPrototypeTest( | ||||||||||||||||||||||
| new RegExp(`^(?:.*\\\\)?${cmd}(?:\\.exe)?$`, 'i'), | ||||||||||||||||||||||
| file | ||||||||||||||||||||||
| ) : | ||||||||||||||||||||||
| (_c, _f) => false; | ||||||||||||||||||||||
| // Set the shell. | ||||||||||||||||||||||
| if (typeof options.shell === 'string') { | ||||||||||||||||||||||
| file = options.shell; | ||||||||||||||||||||||
| } else if (process.platform === 'win32') { | ||||||||||||||||||||||
| file = process.env.comspec || 'cmd.exe'; | ||||||||||||||||||||||
| } else if (process.platform === 'android') { | ||||||||||||||||||||||
| file = '/system/bin/sh'; | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| if (typeof options.shell === 'string') | ||||||||||||||||||||||
| file = options.shell; | ||||||||||||||||||||||
| else if (process.platform === 'android') | ||||||||||||||||||||||
| file = '/system/bin/sh'; | ||||||||||||||||||||||
| else | ||||||||||||||||||||||
| file = '/bin/sh'; | ||||||||||||||||||||||
| args = ['-c', command]; | ||||||||||||||||||||||
| file = '/bin/sh'; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Handle special args for special shells. | ||||||||||||||||||||||
| // '/d /s /c' is used only for cmd.exe. | ||||||||||||||||||||||
| if (testWindowsExe('cmd', file)) { | ||||||||||||||||||||||
| args = ['/d', '/s', '/c']; | ||||||||||||||||||||||
| quote = quoteCmdArg; | ||||||||||||||||||||||
| isCmd = windowsVerbatimArguments = true; | ||||||||||||||||||||||
| } else if ( | ||||||||||||||||||||||
| testWindowsExe('(powershell|pwsh)', file) || | ||||||||||||||||||||||
| StringPrototypeEndsWith(file, '/pwsh') | ||||||||||||||||||||||
| ) { | ||||||||||||||||||||||
| quote = quotePwshArg; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Make it opt-in... for now. | ||||||||||||||||||||||
| if (!options.shellEscape) | ||||||||||||||||||||||
| quote = (s) => s; | ||||||||||||||||||||||
| else if (typeof options.shellEscape == 'function') | ||||||||||||||||||||||
| quote = options.shellEscape; | ||||||||||||||||||||||
|
Comment on lines
+557
to
+560
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if
Suggested change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes you're probably right. Could add a test case for this scenario please? |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Cmd accepts quoted things verbatim under /s. | ||||||||||||||||||||||
| const commandArg = commandArgs.length ? | ||||||||||||||||||||||
| `${command} ${ArrayPrototypeJoin(ArrayPrototypeMap(commandArgs, quote), ' ')}`: | ||||||||||||||||||||||
| command; | ||||||||||||||||||||||
| ArrayPrototypePush(args, isCmd ? `"${commandArg}"` : commandArg); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (typeof options.argv0 === 'string') { | ||||||||||||||||||||||
|
|
@@ -776,6 +815,72 @@ function sanitizeKillSignal(killSignal) { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // This level of indirection is here because the other child_process methods | ||||||||||||||||||||||
| // call spawn internally but should use different cancellation logic. | ||||||||||||||||||||||
| function spawnWithSignal(file, args, options) { | ||||||||||||||||||||||
| const child = spawn(file, args, options); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (options && options.signal) { | ||||||||||||||||||||||
| // Validate signal, if present | ||||||||||||||||||||||
| validateAbortSignal(options.signal, 'options.signal'); | ||||||||||||||||||||||
| function kill() { | ||||||||||||||||||||||
| if (child._handle) { | ||||||||||||||||||||||
| child.kill('SIGTERM'); | ||||||||||||||||||||||
| child.emit('error', new AbortError()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if (options.signal.aborted) { | ||||||||||||||||||||||
| process.nextTick(kill); | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| options.signal.addEventListener('abort', kill); | ||||||||||||||||||||||
| const remove = () => options.signal.removeEventListener('abort', kill); | ||||||||||||||||||||||
| child.once('close', remove); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return child; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function quotePosixShArg(arg) { | ||||||||||||||||||||||
| return `'${StringPrototypeReplace(arg, /'/g, "'\\''")}'`; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function quoteCmdArg(arg) { | ||||||||||||||||||||||
| // We are doing a bit of magic here: | ||||||||||||||||||||||
| // 1. The backslash-escape only cares about the consecutive ones *before* | ||||||||||||||||||||||
| // a quote. That is, before a quote ends or before our magic escape. | ||||||||||||||||||||||
| // 2. The same applies to *not* closing a quote. Throwing a backslash in | ||||||||||||||||||||||
| // there does not really deter CMD, so we use the UNDOCUMENTED double- | ||||||||||||||||||||||
| // the-quotes escape I picked up from CoreFx Sys.Diag.Process.Unix. | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // Don't put newlines in here; there is still no way to escape it. ^ before | ||||||||||||||||||||||
| // EOL only does continuation. | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // As with POSIX, always quoting is simpler and less prone to breakage. | ||||||||||||||||||||||
| // This also makes behavior consistent wrt globs. | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // This is written assuming the receiving program is a normal MSVCRT exe. | ||||||||||||||||||||||
| // Batch files have differences in that cmd double-interprets the string, | ||||||||||||||||||||||
| // but that should not affect us as the result is simply inert to CMD here. | ||||||||||||||||||||||
| // cf. https://daviddeley.com/autohotkey/parameters/parameters.htm#BATCH | ||||||||||||||||||||||
| if (StringPrototypeIncludes(arg, '\n') || StringPrototypeIncludes(arg, '\r')) { | ||||||||||||||||||||||
| throw new ERR_INVALID_ARG_VALUE('arg', arg, | ||||||||||||||||||||||
| 'cannot contain newlines for cmd'); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return `"${StringPrototypeReplace( | ||||||||||||||||||||||
| StringPrototypeReplace(arg, /(\\*)($|")/g, '$1$1$2'), /"/g, '""')}"`; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function quotePwshArg(arg) { | ||||||||||||||||||||||
| // We need to specifically check for powershell now; it does -c but does | ||||||||||||||||||||||
| // not quote in the same way | ||||||||||||||||||||||
| return `'${StringPrototypeReplace(arg, /'/g, "''")}'`; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| module.exports = { | ||||||||||||||||||||||
| _forkChild, | ||||||||||||||||||||||
| ChildProcess, | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.