28

I'm trying to use find to echo 0 into some files, but apparently this only works with sh -c:

find /proc/sys/net/ipv6 -name accept_ra -exec sh -c 'echo 0 > {}' \;

But using sh -c with find -exec makes me feel very uneasy because I suspect quoting problems. I fiddled a bit with it and apparently my suspicions were justified:

  • My test setup:

    martin@dogmeat ~ % cd findtest 
    martin@dogmeat ~/findtest % echo one > file\ with\ spaces
    martin@dogmeat ~/findtest % echo two > file\ with\ \'single\ quotes\'
    martin@dogmeat ~/findtest % echo three > file\ with\ \"double\ quotes\"
    martin@dogmeat ~/findtest % ll
    insgesamt 12K
    -rw-rw-r-- 1 martin martin 6 Sep 17 12:01 file with "double quotes"
    -rw-rw-r-- 1 martin martin 4 Sep 17 12:01 file with 'single quotes'
    -rw-rw-r-- 1 martin martin 4 Sep 17 12:01 file with spaces
    
  • Using find -exec without sh -c seems to work without problems - no quoting necessary here:

    martin@dogmeat ~ % find findtest -type f -exec cat {} \;
    one
    two
    three
    
  • But when I'm using sh -c {} seems to require some kind of quoting:

    martin@dogmeat ~ % LANG=C find findtest -type f -exec sh -c 'cat {}' \;
    cat: findtest/file: No such file or directory
    cat: with: No such file or directory
    cat: spaces: No such file or directory
    cat: findtest/file: No such file or directory
    cat: with: No such file or directory
    cat: single quotes: No such file or directory
    cat: findtest/file: No such file or directory
    cat: with: No such file or directory
    cat: double quotes: No such file or directory
    
  • Double quotes work as long as no file name contains double quotes:

    martin@dogmeat ~ % LANG=C find findtest -type f -exec sh -c 'cat "{}"' \;
    one
    two
    cat: findtest/file with double: No such file or directory
    cat: quotes: No such file or directory
    
  • Single quotes work as long as no file name contains single quotes:

    martin@dogmeat ~ % LANG=C find findtest -type f -exec sh -c "cat '{}'" \;
    one
    cat: findtest/file with single: No such file or directory
    cat: quotes: No such file or directory
    three
    

I haven't found a solution that works in all cases. Is there something I'm overlooking, or is using sh -c in find -exec inherently unsafe?

39

Never embed {} in the shell code! That creates a command injection vulnerability. Note that for cat "{}", it's not only about the " characters, \, `, $ are also a problem (consider for instance a file called ./$(reboot)/accept_ra).

(by the way, some find implementations won't let you do that, and POSIX leaves the behaviour unspecified when {} is not on its own in an argument to find)

Here, you'd want to pass the file names as separate arguments to sh (not in the code argument), and the sh inline script (the code argument) to refer to them using positional parameters:

find . -name accept_ra -exec sh -c 'echo 0 > "$1"' sh {} \;

Or, to avoid running one sh per file:

find . -name accept_ra -exec sh -c 'for file do
  echo 0 > "$file"; done' sh {} +

The same applies to xargs -I{} or zsh's zargs -I{}. Don't write:

<list.txt xargs -I{} sh -c 'cmd > {}'

Which would be a command injection vulnerability the same way as with find above, but:

<list.txt xargs sh -c 'for file do cmd > "$file"; done' sh

Which also has the benefit of avoiding running one sh per file and the error when list.txt doesn't contain any file.

With zsh's zargs, you'd probably want to use a function rather than invoking sh -c:

do-it() cmd > $1
zargs ./*.txt -- do-it

Note that in all the examples above the second sh above goes into the inline script's $0. You should use something relevant there (like sh or find-sh), not things like _, -, -- or the empty string, as the value in $0 is used for the shell's error messages:

$ find . -name accept_ra -exec sh -c 'echo 0 > "$1"' inline-sh {} \;
inline-sh: ./accept_ra: Permission denied

GNU parallel works differently. With it, you do not want to use sh -c as parallel does run a shell already and tries to replace {} with the argument quoted in the right syntax for the shell.

<list.txt PARALLEL_SHELL=sh parallel 'cmd > {}'
  • thats second sh seems to be some kind of placeholder, it works too if replaced by _ for example - very useful if you want to call bash internals: find /tmp -name 'fil*' -exec bash -c 'printf "%q\n" "$1"' _ {} \;. But does anybody know where this is documented? – Florian Fida Sep 17 '14 at 14:12
  • 1
    @FlorianFida The first argument to the shell becomes $0 (usually the name of the shell. You need to skip it in this scenario so it doesn't eat one of your normal positional arguments. The documentation for -c mentions this. – Etan Reisner Sep 17 '14 at 14:18
  • 2
    @EtanReisner in-ulm.de/~mascheck/various/find/#embedded – Gilles Sep 17 '14 at 14:35
  • 1
    @phk, that's not argv[0] here, that's just the $0 of the script. Sven's page is inaccurate here, a r won't make the shell enter a restricted mode as far as can tell and zsh won't change mode based on $0. (exec -a rksh ksh -c 'cd /') will run a restricted ksh, but not ksh -c 'cd /' rksh). – Stéphane Chazelas Nov 1 '16 at 16:33
  • 1
    Stéphane, if you don't mind, is there an answer of yours that explains "why not embed {} in the shell code" ? I went though all your answers under find but couldn't find one although I could swear I saw some stuff that you had written about this subject but can't recall if it was an answer, a comment in chat or on another site like compunix... If/when you have the time, would you mind expanding on the "Never embed {}" part or do you think we should have a dedicated Q e.g. "Security implications of using find with -exec sh -c and embedding {} in the shell code" ? – don_crissti Dec 23 '16 at 22:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.