Repository navigation
child_process.exec() fails with spaces in absolute or relative path to binary file #6803
Description
Activity
If the same binary is passed as a relative path, e.g. just
test script.shthenexec()succeeds.- addedchild_processIssues and PRs related to the child_process subsystem.Issues and PRs related to the child_process subsystem.
on May 17, 2016 - addedconfirmed-bugIssues and PRs for confirmed bugs.Issues and PRs for confirmed bugs.
on May 17, 2016 Yep, quick test locally confirms this. As a workaround, if you wrap the command in quotes it should work, e.g.:
child_process.exec('"/path/to/test script.sh"', (err, stdout, stderr) => { });
Looks like we need to make sure we're wrapping it in quotes internally if is isn't wrapped already.
Care to make a PR? :-)
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?@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 modifyfilethis._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.Reacted by Amin YaThanks @thealphanerd!
I see that the
shelloption is in fact passed astruebecause of normalizeExecArgs which does:options.shell = typeof options.shell === 'string' ? options.shell : true;This means that the
options.shellblock is in fact executed so thatfileis modified:file = '/bin/sh'; args = ['-c', command];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:/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/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.Reacted by cassieIt 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.sh
foo bar.shbecomesfoo" "bar.sh
foo & bar.shbecomesfoo" & "bar.sh
foo\ 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: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 astest/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
commandwithin 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 escapeexec()commands. The intention was just to escape thefileargument, not thecommanditself.What about
execFile? Does thefileargument there escape spaces and special characters?What about execFile? Does the file argument there escape spaces and special characters?
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.Reacted by Amin Ya6 remaining items
- addeddocIssues and PRs related to Node.js documentation.Issues and PRs related to Node.js documentation.and removeddocIssues and PRs related to Node.js documentation.Issues and PRs related to Node.js documentation.
on Dec 1, 2016 - added a commit that references this issue
on Mar 27, 2017 - added a commit that references this issue
on Apr 27, 2017 - added a commit that references this issue
on May 1, 2017 - added a commit that references this issue
on Jul 12, 2018 - added a commit that references this issue
on Jul 19, 2018 - added a commit that references this issue
on Jul 27, 2026
Using exec() to execute an absolute path to a binary, with spaces in the absolute path, e.g.
/Users/Joran/test script.shfails: