I have a bash
shell variable containing a string formed of multiple words delimited by whitespace. The string can contain escapes, such as escaped whitespace within a word. Words containing whitespace may alternatively be quoted.
A shell variable that is used unquoted ($FOO
instead of "$FOO"
) becomes multiple words but quotes and escapes in the original string have no effect.
How can a string be split into words, giving consideration to quoted and escaped characters?
Background
A server offers restricted access over ssh
using the ForceCommand
option in the sshd_config
file to force execution of a script regardless of the command-line given to the ssh
client.
The script uses the variable SSH_ORIGINAL_COMMAND
(which is a string, set by ssh
, that contains the command-line provided to the ssh
client) to set its argument list before proceeding. So, a user doing
$ ssh some_server foo 'bar car' baz
will see the script execute and it will have SSH_ORIGINAL_COMMAND
set to foo bar car baz
which would become four arguments when the script does
set -- ${SSH_ORIGINAL_COMMAND}
Not the desired result. So the user tries again:
$ ssh some_server foo bar\ car baz
Same result - the backslash in the second argument needs to be escaped for the client's shell so ssh
sees it. What about these:
$ ssh some_server foo 'bar\ car' baz
$ ssh some_server foo bar\\ car baz
Both work, as would a printf "%q"
quoting wrapper that can simplify the client-side quoting.
Client-side quoting allows ssh
to send the correctly quoted string to the server so that it receives SSH_ORIGINAL_COMMAND
with the backslash intact: foo bar\ car baz
.
However there is still a problem because set
does not consider the quoting or escaping. There is a solution:
eval set -- ${SSH_ORIGINAL_COMMAND}
but it is unacceptable. Consider
$ ssh some_server \; /bin/sh -i
Very undesirable: eval
can't be used because the input can't be controlled.
What is required is the string expansion capability of eval
without the execution part.
2 Answers 2
Use read
:
read -a ssh_args <<< "${SSH_ORIGINAL_COMMAND}"
set -- "${ssh_args[@]}"
This will parse words from SSH_ORIGINAL_COMMAND
into the array ssh_args
, treating backslash (\
) as an escape character. The array elements are then given as arguments to set
. It works with an argument list passed through ssh
like this:
$ ssh some_server foo 'bar\ car' baz
$ ssh some_server foo bar\\ car baz
A printf "%q" quoting ssh wrapper allows these:
$ sshwrap some_server foo bar\ car baz
$ sshwrap some_server foo 'bar car' baz
Here is such a wrapper example:
#!/bin/bash
h=1ドル; shift
QUOTE_ARGS=''
for ARG in "$@"
do
ARG=$(printf "%q" "$ARG")
QUOTE_ARGS="${QUOTE_ARGS} $ARG"
done
ssh "$h" "${QUOTE_ARGS}"
How to quote an string:
See ${parameter@operator}
section in https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
- For a single var:
"${var@Q}"
a
-> 'a'
, a'b
-> 'a'\''b'
.
- For an array,
"@{array[@]@Q}"
will quote each element in the array then join with space into a big string.
- For program arguments
$@
"${@@Q}"
(probably require Bash 4.4+, if not available you can use printf "%q" ... but you lose the ability of quoting each elements in array)
How to de-quote the quoted string back into array
I only found 1 safe way is as @eel ghEEz pointed:
declare -a array="($QUOTED_ARGS)"
EDIT 2022年08月31日: it is important to pass quoted args to avoid command injection.
More precisely, it should be
JOINED_ARGMENTS_STRING="......"
declare -a array="(${JOINED_ARGMENTS_STRING@Q})"
Sample:
cat <<'EOF' > show_args
#!/bin/bash
for arg in "$@"; do
echo "ARG_$((++i))=$arg"
done
EOF
chmod +x show_args
cat <<'EOF' > test.sh
#!/bin/bash
QUOTED_ARGS=${@@Q} # this is important!!!!!!
echo QUOTED_ARGS is "$QUOTED_ARGS"
echo de-quote QUOTED_ARGS
declare -a args="($QUOTED_ARGS)"
./show_args "${args[@]}"
EOF
chmod +x test.sh
Tests:
ARGS=("a a a" "b'b'b" 'c"c"c')
./show_args "${ARGS[@]}"
you can show it is an array with 3 elements
ARG_1=a a a
ARG_2=b'b'b
ARG_3=c"c"c
Let us see how the quotes and de-quotes work
./test.sh "${ARGS[@]}"
or
./test.sh "a a a" "b'b'b" 'c"c"c'
The result is
QUOTED_ARGS is 'a a a' 'b'\''b'\''b' 'c"c"c'
de-quote QUOTED_ARGS
ARG_1=a a a
ARG_2=b'b'b
ARG_3=c"c"c
EDIT 2022年08月31日: added a test of injection attempts:
./test.sh "\$(echo test >&2)"
Result:
QUOTED_ARGS is '$(echo test >&2)'
de-quote QUOTED_ARGS
ARG_1=$(echo test >&2)
The "echo test" command did not get called, that is fine.
Successfully restored.
-
1This still seems to be implemented unsafely by Bash, similarly to
eval
.inc="\$(echo test >&2)"
, thendeclare -a x="("${inc}"/*)"
outputstest
immediately (to stderr).eel ghEEz– eel ghEEz2022年08月25日 20:32:28 +00:00Commented Aug 25, 2022 at 20:32 -
@eelghEEz thanks for pointing out this, that is really dangerous.osexp2000– osexp20002022年08月31日 11:26:54 +00:00Commented Aug 31, 2022 at 11:26
-
@eelghEEz, strangely, I found that
./test.sh "\$(echo test >&2)"
runs well, no injection happened, wondering why.osexp2000– osexp20002022年08月31日 11:35:49 +00:00Commented Aug 31, 2022 at 11:35 -
Got the reason, it is because I ran
QUOTED_ARGS=${@@Q}
first, so avoided the injection.osexp2000– osexp20002022年08月31日 11:36:38 +00:00Commented Aug 31, 2022 at 11:36 -
@eelghEEz I have tested the injection attempts, it is fine now, no worry.osexp2000– osexp20002022年08月31日 11:45:55 +00:00Commented Aug 31, 2022 at 11:45
You must log in to answer this question.
Explore related questions
See similar questions with these tags.
declare
instead ofeval
. But that's just as evil. And so islocal
.declare -a m="(${s})"
appears to expand quotes yet does not allow injecting a command by using a semicolon;
or a closing brace)
. It does throw a runtime syntax error that cannot be caught.