]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Use "dollar-single-quotes" to escape shell-sensitive strings 6113/head
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Sun, 11 Jun 2017 19:24:07 +0000 (15:24 -0400)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Mon, 19 Jun 2017 23:39:43 +0000 (19:39 -0400)
Also called "ANSI-C Quoting" in info:(bash) ANSI-C Quoting.

The escaping rules are a POSIX proposal, and are described in
http://austingroupbugs.net/view.php?id=249. There's a lot of back-and-forth on
the details of escaping of control characters, but we'll be only using a small
subset of the syntax that is common to all proposals and is widely supported.
Unfortunately dash and fish and maybe some other shells do not support it (see
the man page patch for a list).

This allows environment variables to be safely exported using show-environment
and imported into the shell. Shells which do not support this syntax will have
to do something like
    export $(systemctl show-environment|grep -v '=\$')
or whatever is appropriate in their case. I think csh and fish do not support
the A=B syntax anyway, so the change is moot for them.

Fixes #5536.

v2:
- also escape newlines (which currently disallowed in shell values, so this
  doesn't really matter), and tabs (as $'\t'), and ! (as $'!'). This way quoted
  output can be included directly in both interactive and noninteractive bash.

man/systemctl.xml
src/basic/escape.c
src/basic/escape.h
src/core/job.c
src/environment-d-generator/environment-d-generator.c
src/shared/bus-unit-util.c
src/systemctl/systemctl.c
src/test/test-escape.c

index d4afb36f8591a859e55be84a95146520da0c77a7..14405141cfcd523370577dfd3cec9429e74e6629 100644 (file)
@@ -1495,11 +1495,26 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err
           <term><command>show-environment</command></term>
 
           <listitem>
-            <para>Dump the systemd manager environment block. The
-            environment block will be dumped in straight-forward form
-            suitable for sourcing into a shell script. This environment
-            block will be passed to all processes the manager
-            spawns.</para>
+            <para>Dump the systemd manager environment block. This is the environment
+            block that is passed to all processes the manager spawns. The environment
+            block will be dumped in straight-forward form suitable for sourcing into
+            most shells. If no special characters or whitespace is present in the variable
+            values, no escaping is performed, and the assignments have the form
+            <literal>VARIABLE=value</literal>. If whitespace or characters which have
+            special meaning to the shell are present, dollar-single-quote escaping is
+            used, and assignments have the form <literal>VARIABLE=$'value'</literal>.
+            This syntax is known to be supported by
+            <citerefentry project='die-net'><refentrytitle>bash</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+            <citerefentry project='die-net'><refentrytitle>zsh</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+            <citerefentry project='die-net'><refentrytitle>ksh</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+            and
+            <citerefentry project='die-net'><refentrytitle>busybox</refentrytitle><manvolnum>1</manvolnum></citerefentry>'s
+            <citerefentry project='die-net'><refentrytitle>ash</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+            but not
+            <citerefentry project='die-net'><refentrytitle>dash</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+            or
+            <citerefentry project='die-net'><refentrytitle>fish</refentrytitle><manvolnum>1</manvolnum></citerefentry>.
+            </para>
           </listitem>
         </varlistentry>
         <varlistentry>
index 4a1ec4505ed3b12d508327be60905247570b2e57..85e4b5282ed758a914ad0109176f3a6601ee8815 100644 (file)
@@ -441,10 +441,16 @@ char *octescape(const char *s, size_t len) {
 
 }
 
-static char *strcpy_backslash_escaped(char *t, const char *s, const char *bad) {
+static char *strcpy_backslash_escaped(char *t, const char *s, const char *bad, bool escape_tab_nl) {
         assert(bad);
 
         for (; *s; s++) {
+                if (escape_tab_nl && IN_SET(*s, '\n', '\t')) {
+                        *(t++) = '\\';
+                        *(t++) = *s == '\n' ? 'n' : 't';
+                        continue;
+                }
+
                 if (*s == '\\' || strchr(bad, *s))
                         *(t++) = '\\';
 
@@ -461,20 +467,21 @@ char *shell_escape(const char *s, const char *bad) {
         if (!r)
                 return NULL;
 
-        t = strcpy_backslash_escaped(r, s, bad);
+        t = strcpy_backslash_escaped(r, s, bad, false);
         *t = 0;
 
         return r;
 }
 
-char *shell_maybe_quote(const char *s) {
+char* shell_maybe_quote(const char *s, EscapeStyle style) {
         const char *p;
         char *r, *t;
 
         assert(s);
 
-        /* Encloses a string in double quotes if necessary to make it
-         * OK as shell string. */
+        /* Encloses a string in quotes if necessary to make it OK as a shell
+         * string. Note that we treat benign UTF-8 characters as needing
+         * escaping too, but that should be OK. */
 
         for (p = s; *p; p++)
                 if (*p <= ' ' ||
@@ -485,17 +492,30 @@ char *shell_maybe_quote(const char *s) {
         if (!*p)
                 return strdup(s);
 
-        r = new(char, 1+strlen(s)*2+1+1);
+        r = new(char, (style == ESCAPE_POSIX) + 1 + strlen(s)*2 + 1 + 1);
         if (!r)
                 return NULL;
 
         t = r;
-        *(t++) = '"';
+        if (style == ESCAPE_BACKSLASH)
+                *(t++) = '"';
+        else if (style == ESCAPE_POSIX) {
+                *(t++) = '$';
+                *(t++) = '\'';
+        } else
+                assert_not_reached("Bad EscapeStyle");
+
         t = mempcpy(t, s, p - s);
 
-        t = strcpy_backslash_escaped(t, p, SHELL_NEED_ESCAPE);
+        if (style == ESCAPE_BACKSLASH)
+                t = strcpy_backslash_escaped(t, p, SHELL_NEED_ESCAPE, false);
+        else
+                t = strcpy_backslash_escaped(t, p, SHELL_NEED_ESCAPE_POSIX, true);
 
-        *(t++)= '"';
+        if (style == ESCAPE_BACKSLASH)
+                *(t++) = '"';
+        else
+                *(t++) = '\'';
         *t = 0;
 
         return r;
index deaa4def2828e27776b95a4062b92238e4a4d269..6f5cc60bc80d7bc58538d4058775028451c061a2 100644 (file)
 /* What characters are special in the shell? */
 /* must be escaped outside and inside double-quotes */
 #define SHELL_NEED_ESCAPE "\"\\`$"
-/* can be escaped or double-quoted */
-#define SHELL_NEED_QUOTES SHELL_NEED_ESCAPE GLOB_CHARS "'()<>|&;"
+
+/* Those that can be escaped or double-quoted.
+ *
+ * Stricly speaking, ! does not need to be escaped, except in interactive
+ * mode, but let's be extra nice to the user and quote ! in case this
+ * output is ever used in interactive mode. */
+#define SHELL_NEED_QUOTES SHELL_NEED_ESCAPE GLOB_CHARS "'()<>|&;!"
+
+/* Note that we assume control characters would need to be escaped too in
+ * addition to the "special" characters listed here, if they appear in the
+ * string. Current users disallow control characters. Also '"' shall not
+ * be escaped.
+ */
+#define SHELL_NEED_ESCAPE_POSIX "\\\'"
 
 typedef enum UnescapeFlags {
         UNESCAPE_RELAX = 1,
 } UnescapeFlags;
 
+typedef enum EscapeStyle {
+        ESCAPE_BACKSLASH = 1,
+        ESCAPE_POSIX = 2,
+} EscapeStyle;
+
 char *cescape(const char *s);
 char *cescape_length(const char *s, size_t n);
 size_t cescape_char(char c, char *buf);
@@ -51,4 +68,4 @@ char *xescape(const char *s, const char *bad);
 char *octescape(const char *s, size_t len);
 
 char *shell_escape(const char *s, const char *bad);
-char *shell_maybe_quote(const char *s);
+char* shell_maybe_quote(const char *s, EscapeStyle style);
index 5067006d631dcbc89d4a84ab043f3a1b3d44e9a5..257c1d425275f4558ba4b4ad1cff0fc005376896 100644 (file)
@@ -741,7 +741,7 @@ static void job_print_status_message(Unit *u, JobType t, JobResult result) {
         if (t == JOB_START && result == JOB_FAILED) {
                 _cleanup_free_ char *quoted;
 
-                quoted = shell_maybe_quote(u->id);
+                quoted = shell_maybe_quote(u->id, ESCAPE_BACKSLASH);
                 manager_status_printf(u->manager, STATUS_TYPE_NORMAL, NULL, "See 'systemctl status %s' for details.", strna(quoted));
         }
 }
index 2d4c4235e4bb7c213f78d87bd370d63a71b6d5a8..9c7250237340a3814e4e3db23edeef6a19559f85 100644 (file)
@@ -78,7 +78,7 @@ static int load_and_print(void) {
                 t = strchr(*i, '=');
                 assert(t);
 
-                q = shell_maybe_quote(t + 1);
+                q = shell_maybe_quote(t + 1, ESCAPE_BACKSLASH);
                 if (!q)
                         return log_oom();
 
index aae69f6da5f4079e38dab09f0b9e8466fccbb4a9..7b0bc8255c366ce4b0443aa3d93ddabb475248b0 100644 (file)
@@ -860,7 +860,7 @@ static void log_job_error_with_service_result(const char* service, const char *r
 
         assert(service);
 
-        service_shell_quoted = shell_maybe_quote(service);
+        service_shell_quoted = shell_maybe_quote(service, ESCAPE_BACKSLASH);
 
         if (extra_args) {
                 _cleanup_free_ char *t;
index 2f69e863c5e8ae0dee8935b0f4e47f47da6aaf97..6699ea3e4c6e4169421e064a60b7df4adf85fabb 100644 (file)
@@ -47,6 +47,7 @@
 #include "dropin.h"
 #include "efivars.h"
 #include "env-util.h"
+#include "escape.h"
 #include "exit-status.h"
 #include "fd-util.h"
 #include "fileio.h"
@@ -5573,6 +5574,24 @@ static int reset_failed(int argc, char *argv[], void *userdata) {
         return r;
 }
 
+static int print_variable(const char *s) {
+        const char *sep;
+        _cleanup_free_ char *esc = NULL;
+
+        sep = strchr(s, '=');
+        if (!sep) {
+                log_error("Invalid environment block");
+                return -EUCLEAN;
+        }
+
+        esc = shell_maybe_quote(sep + 1, ESCAPE_POSIX);
+        if (!esc)
+                return log_oom();
+
+        printf("%.*s=%s\n", (int)(sep-s), s, esc);
+        return 0;
+}
+
 static int show_environment(int argc, char *argv[], void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
@@ -5602,8 +5621,11 @@ static int show_environment(int argc, char *argv[], void *userdata) {
         if (r < 0)
                 return bus_log_parse_error(r);
 
-        while ((r = sd_bus_message_read_basic(reply, SD_BUS_TYPE_STRING, &text)) > 0)
-                puts(text);
+        while ((r = sd_bus_message_read_basic(reply, SD_BUS_TYPE_STRING, &text)) > 0) {
+                r = print_variable(text);
+                if (r < 0)
+                        return r;
+        }
         if (r < 0)
                 return bus_log_parse_error(r);
 
index 6cbb8443fe2c682957a5dd98af643f34ffaf780d..181d7db5b4ca3d2010e0dc920bada0a7317ba7fc 100644 (file)
@@ -86,25 +86,52 @@ static void test_shell_escape(void) {
         test_shell_escape_one("foo:bar,baz", ",:", "foo\\:bar\\,baz");
 }
 
-static void test_shell_maybe_quote_one(const char *s, const char *expected) {
-        _cleanup_free_ char *r;
-
-        assert_se(r = shell_maybe_quote(s));
-        assert_se(streq(r, expected));
+static void test_shell_maybe_quote_one(const char *s,
+                                       EscapeStyle style,
+                                       const char *expected) {
+        _cleanup_free_ char *ret = NULL;
+
+        assert_se(ret = shell_maybe_quote(s, style));
+        log_debug("[%s] → [%s] (%s)", s, ret, expected);
+        assert_se(streq(ret, expected));
 }
 
 static void test_shell_maybe_quote(void) {
 
-        test_shell_maybe_quote_one("", "");
-        test_shell_maybe_quote_one("\\", "\"\\\\\"");
-        test_shell_maybe_quote_one("\"", "\"\\\"\"");
-        test_shell_maybe_quote_one("foobar", "foobar");
-        test_shell_maybe_quote_one("foo bar", "\"foo bar\"");
-        test_shell_maybe_quote_one("foo \"bar\" waldo", "\"foo \\\"bar\\\" waldo\"");
-        test_shell_maybe_quote_one("foo$bar", "\"foo\\$bar\"");
+        test_shell_maybe_quote_one("", ESCAPE_BACKSLASH, "");
+        test_shell_maybe_quote_one("", ESCAPE_POSIX, "");
+        test_shell_maybe_quote_one("\\", ESCAPE_BACKSLASH, "\"\\\\\"");
+        test_shell_maybe_quote_one("\\", ESCAPE_POSIX, "$'\\\\'");
+        test_shell_maybe_quote_one("\"", ESCAPE_BACKSLASH, "\"\\\"\"");
+        test_shell_maybe_quote_one("\"", ESCAPE_POSIX, "$'\"'");
+        test_shell_maybe_quote_one("foobar", ESCAPE_BACKSLASH, "foobar");
+        test_shell_maybe_quote_one("foobar", ESCAPE_POSIX, "foobar");
+        test_shell_maybe_quote_one("foo bar", ESCAPE_BACKSLASH, "\"foo bar\"");
+        test_shell_maybe_quote_one("foo bar", ESCAPE_POSIX, "$'foo bar'");
+        test_shell_maybe_quote_one("foo\tbar", ESCAPE_BACKSLASH, "\"foo\tbar\"");
+        test_shell_maybe_quote_one("foo\tbar", ESCAPE_POSIX, "$'foo\\tbar'");
+        test_shell_maybe_quote_one("foo\nbar", ESCAPE_BACKSLASH, "\"foo\nbar\"");
+        test_shell_maybe_quote_one("foo\nbar", ESCAPE_POSIX, "$'foo\\nbar'");
+        test_shell_maybe_quote_one("foo \"bar\" waldo", ESCAPE_BACKSLASH, "\"foo \\\"bar\\\" waldo\"");
+        test_shell_maybe_quote_one("foo \"bar\" waldo", ESCAPE_POSIX, "$'foo \"bar\" waldo'");
+        test_shell_maybe_quote_one("foo$bar", ESCAPE_BACKSLASH, "\"foo\\$bar\"");
+        test_shell_maybe_quote_one("foo$bar", ESCAPE_POSIX, "$'foo$bar'");
+
+        /* Note that current users disallow control characters, so this "test"
+         * is here merely to establish current behaviour. If control characters
+         * were allowed, they should be quoted, i.e. \001 should become \\001. */
+        test_shell_maybe_quote_one("a\nb\001", ESCAPE_BACKSLASH, "\"a\nb\001\"");
+        test_shell_maybe_quote_one("a\nb\001", ESCAPE_POSIX, "$'a\\nb\001'");
+
+        test_shell_maybe_quote_one("foo!bar", ESCAPE_BACKSLASH, "\"foo!bar\"");
+        test_shell_maybe_quote_one("foo!bar", ESCAPE_POSIX, "$'foo!bar'");
 }
 
 int main(int argc, char *argv[]) {
+        log_set_max_level(LOG_DEBUG);
+        log_parse_environment();
+        log_open();
+
         test_cescape();
         test_cunescape();
         test_shell_escape();