Differences between revisions 8 and 10 (spanning 2 versions)
Revision 8 as of 2018-02-28 14:47:05
Size: 4723
Editor: GreyCat
Comment: @Q and code injection
Revision 10 as of 2020-03-10 20:44:49
Size: 5212
Editor: GreyCat
Comment: add sh-encoding variant
Deletions are marked like this. Additions are marked like this.
Line 87: Line 87:
# Bash 2.05b and up # Bash 3.1 and up
Line 89: Line 89:
unset a i args=()
Line 91: Line 91:
  a[i++]=$(printf %q "$arg")   printf -v temp %q "$arg"
  args+=("$temp"
)
Line 93: Line 94:
exec ssh remotehost cd "$PWD" "&&" make "${a[@]}" printf -v dir %q "$PWD"
exec ssh remotehost cd "$dir" "&&" make "${args[@]}"
Line 96: Line 98:
(If `$PWD` contains spaces, then it also need to be protected with the same `printf %q` trick, left as an exercise for the reader.) The drawback of this approach is that it only works if the remote shell is Bash. Bash's `printf %q` produces output that other shells may not be able to parse (such as `$'\n'` for newlines).
Line 98: Line 100:
The first major drawback of this approach is that it only works if the remote shell is Bash. Bash's `printf %q` produces output that other shells may not be able to parse (such as `$'\n'` for newlines). There is no simple alternative that works for other shells. For other Bourne family shells, the closest approximation is to replace all single quotes in the data with the four characters `'\''` and then enclose the data in single quotes. For example,
Line 100: Line 102:
The second major drawback of this approach is that it allows ''code injection'', because your arguments are parsed as part of the ''code'' on the remote side. For a discussion of this issue and workarounds, see [[BashProgramming/05|avoiding code injection]]. {{{
# POSIX
first=1
for arg in "$@"; do
  test "$first" = 1 && set --
  first=0
  set -- "$@" "'$(printf %s "$arg" | sed "s/'/'\\\\''/g")'"
done
exec ssh remotehost make "$@"
}}}

POSIX sh has no arrays, so we have to use the positional parameters as both input and output. It also can't perform simple character replacements in parameter expansion, so we have to `fork()` a sed process for every single argument. Of course, if the ''client'' is bash, and only the remote server is using sh, then the client script could be written using bash's parameter expansions to generate the same "sh-encoded" arguments. Not shown.

For more discussion of this issue, see [[CodeInjection|avoiding code injection]].

ssh eats my word boundaries! I can't do ssh remotehost make CFLAGS="-g -O"!

ssh emulates the behavior of the Unix remote shell command (rsh or remsh), including this bug. There are a few ways to work around it, depending on exactly what you need.

First, here is a full illustration of the problem:

~$ ~/bin/args make CFLAGS="-g -O"
2 args: <make> <CFLAGS=-g -O>
~$ ssh localhost ~/bin/args make CFLAGS="-g -O"
Password: 
3 args: <make> <CFLAGS=-g> <-O>

What's happening is the command and its arguments are being smashed together into a string on the client side, then shoved through the ssh connection to the server side, where that string is handed to your shell as an argument for re-parsing. This is not what we want.

Manual requoting

The simplest workaround is to mash everything together into a single argument, and manually add quotes in just the right places, until we get it to work.

~$ ssh localhost '~/bin/args make CFLAGS="-g -O"'
Password: 
2 args: <make> <CFLAGS=-g -O>

The shell on the remote host will re-parse the argument, break it into words, and then execute it.

The first problem with this approach is that it's tedious. If we already have both kinds of quotes, and lots of shell substitutions that need to be performed, then we may end up needing to rearrange quite a lot, add backslashes to protect the right things, and so on. The second problem is that it doesn't work very well if our exact command isn't known in advance -- e.g., if we're writing a WrapperScript.

Passing data on stdin instead of the command line

Another workaround is to pass the command(s) as standard input to the remote shell, rather than as an argument. This won't work in all cases; it means the command being executed on the remote system can't use stdin for any other purpose, since we're tying up stdin to send our commands. But in the cases where it can be used, it works quite well:

# POSIX
# Stdin will not be available for use by the remote program
ssh remotehost sh <<EOF
make CFLAGS="-g -O"
EOF

Automatic requoting of each parameter

Let's now consider a more realistic problem: we want to write a wrapper script that invokes make on a remote host, with the arguments provided by the user being passed along intact. This is a lot harder than it would appear at first, because we can't just mash everything together into one word -- the script's caller might use really complex arguments, and quotes, and pathnames with spaces and shell metacharacters, that all need to be preserved carefully. Fortunately for us, bash provides a way to protect such things safely: printf %q. Together with an array and a loop, we can write a wrapper:

# Bash 2.05b and up
# Your account's shell on the remote host MUST BE BASH, not sh
unset a i
for arg; do
  a[i++]=$(printf %q "$arg")
done
exec ssh remotehost make "${a[@]}"

# Bash 3.1 and up
# Your account's shell on the remote host MUST BE BASH, not sh
unset a
for arg; do
  printf -v temp %q "$arg"
  a+=("$temp")
done
exec ssh remotehost make "${a[@]}"

# Bash 4.1 and up
# Your account's shell on the remote host MUST BE BASH, not sh
unset a i
for arg; do
  printf -v 'a[i++]' %q "$arg"
done
exec ssh remotehost make "${a[@]}"

# Bash 4.4 and up
# Your account's shell on the remote host MUST BE BASH, not sh
exec ssh remotehost make "${@@Q}"

See FAQ 73 for a brief description of the bash 4.4 parameter transformation operators (@Q and so on).

If we also need to change directory on the remote host before running make, we can add that as well:

# Bash 3.1 and up
# Your account's shell on the remote host MUST BE BASH, not sh
args=()
for arg; do
  printf -v temp %q "$arg"
  args+=("$temp")
done
printf -v dir %q "$PWD"
exec ssh remotehost cd "$dir" "&&" make "${args[@]}"

The drawback of this approach is that it only works if the remote shell is Bash. Bash's printf %q produces output that other shells may not be able to parse (such as $'\n' for newlines).

For other Bourne family shells, the closest approximation is to replace all single quotes in the data with the four characters '\'' and then enclose the data in single quotes. For example,

# POSIX
first=1
for arg in "$@"; do
  test "$first" = 1 && set --
  first=0
  set -- "$@" "'$(printf %s "$arg" | sed "s/'/'\\\\''/g")'"
done
exec ssh remotehost make "$@"

POSIX sh has no arrays, so we have to use the positional parameters as both input and output. It also can't perform simple character replacements in parameter expansion, so we have to fork() a sed process for every single argument. Of course, if the client is bash, and only the remote server is using sh, then the client script could be written using bash's parameter expansions to generate the same "sh-encoded" arguments. Not shown.

For more discussion of this issue, see avoiding code injection.


CategorySsh CategorySsh CategorySsh

BashFAQ/096 (last edited 2020-03-10 20:44:49 by GreyCat)