From 5d7a79f01ee0a64208cb12f636070a371bfb254b Mon Sep 17 00:00:00 2001 From: Thomas Haller Date: Fri, 9 Jul 2021 09:18:26 +0200 Subject: [PATCH] ifcfg: reject non-UTF-8 at the lowest layer when reading shell variable ifcfg files are a text format. It makes no sense to ever accept non-UTF-8 blobs. If binary data is to be encoded in a ifcfg file, then the upper layers must escape/encode it in valid UTF-8. Let svUnescape() silently reject any binary "text". This will lead to treat such strings as empty strings "". This is no different than some invalid quoting: the string is not parsable as (UTF-8) text and will be treated as such. This is potentially a breaking change. But the benefit is that all the upper layers can rely on only getting valid UTF-8 strings. For example, a non-UTF-8 string cannot be converted to a "s" GVariant (of course not, it's not a string). But our nm_connection_verify() commonly does not check that all strings are in fact valid UTF-8. So a user who edits an ifcfg file could inject non-valid strings, and cause assertion failures later on. It's actually easy to provoke a crash (or at least an assertion failure) by writing an ifcfg file with certain keys as binary. Note that you can either reproduce the binary files by writing non-UTF-8 "strings" dirctly, or by using \x, \u, or \U escape sequences. Note that also '\0' gets rejected and renders the string as invalid (i.e. as empty). Before the returned string would have been simply truncated and the rest ignored. Such NUL bytes can only be produced using the escape sequences, because the ifcfg reader already (silently) truncates the file on the first binary NUL. (cherry picked from commit 7c9b0d68e422a3ecb9b94ce91f6b83a7176fb7c0) --- src/core/settings/plugins/ifcfg-rh/shvar.c | 18 +++++++-- src/core/settings/plugins/ifcfg-rh/shvar.h | 1 + .../ifcfg-test-write-unknown-4.expected | 6 ++- .../plugins/ifcfg-rh/tests/test-ifcfg-rh.c | 38 ++++++++++++++++++- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/core/settings/plugins/ifcfg-rh/shvar.c b/src/core/settings/plugins/ifcfg-rh/shvar.c index 548a5b43e3..7b83c094a3 100644 --- a/src/core/settings/plugins/ifcfg-rh/shvar.c +++ b/src/core/settings/plugins/ifcfg-rh/shvar.c @@ -387,6 +387,12 @@ _strbuf_init(NMStrBuf *str, const char *value, gsize i) const char * svUnescape(const char *value, char **to_free) +{ + return svUnescape_full(value, to_free, TRUE); +} + +const char * +svUnescape_full(const char *value, char **to_free, gboolean check_utf8) { NMStrBuf str = NM_STR_BUF_INIT(0, FALSE); int looks_like_old_svescaped = -1; @@ -662,6 +668,8 @@ out_value: } if (str.allocated > 0) { + if (check_utf8 && !nm_str_buf_utf8_validate(&str)) + goto out_error; if (str.len == 0 || nm_str_buf_get_str_unsafe(&str)[0] == '\0') { nm_str_buf_destroy(&str); *to_free = NULL; @@ -672,6 +680,11 @@ out_value: } } + if (check_utf8 && !g_utf8_validate(value, i, NULL)) { + *to_free = NULL; + return NULL; + } + if (value[i] != '\0') { *to_free = g_strndup(value, i); return *to_free; @@ -1136,9 +1149,8 @@ _svGetValue(shvarFile *s, const char *key, char **to_free) if (line && line->line) { v = svUnescape(line->line, to_free); if (!v) { - /* a wrongly quoted value is treated like the empty string. - * See also svWriteFile(), which handles unparsable values - * that way. */ + /* a wrongly quoted value or non-UTF-8 is treated like the empty string. + * See also svWriteFile(), which handles unparsable values that way. */ nm_assert(!*to_free); return ""; } diff --git a/src/core/settings/plugins/ifcfg-rh/shvar.h b/src/core/settings/plugins/ifcfg-rh/shvar.h index 6965d87314..49101669ef 100644 --- a/src/core/settings/plugins/ifcfg-rh/shvar.h +++ b/src/core/settings/plugins/ifcfg-rh/shvar.h @@ -107,6 +107,7 @@ void svCloseFile(shvarFile *s); const char *svEscape(const char *s, char **to_free); const char *svUnescape(const char *s, char **to_free); +const char *svUnescape_full(const char *value, char **to_free, gboolean check_utf8); static inline void _nm_auto_shvar_file_close(shvarFile **p_s) diff --git a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-write-unknown-4.expected b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-write-unknown-4.expected index 040ddc9d7d..92c03b12ec 100644 --- a/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-write-unknown-4.expected +++ b/src/core/settings/plugins/ifcfg-rh/tests/network-scripts/ifcfg-test-write-unknown-4.expected @@ -13,8 +13,10 @@ #L2 METRIC1='' -METRIC2=$'\U0x' -METRIC3=$'x\U0' +METRIC2= +#NM: METRIC2=$'\U0x' +METRIC3= +#NM: METRIC3=$'x\U0' #L4 IPADDR=set-by-test1 diff --git a/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c b/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c index 6193b952b9..f0c6c05396 100644 --- a/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c +++ b/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c @@ -10476,7 +10476,7 @@ _svUnescape(const char *str, char **to_free) str = (str_free = g_strdup(str)); } - s = svUnescape(str, to_free); + s = svUnescape_full(str, to_free, FALSE); if (*to_free) { g_assert(s == *to_free); g_assert(s[0]); @@ -10484,6 +10484,37 @@ _svUnescape(const char *str, char **to_free) g_assert(s == NULL || (!s[0] && (s < str || s > strchr(str, '\0'))) || (s[0] && s >= str && s <= strchr(str, '\0'))); } + + { + const char * s2; + gs_free char *to_free2 = NULL; + + gboolean is_utf8 = s && g_utf8_validate(s, -1, NULL); + + s2 = svUnescape_full(str, &to_free2, TRUE); + if (NM_IN_STRSET(str, "$'\\U0x'", "$'\\x0'", "$'\\008'", "$'\\08'")) { + g_assert_cmpstr(s2, ==, NULL); + g_assert(!to_free2); + g_assert_cmpstr(s, ==, ""); + g_assert(!*to_free); + } else if (NM_IN_STRSET(str, "$'x\\U0'")) { + g_assert_cmpstr(s2, ==, NULL); + g_assert(!to_free2); + g_assert_cmpstr(s, ==, "x"); + g_assert(*to_free == s); + } else if (!is_utf8) { + g_assert(!s2); + g_assert(!to_free2); + } else if (!to_free2) { + g_assert_cmpstr(s, ==, s2); + g_assert(s == s2); + } else { + g_assert_cmpstr(s, ==, s2); + g_assert(s != s2); + g_assert(s2 == to_free2); + } + } + return s; } @@ -10665,6 +10696,9 @@ test_svUnescape(void) V1("\"\\'\"''", "\\'"), V0("\"b\\~b\" ", "b\\~b"), V1("\"b\\~b\"x", "b\\~bx"), + + V0("$'x\\U0'", "x"), + V0("$'\\U0x'", ""), }; const UnescapeTestData data_ansi[] = { /* strings inside $''. They cannot be compared directly, but must @@ -10851,7 +10885,7 @@ test_write_unknown(gconstpointer test_data) _svGetValue_check(sv, "METRIC", NULL); _svGetValue_check(sv, "METRIC1", ""); _svGetValue_check(sv, "METRIC2", ""); - _svGetValue_check(sv, "METRIC3", "x"); + _svGetValue_check(sv, "METRIC3", ""); _svGetValue_check(sv, "IPADDR", "set-by-test1"); _svGetValue_check(sv, "IPADDR2", "set-by-test2");