51

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?

1 Answers1

75

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 -exec)

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 -l1 ./*.txt -- do-it

Though you might as well use a for loop which in zsh can be very short:

for f (*.txt) cmd > $f

(failures of cmd except the last are not reported in the overall exit status though).

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 > {}'

¹ and depending on the sh implementation other characters whose encoding contains the encoding of those characters (in practice and with real-life character encoding, that's limited to bytes 0x5c and 0x60)