about summary refs log tree commit diff
path: root/lib/strings.nix
diff options
context:
space:
mode:
authoraszlig <aszlig@redmoonstudios.org>2016-06-20 18:31:49 +0200
committeraszlig <aszlig@redmoonstudios.org>2016-06-20 23:53:36 +0200
commitdf475092e92b9dab9642c48f2216d49027a457a1 (patch)
treecf45731f0ab5c88c57bd225c6c436430c3e3eb0a /lib/strings.nix
parent99c6c9d42f5441354de5cf5f9e40b96f4b99f1bc (diff)
downloadnixlib-df475092e92b9dab9642c48f2216d49027a457a1.tar
nixlib-df475092e92b9dab9642c48f2216d49027a457a1.tar.gz
nixlib-df475092e92b9dab9642c48f2216d49027a457a1.tar.bz2
nixlib-df475092e92b9dab9642c48f2216d49027a457a1.tar.lz
nixlib-df475092e92b9dab9642c48f2216d49027a457a1.tar.xz
nixlib-df475092e92b9dab9642c48f2216d49027a457a1.tar.zst
nixlib-df475092e92b9dab9642c48f2216d49027a457a1.zip
lib: Make escapeShellArg more robust
Quoting various characters that the shell *may* interpret specially is a
very fragile thing to do.

I've used something more robust all over the place in various Nix
expression I've written just because I didn't trust escapeShellArg.

Here is a proof of concept showing that I was indeed right in
distrusting escapeShellArg:

with import <nixpkgs> {};

let
  payload = runCommand "payload" {} ''
    # \x00 is not allowed for Nix strings, so let's begin at 1
    for i in $(seq 1 255); do
      echo -en "\\x$(printf %02x $i)"
    done > "$out"
  '';

  escapers = with lib; {
    current = escapeShellArg;
    better = arg: let
      backslashEscapes = stringToCharacters "\"\\ ';$`()|<>\r\t*[]&!~#";
      search = backslashEscapes ++ [ "\n" ];
      replace = map (c: "\\${c}") backslashEscapes ++ [ "'\n'" ];
    in replaceStrings search replace (toString arg);
    best = arg: "'${replaceStrings ["'"] ["'\\''"] (toString arg)}'";
  };

  testWith = escaper: let
    escaped = escaper (builtins.readFile payload);
  in runCommand "test" {} ''
    if ! r="$(bash -c ${escapers.best "echo -nE ${escaped}"} 2> /dev/null)"
    then
      echo bash eval error > "$out"
      exit 0
    fi
    if echo -n "$r" | cmp -s "${payload}"; then
      echo success > "$out"
    else
      echo failed > "$out"
    fi
  '';

in runCommand "results" {} ''
  echo "Test results:"
  ${lib.concatStrings (lib.mapAttrsToList (name: impl: ''
    echo "  ${name}: $(< "${testWith impl}")"
  '') escapers)}
  exit 1
''

The resulting output is the following:

Test results:
  best: success
  better: success
  current: bash eval error

I did the "better" implementation just to illustrate that the method of
quoting only "harmful" characters results in madness in terms of
implementation and performance.

Signed-off-by: aszlig <aszlig@redmoonstudios.org>
Cc: @edolstra, @zimbatm
Diffstat (limited to 'lib/strings.nix')
-rw-r--r--lib/strings.nix15
1 files changed, 7 insertions, 8 deletions
diff --git a/lib/strings.nix b/lib/strings.nix
index 04376a5f2fbe..5e5f7b378667 100644
--- a/lib/strings.nix
+++ b/lib/strings.nix
@@ -203,20 +203,19 @@ rec {
   */
   escape = list: replaceChars list (map (c: "\\${c}") list);
 
-  /* Escape all characters that have special meaning in the Bourne shell.
+  /* Quote string to be used safely within the Bourne shell.
 
      Example:
-       escapeShellArg "so([<>])me"
-       => "so\\(\\[\\<\\>\\]\\)me"
+       escapeShellArg "esc'ape\nme"
+       => "'esc'\\''ape\nme'"
   */
-  escapeShellArg = arg:
-    lib.escape (stringToCharacters "\\ ';$`()|<>\t*[]") (toString arg);
+  escapeShellArg = arg: "'${replaceStrings ["'"] ["'\\''"] (toString arg)}'";
 
-  /* Escape all arguments to be passed to the Bourne shell.
+  /* Quote all arguments to be safely passed to the Bourne shell.
 
      Example:
-       escapeShellArgs ["one" "two three"]
-       => "one two\\ three"
+       escapeShellArgs ["one" "two three" "four'five"]
+       => "'one' 'two three' 'four'\\''five'"
   */
   escapeShellArgs = concatMapStringsSep " " escapeShellArg;