49

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?

Martin von Wittich
  • 13,857
  • 6
  • 51
  • 74

1 Answers1

73

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)

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
  • 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 F Sep 17 '14 at 14:12
  • 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. – Etan Reisner Sep 17 '14 at 14:18
  • Do you have a reference to `find` not allowing that embedding? – Etan Reisner Sep 17 '14 at 14:20
  • 2
    @EtanReisner http://www.in-ulm.de/~mascheck/various/find/#embedded – Gilles 'SO- stop being evil' Sep 17 '14 at 14:35
  • Thanks. That's a nice reference and +1 for the the `s/most/some/` in your answer. – Etan Reisner Sep 17 '14 at 14:38
  • +1, though I think using `sh` has its pluses and minuses. It gives better error messages, but isn't quite as clear to people as a placeholder vs `_`, which would be common and very intuitive. –  Sep 18 '14 at 00:04
  • @BroSlow, I couldn't agree less. It's not a placeholder. People need to know that's to set `$0`, and using `_` would deceive them into thinking it's a placeholder. And I can't see what's intuitive about `_` (it's even more cumbersome to type than `sh` on my British keyboard). If you want something shorter, use `.`. At least that suggests the `.` (`source`) command which would be slightly more appropriate for `$0`. – Stéphane Chazelas Sep 18 '14 at 08:37
  • @Stéphane Chazelas I guess it might be also worth noting that some programs might behave differently with a different `argv[0]`. – phk Nov 01 '16 at 15:44
  • 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 01 '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 [tag: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
  • 1
    @don_crissti, there's [How to escape shell metacharacters automatically with \`find\` command?](//unix.stackexchange.com/a/262072) 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?](//unix.stackexchange.com/a/113799) – Stéphane Chazelas Dec 24 '16 at 08:25
  • Ah, I somehow missed the 1st one yesterday while skimming through the questions... The reason for asking was that (although I do know that {} should never be embedded the shell code) I figured it would be best to have a post explaining "why not" so that we could link to it when seeing answers [like this one](http://unix.stackexchange.com/a/332455). I guess the answer in your 1st link will do. Thanks ! – don_crissti Dec 24 '16 at 12:48
  • Thanks. [What does "`_`, `-`, `--` or the empty string is used for the shell's error messages" mean? Why does using `inline-sh` not work in the example, given that `inline-sh` is not `_`, `-`, `--` or the empty string?](https://unix.stackexchange.com/q/448642/674) – Tim Jun 08 '18 at 13:16