From 804ee07c1370d49aa9a555c91c26fe3ed22f9957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sun, 11 Jun 2017 15:24:07 -0400 Subject: [PATCH] Use "dollar-single-quotes" to escape shell-sensitive strings 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 | 25 ++++++++-- src/basic/escape.c | 38 ++++++++++---- src/basic/escape.h | 23 +++++++-- src/core/job.c | 2 +- .../environment-d-generator.c | 2 +- src/shared/bus-unit-util.c | 2 +- src/systemctl/systemctl.c | 26 +++++++++- src/test/test-escape.c | 49 ++++++++++++++----- 8 files changed, 134 insertions(+), 33 deletions(-) diff --git a/man/systemctl.xml b/man/systemctl.xml index d4afb36f85..14405141cf 100644 --- a/man/systemctl.xml +++ b/man/systemctl.xml @@ -1495,11 +1495,26 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err show-environment - 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. + 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 + VARIABLE=value. If whitespace or characters which have + special meaning to the shell are present, dollar-single-quote escaping is + used, and assignments have the form VARIABLE=$'value'. + This syntax is known to be supported by + bash1, + zsh1, + ksh1, + and + busybox1's + ash1, + but not + dash1 + or + fish1. + diff --git a/src/basic/escape.c b/src/basic/escape.c index 4a1ec4505e..85e4b5282e 100644 --- a/src/basic/escape.c +++ b/src/basic/escape.c @@ -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; diff --git a/src/basic/escape.h b/src/basic/escape.h index deaa4def28..6f5cc60bc8 100644 --- a/src/basic/escape.h +++ b/src/basic/escape.h @@ -31,13 +31,30 @@ /* 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); diff --git a/src/core/job.c b/src/core/job.c index 5067006d63..257c1d4252 100644 --- a/src/core/job.c +++ b/src/core/job.c @@ -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)); } } diff --git a/src/environment-d-generator/environment-d-generator.c b/src/environment-d-generator/environment-d-generator.c index 2d4c4235e4..9c72502373 100644 --- a/src/environment-d-generator/environment-d-generator.c +++ b/src/environment-d-generator/environment-d-generator.c @@ -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(); diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index aae69f6da5..7b0bc8255c 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -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; diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c index 2f69e863c5..6699ea3e4c 100644 --- a/src/systemctl/systemctl.c +++ b/src/systemctl/systemctl.c @@ -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); diff --git a/src/test/test-escape.c b/src/test/test-escape.c index 6cbb8443fe..181d7db5b4 100644 --- a/src/test/test-escape.c +++ b/src/test/test-escape.c @@ -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; +static void test_shell_maybe_quote_one(const char *s, + EscapeStyle style, + const char *expected) { + _cleanup_free_ char *ret = NULL; - assert_se(r = shell_maybe_quote(s)); - assert_se(streq(r, expected)); + 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();