From 45b3cbf1206f16798b5cda377038f1de9372ae0b Mon Sep 17 00:00:00 2001 From: Armin Novak Date: Wed, 9 Jul 2025 09:42:12 +0200 Subject: [PATCH 1/2] [winpr,utils] Add escaped quote support In CommandLineParseCommaSeparatedValues[Ex] now escaped characters are respected, so a single quote in a string does not break parsing. --- winpr/libwinpr/utils/cmdline.c | 48 +++++++++++++++---- winpr/libwinpr/utils/test/TestCmdLine.c | 61 +++++++++++++++---------- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/winpr/libwinpr/utils/cmdline.c b/winpr/libwinpr/utils/cmdline.c index 46d954f80..8b83a8d52 100644 --- a/winpr/libwinpr/utils/cmdline.c +++ b/winpr/libwinpr/utils/cmdline.c @@ -495,6 +495,7 @@ static size_t get_element_count(const char* list, BOOL* failed, BOOL fullquoted) { size_t count = 0; int quoted = 0; + bool escaped = false; BOOL finished = FALSE; BOOL first = TRUE; const char* it = list; @@ -507,7 +508,17 @@ static size_t get_element_count(const char* list, BOOL* failed, BOOL fullquoted) while (!finished) { BOOL nextFirst = FALSE; - switch (*it) + + const char cur = *it++; + + /* Ignore the symbol that was escaped. */ + if (escaped) + { + escaped = false; + continue; + } + + switch (cur) { case '\0': if (quoted != 0) @@ -518,11 +529,18 @@ static size_t get_element_count(const char* list, BOOL* failed, BOOL fullquoted) } finished = TRUE; break; + case '\\': + if (!escaped) + { + escaped = true; + continue; + } + break; case '\'': case '"': if (!fullquoted) { - int now = is_quoted(*it); + int now = is_quoted(cur) && !escaped; if (now == quoted) quoted = 0; else if (quoted == 0) @@ -547,7 +565,6 @@ static size_t get_element_count(const char* list, BOOL* failed, BOOL fullquoted) } first = nextFirst; - it++; } return count + 1; } @@ -556,24 +573,40 @@ static char* get_next_comma(char* string, BOOL fullquoted) { const char* log = string; int quoted = 0; - BOOL first = TRUE; + bool first = true; + bool escaped = false; WINPR_ASSERT(string); while (TRUE) { - switch (*string) + char* last = string; + const char cur = *string++; + if (escaped) + { + escaped = false; + continue; + } + + switch (cur) { case '\0': if (quoted != 0) WLog_ERR(TAG, "Invalid quoted argument '%s'", log); return NULL; + case '\\': + if (!escaped) + { + escaped = true; + continue; + } + break; case '\'': case '"': if (!fullquoted) { - int now = is_quoted(*string); + int now = is_quoted(cur); if ((quoted == 0) && !first) { WLog_ERR(TAG, "Invalid quoted argument '%s'", log); @@ -593,14 +626,13 @@ static char* get_next_comma(char* string, BOOL fullquoted) return NULL; } if (quoted == 0) - return string; + return last; break; default: break; } first = FALSE; - string++; } return NULL; diff --git a/winpr/libwinpr/utils/test/TestCmdLine.c b/winpr/libwinpr/utils/test/TestCmdLine.c index 9be3c212f..445205f93 100644 --- a/winpr/libwinpr/utils/test/TestCmdLine.c +++ b/winpr/libwinpr/utils/test/TestCmdLine.c @@ -21,8 +21,19 @@ static const char* testArgv[] = { "mstsc.exe", static const char testListAppName[] = "test app name"; static const char* testListArgs[] = { - "a,b,c,d", "a:,\"b:xxx, yyy\",c", "a:,,,b", "a:,\",b", "\"a,b,c,d d d,fff\"", "", - NULL, "'a,b,\",c'", "\"a,b,',c\"", "', a, ', b,c'", "\"a,b,\",c\"" + "g:some.gateway.server,u:some\\\"user,p:some\\\"password,d:some\\\"domain,type:auto", + "a,b,c,d", + "a:,\"b:xxx, yyy\",c", + "a:,,,b", + "a:,\",b", + "\"a,b,c,d d d,fff\"", + "", + NULL, + "'a,b,\",c'", + "\"a,b,',c\"", + "', a, ', b,c'", + "\"a,b,\",c\"", + }; static const char* testListArgs1[] = { testListAppName, "a", "b", "c", "d" }; @@ -36,30 +47,32 @@ static const char* testListArgs8[] = { testListAppName, "a", "b", "\"", "c" }; static const char* testListArgs9[] = { testListAppName, "a", "b", "'", "c" }; // static const char* testListArgs10[] = {}; // static const char* testListArgs11[] = {}; +static const char* testListArgs12[] = { testListAppName, "g:some.gateway.server", + "u:some\\\"user", "p:some\\\"password", + "d:some\\\"domain", "type:auto" }; -static const char** testListArgsResult[] = { testListArgs1, - testListArgs2, - NULL /* testListArgs3 */, - NULL /* testListArgs4 */, - testListArgs5, - testListArgs6, - testListArgs7, - testListArgs8, - testListArgs9, - NULL /* testListArgs10 */, - NULL /* testListArgs11 */ }; +static const char** testListArgsResult[] = { + testListArgs12, + testListArgs1, + testListArgs2, + NULL /* testListArgs3 */, + NULL /* testListArgs4 */, + testListArgs5, + testListArgs6, + testListArgs7, + testListArgs8, + testListArgs9, + NULL /* testListArgs10 */, + NULL /* testListArgs11 */ +}; static const size_t testListArgsCount[] = { - ARRAYSIZE(testListArgs1), - ARRAYSIZE(testListArgs2), - 0 /* ARRAYSIZE(testListArgs3) */, - 0 /* ARRAYSIZE(testListArgs4) */, - ARRAYSIZE(testListArgs5), - ARRAYSIZE(testListArgs6), - ARRAYSIZE(testListArgs7), - ARRAYSIZE(testListArgs8), - ARRAYSIZE(testListArgs9), - 0 /* ARRAYSIZE(testListArgs10) */, - 0 /* ARRAYSIZE(testListArgs11) */ + ARRAYSIZE(testListArgs12), ARRAYSIZE(testListArgs1), + ARRAYSIZE(testListArgs2), 0 /* ARRAYSIZE(testListArgs3) */, + 0 /* ARRAYSIZE(testListArgs4) */, ARRAYSIZE(testListArgs5), + ARRAYSIZE(testListArgs6), ARRAYSIZE(testListArgs7), + ARRAYSIZE(testListArgs8), ARRAYSIZE(testListArgs9), + 0 /* ARRAYSIZE(testListArgs10) */, 0 /* ARRAYSIZE(testListArgs11) */ + }; static BOOL checkResult(size_t index, char** actual, size_t actualCount) From 2f617c9e39bf23431d773726b5fb81b9c4c9bf57 Mon Sep 17 00:00:00 2001 From: Armin Novak Date: Wed, 9 Jul 2025 09:55:14 +0200 Subject: [PATCH 2/2] [client,common] Unescape /gateway option values To support escaped characters with /gateway all values are now unuescaped before use. --- client/common/cmdline.c | 43 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/client/common/cmdline.c b/client/common/cmdline.c index 84b66e905..ed1ac6ebe 100644 --- a/client/common/cmdline.c +++ b/client/common/cmdline.c @@ -4087,8 +4087,43 @@ static BOOL parse_gateway_usage_option(rdpSettings* settings, const char* value) return freerdp_set_gateway_usage_method(settings, type); } +static char* unescape(const char* str) +{ + char* copy = strdup(str); + if (!copy) + return NULL; + + bool escaped = false; + char* dst = copy; + while (*str != '\0') + { + char cur = *str++; + + switch (cur) + { + case '\\': + if (!escaped) + { + escaped = true; + continue; + } + // fallthrough + WINPR_FALLTHROUGH + default: + *dst++ = cur; + escaped = false; + break; + } + } + + *dst = '\0'; + + return copy; +} + static BOOL parse_gateway_options(rdpSettings* settings, const COMMAND_LINE_ARGUMENT_A* arg) { + char* argval = NULL; BOOL rc = FALSE; WINPR_ASSERT(settings); @@ -4107,9 +4142,10 @@ static BOOL parse_gateway_options(rdpSettings* settings, const COMMAND_LINE_ARGU for (size_t x = 0; x < count; x++) { BOOL validOption = FALSE; - const char* argval = ptr[x]; - - WINPR_ASSERT(argval); + free(argval); + argval = unescape(ptr[x]); + if (!argval) + goto fail; const char* gw = option_starts_with("g:", argval); if (gw) @@ -4216,6 +4252,7 @@ static BOOL parse_gateway_options(rdpSettings* settings, const COMMAND_LINE_ARGU rc = TRUE; fail: + free(argval); CommandLineParserFree(ptr); return rc; }