54

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 Answer 1

78

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)

13
  • 3
    @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. Commented Sep 17, 2014 at 14:18
  • 2
    @EtanReisner in-ulm.de/~mascheck/various/find/#embedded Commented Sep 17, 2014 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). Commented Nov 1, 2016 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" ? Commented Dec 23, 2016 at 22:01
  • 2
    @don_crissti, there's How to escape shell metacharacters automatically with `find` command? and possibly others. A dedicated Q&A (for find and xargs) if it doesn't exist already would probably be a good idea, yes, though here it's a specific case of code injection so there's a broader range of things to avoid in general. See also Do I need to encapsulate awk variables in quotes in order to sanitize them? Commented Dec 24, 2016 at 8:25

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .