shell – Running Commands with Untrusted Data in Shell

shell

From findutils' manual:

For example constructs such as these two commands

# risky
find -exec sh -c "something {}" \;
find -execdir sh -c "something {}" \;

are very dangerous. The reason for this is that the ‘{}’ is expanded
to a filename which might contain a semicolon or other characters
special to the shell. If for example someone creates the file
/tmp/foo; rm -rf $HOME then the two commands above could delete
someone’s home directory.

So for this reason do not run any command which will pass untrusted
data (such as the names of fi les) to commands which interpret
arguments as commands to be further interpreted (for example ‘sh’).

In the case of the shell, there is a clever workaround for this
problem:

# safer
find -exec sh -c 'something "$@"' sh {} \;
find -execdir sh -c 'something "$@"' sh {} \;

This approach is not guaranteed to avoid every problem, but it is much
safer than substituting data of an attacker’s choice into the text of
a shell command.

  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?
  2. In the solution find -exec sh -c 'something "$@"' sh {} \;,

    • first {} is replaced, 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"?

    • why is {} not escaped or quoted?

  3. Could you give other examples (still with sh -c, or without it if
    applicable; with or without find which may be not necessary) where the same kind of problem and solution apply, and
    which are minimal examples so that we can focus on the problem and
    solution with little distraction as possible? See Ways to provide arguments to a command executed by `bash -c`

Thanks.

Best Answer

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
Related Question