You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
child_process.exec() fails with spaces in absolute or relative path to binary file #6803
It would appear that the path handling at that point is happening at the libuv layer. We could likely do some prints from the c++ layer to verify, but I'm 99% that we are just passing the filepath as given as long as the shell option is not given.
This means that the options.shell block is in fact executed so that file is modified:
file = '/bin/sh';
args = ['-c', command];
This is eventually executed on Linux and OS X by libuv using execvp without further modification of the arguments (On Windows, libuv calls quote_cmd_arg on each argument if windowsVerbatimArguments is true). If one were to execute this in a Unix terminal it would look like:
/bin/sh -c "command"
So exec('echo hello world') would be:
/bin/sh -c "echo hello world"
And exec('/Users/Joran/test script.sh') would be:
/bin/sh -c "/Users/Joran/test script.sh"
Therein lies the problem, /bin/sh is interpreting the command string as command plus arguments, so that the command is seen as /Users/Joran/test and the arguments are seen as script.sh.
It looks like Windows is also affected. I have also found the reason why my earlier test script.sh using a relative path succeeded, /bin/test is a valid binary. So relative paths are also affected, it is not just absolute paths.
Should exec() escape special characters in the binary file argument? I think so. Other interfaces such as fs.stat() already handle special characters in the path.
I am sure there are users who are already passing a fully quoted or partially quoted/escaped file to exec(). It is also more complicated then just wrapping with quotes if quotes are not yet already used, as quotes may be literal, and users may have escaped characters rather than quoting them.
I would like to make this change as safe and backward compatible as possible. I think what would work best is if we escape any special character which is not yet already quoted or escaped, as follows:
These should all then work when they end up being called. For example:
/bin/sh -c "./foo\" & \"bar.sh"
Should the escaping be done in the options.shell block (this would then apply to exec, execFile, spawn etc.)? This is where file is used to form command:
const command = [file].concat(args).join(' ');
What have I missed? Are there any other edge cases to consider? What should we consider to be quote-worthy special characters on Windows and on Linux and OS X? Anything that's not in [a-zA-Z0-9\.\/]? Would this affect alternate data streams on Windows if : is quoted?
These changes should hopefully not break anything, but if you want me to proceed then I think they should still best be landed only in the next semver major. It would be good also to get some more eyes on this.
changed the title [-]child_process.exec() fails with spaces in absolute path to binary[/-][+]child_process.exec() fails with spaces in absolute or relative path to binary file[/+]on May 18, 2016
Could we pass the command to JSON.stringify() to do the escaping? I'm not sure if this takes care of all of the edge cases or not, but I've seen it used in some of our tests, such as test/parallel/test-stdout-close-catch.js.
Not escaping spaces is the expected behavior. If exec() started escaping, then e.g. exec('echo hello') would stop working.
Not a bug, IMO, although the documentation for exec() can probably be more explicit; "executes the command within that shell" is currently all it says.
@bnoordhuis ... good point. That would imply that the right solution here is to expand the documentation to address the fact that paths with spaces need to be quoted before passing in.
@bnoordhuis Thanks, I came across this via exec() and it make sense we can't escape exec() commands. The intention was just to escape the file argument, not the command itself.
What about execFile? Does the file argument there escape spaces and special characters?
Activity
jorangreef commentedon May 17, 2016
If the same binary is passed as a relative path, e.g. just
test script.shthenexec()succeeds.jasnell commentedon May 17, 2016
Yep, quick test locally confirms this. As a workaround, if you wrap the command in quotes it should work, e.g.:
Looks like we need to make sure we're wrapping it in quotes internally if is isn't wrapped already.
jasnell commentedon May 17, 2016
Care to make a PR? :-)
jorangreef commentedon May 17, 2016
Sure.
I would like to understand a bit more about why
test script.shas a relative path works when the same file as an absolute path does not?I tried tracing the
exec()call through but didn't see where it is being escaped as a relative path or passed to/bin/sh?MylesBorins commentedon May 17, 2016
@jorangreef I did some digging for you
execeventually callsspawnand just passes the file argumentspawnsanitizes the input and then callsinternal/child_process.prototype.spawnIf the
shelloption is passed tochild.spawnthen file is modified This does not appear to be the case in this instance... but this is likely a good place to modifyfileIn
internal/child_process.prototype.spawnthe file is untouched and passed tothis._handle.spawnasthis.spawnfilethis._handleis an instance ofProcesswhich is a binding toprocess_wrap.ProcessThe
process_wrap.ProcessClass has it's name set here.Process.prototype.spawnis set here and references this method. The filepath passed as an option to this method is handled here. It is assigned to options.file and passed to libuvIt would appear that the path handling at that point is happening at the libuv layer. We could likely do some prints from the c++ layer to verify, but I'm 99% that we are just passing the filepath as given as long as the
shelloption is not given.jorangreef commentedon May 18, 2016
Thanks @thealphanerd!
I see that the
shelloption is in fact passed astruebecause of normalizeExecArgs which does:This means that the
options.shellblock is in fact executed so thatfileis modified:This is eventually executed on Linux and OS X by
libuvusingexecvpwithout further modification of the arguments (On Windows,libuvcallsquote_cmd_argon each argument ifwindowsVerbatimArgumentsis true). If one were to execute this in a Unix terminal it would look like:So
exec('echo hello world')would be:And
exec('/Users/Joran/test script.sh')would be:Therein lies the problem,
/bin/shis interpreting the command string as command plus arguments, so that the command is seen as/Users/Joran/testand the arguments are seen asscript.sh.jorangreef commentedon May 18, 2016
@jasnell and @thealphanerd
It looks like Windows is also affected. I have also found the reason why my earlier
test script.shusing a relative path succeeded,/bin/testis a valid binary. So relative paths are also affected, it is not just absolute paths.Should
exec()escape special characters in the binaryfileargument? I think so. Other interfaces such asfs.stat()already handle special characters in the path.I am sure there are users who are already passing a fully quoted or partially quoted/escaped
filetoexec(). It is also more complicated then just wrapping with quotes if quotes are not yet already used, as quotes may be literal, and users may have escaped characters rather than quoting them.I would like to make this change as safe and backward compatible as possible. I think what would work best is if we escape any special character which is not yet already quoted or escaped, as follows:
echobecomesecho/Users/Joran Greef/"foo bar.sh"becomes/Users/Joran" "Greef/"foo bar.sh"/Users/Joran Greef/'foo bar.sh'becomes/Users/Joran" "Greef/'foo bar.sh'/Users/Joran Greef/foo\ bar.shbecomes/Users/Joran" "Greef/foo\ bar.shfoo bar.shbecomesfoo" "bar.shfoo & bar.shbecomesfoo" & "bar.shfoo\ bar.shbecomesfoo\ bar.sh"foo bar.sh"becomes"foo bar.sh""foo\ bar.sh"becomes"foo\ bar.sh"'foo bar.sh'becomes'foo bar.sh''foo\ bar.sh'becomes'foo\ bar.sh'These should all then work when they end up being called. For example:
/bin/sh -c "./foo\" & \"bar.sh"Should the escaping be done in the
options.shellblock (this would then apply toexec,execFile,spawnetc.)? This is wherefileis used to formcommand:What have I missed? Are there any other edge cases to consider? What should we consider to be quote-worthy special characters on Windows and on Linux and OS X? Anything that's not in
[a-zA-Z0-9\.\/]? Would this affect alternate data streams on Windows if:is quoted?These changes should hopefully not break anything, but if you want me to proceed then I think they should still best be landed only in the next semver major. It would be good also to get some more eyes on this.
[-]child_process.exec() fails with spaces in absolute path to binary[/-][+]child_process.exec() fails with spaces in absolute or relative path to binary file[/+]cjihrig commentedon May 18, 2016
Could we pass the command to
JSON.stringify()to do the escaping? I'm not sure if this takes care of all of the edge cases or not, but I've seen it used in some of our tests, such astest/parallel/test-stdout-close-catch.js.bnoordhuis commentedon May 18, 2016
Not escaping spaces is the expected behavior. If exec() started escaping, then e.g.
exec('echo hello')would stop working.Not a bug, IMO, although the documentation for exec() can probably be more explicit; "executes the
commandwithin that shell" is currently all it says.jasnell commentedon May 18, 2016
@bnoordhuis ... good point. That would imply that the right solution here is to expand the documentation to address the fact that paths with spaces need to be quoted before passing in.
jorangreef commentedon May 19, 2016
@bnoordhuis Thanks, I came across this via
exec()and it make sense we can't escapeexec()commands. The intention was just to escape thefileargument, not thecommanditself.What about
execFile? Does thefileargument there escape spaces and special characters?bnoordhuis commentedon May 19, 2016
No, it's passed verbatim as the
argv[0]argument toexecvp().cp.execFile('/Users/Joran/test script.sh')should do what you expect it to, provided the script is executable and has a shebang.6 remaining items
doc: child_process exec string is processed by shell
Escape path passed into spawn so `npm start` works if there are space…
Escape path passed into spawn so `npm start` works if there are space…
Allow execution of post-install scripts on paths with space
Allow execution of post-install scripts on paths with space
doc: child_process example for special chars