"run any command which will pass untrusted data to commands which interpret arguments as commands"
This isn’t really related to quoting, but rather to argument processing.
Consider the risky example:
find -exec sh -c "something {}" \;
This is parsed by the shell, and split into six words:
find
,-exec
,sh
,-c
,something {}
(no quotes any more),;
. There’s nothing to expand. The shell runsfind
with those six words as arguments.When
find
finds something to process, sayfoo; rm -rf $HOME
, it replaces{}
withfoo; rm -rf $HOME
, and runssh
with the argumentssh
,-c
, andsomething foo; rm -rf $HOME
.sh
now sees-c
, and as a result parsessomething foo; rm -rf $HOME
(the first non-option argument) and executes the result.
Now consider the safer variant:
find -exec sh -c 'something "$@"' sh {} \;
The shell runs
find
with the argumentsfind
,-exec
,sh
,-c
,something "$@"
,sh
,{}
,;
.Now when
find
findsfoo; rm -rf $HOME
, it replaces{}
again, and runssh
with the argumentssh
,-c
,something "$@"
,sh
,foo; rm -rf $HOME
.sh
sees-c
, and parsessomething "$@"
as the command to run, andsh
andfoo; rm -rf $HOME
as the positional parameters (starting from$0
), expands"$@"
tofoo; rm -rf $HOME
as a single value, and runssomething
with the single argumentfoo; rm -rf $HOME
.
You can see this by using printf
. Create a new directory, enter it, and run
touch "hello; echo pwned"
Running the first variant as follows
find -exec sh -c "printf \"Argument: %s\n\" {}" \;
produces
Argument: .
Argument: ./hello
pwned
whereas the second variant, run as
find -exec sh -c 'printf "Argument: %s\n" "$@"' sh {} \;
produces
Argument: .
Argument: ./hello; echo pwned
Part 1:
find
just uses text replacement.
Yes, if you did it unquoted, like this:
find . -type f -exec sh -c "echo {}" \;
and an attacker was able to create a file called ; echo owned
, then it would exec
sh -c "echo ; echo owned"
which would result in the shell running echo
then echo owned
.
But if you added quotes, the attacker could just end your quotes then put the malicious command after it by creating a file called '; echo owned
:
find . -type f -exec sh -c "echo '{}'" \;
which would result in the shell running echo ''
, echo owned
.
(if you swapped the double quotes for single quotes, the attacker could use the other type of quotes too.)
Part 2:
In find -exec sh -c 'something "$@"' sh {} \;
, the {}
is not initially interpreted by the shell, it's executed directly with execve
, so adding shell quotes wouldn't help.
find -exec sh -c 'something "$@"' sh "{}" \;
has no effect, since the shell strips the double quotes before running find
.
find -exec sh -c 'something "$@"' sh "'{}'" \;
adds quotes that the shell doesn't treat specially, so in most cases it just means the command won't do what you want.
Having it expand to /tmp/foo;
, rm
, -rf
, $HOME
shouldn't be a problem, because those are arguments to something
, and something
probably doesn't treat its arguments as commands to execute.
Part 3:
I assume similar considerations apply for anything that takes untrusted input and runs it as (part of) a command, for example xargs
and parallel
.
1. Is the cause of the problem in
find -exec sh -c "something {}" \;
that the replacement for{}
is unquoted and therefore not treated as a single string?
In a sense, but quoting cannot help here. The filename that gets replaced in place of {}
can contain any characters, including quotes. Whatever form of quoting was used, the filename could contain the same, and "break out" of the quoting.
2. ... but since
{}
is unquoted, doesn't"$@"
also have the same problem as the original command? For example,"$@"
will be expanded to"/tmp/foo;", "rm", "-rf", and "$HOME"
?
No. "$@"
expands to the positional parameters, as separate words, and doesn't split them further. Here, {}
is an argument to find
in itself, and find
passes the current filename also as a distinct argument to sh
. It's directly available as a variable in the shell script, it's not processed as a shell command itself.
... why is
{}
not escaped or quoted?
It doesn't need to be, in most shells. If you run fish
, it needs to be: fish -c 'echo {}'
prints an empty line. But it doesn't matter if you quote it, the shell will just remove the quotes.
3. Could you give other examples...
Any time you expand a filename (or another uncontrolled string) as-is inside a string that's taken as some kind of code(*), there's a possibility of arbitrary command execution.
For example, this expands $f
directly the Perl code, and will cause problems if a filename contains a double quote. The quote in the filename will end the quote in the Perl code, and the rest of the filename can contain any Perl code:
touch '"; print "HELLO";"'
for f in ./*; do
perl -le "print \"size: \" . -s \"$f\""
done
(The filename has to be a bit weird since Perl parses the whole code up front, before running any of it. So we'll have to avoid a parse error.)
While this passes it safely through an argument:
for f in ./*; do
perl -le 'print "size: " . -s $ARGV[0]' "$f"
done
(It doesn't make sense to run another shell directly from a shell, but if you do, it's similar to the find -exec sh ...
case)
(* some kind of code includes SQL, so obligatory XKCD: https://xkcd.com/327/ plus explanation: https://www.explainxkcd.com/wiki/index.php/Little_Bobby_Tables )