"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 runs find with those six words as arguments.

  • When find finds something to process, say foo; rm -rf $HOME, it replaces {} with foo; rm -rf $HOME, and runs sh with the arguments sh, -c, and something foo; rm -rf $HOME.

  • sh now sees -c, and as a result parses something 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 arguments find, -exec, sh, -c, something "$@", sh, {}, ;.

  • Now when find finds foo; rm -rf $HOME, it replaces {} again, and runs sh with the arguments sh, -c, something "$@", sh, foo; rm -rf $HOME.

  • sh sees -c, and parses something "$@" as the command to run, and sh and foo; rm -rf $HOME as the positional parameters (starting from $0), expands "$@" to foo; rm -rf $HOME as a single value, and runs something with the single argument foo; 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 )

Tags:

Shell