[PATCH v1] win32: unit-test the CreateProcess() command line quoting

Gert Doering <[email protected]>
Newsgroups gmane.network.openvpn.devel
Message-ID <[email protected]>
From: Lev Stipakov <[email protected]>

Cover which argument shapes wide_cmd_line() quotes: plain arguments and
the batch delimiters , ; = stay bare, a space or a cmd.exe metacharacter
forces quotes, and an embedded double quote is replaced so quoting cannot
be broken out of. The first two cases pin the compatibility guarantee -
they are what a later "just quote everything" simplification would break.

Move wide_cmd_line() to win32-util.c next to wide_string(), which it
calls. The unit tests already link that file, so nothing else needs to
change to reach it.

Change-Id: Iba66235cbad52692da542abd47e9f51810b0ed66
Signed-off-by: Lev Stipakov <[email protected]>
Acked-by: Heiko Hund <[email protected]>
Gerrit URL: https://gerrit.openvpn.net/c/openvpn/+/1892
---

This change was reviewed on Gerrit and approved by at least one
developer. I request to merge it to master.

Gerrit URL: https://gerrit.openvpn.net/c/openvpn/+/1892
This mail reflects revision 1 of this Change.

Acked-by according to Gerrit (reflected above):
Heiko Hund <[email protected]>

        
diff --git a/src/openvpn/win32-util.c b/src/openvpn/win32-util.c
index 209e34e..4e3819c 100644
--- a/src/openvpn/win32-util.c
+++ b/src/openvpn/win32-util.c
@@ -45,6 +45,73 @@
     return ucs16;
 }
 
+/* special to cmd.exe, which CreateProcess() uses to run .bat/.cmd (VU#123335) */
+#define CMD_QUOTE_TRIGGERS " &|<>^%()!"
+
+static bool
+argv_element_needs_quotes(const char *str)
+{
+    for (const char *c = str; *c != '\0'; ++c)
+    {
+        if (strchr(CMD_QUOTE_TRIGGERS, *c) != NULL)
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+WCHAR *
+wide_cmd_line(const struct argv *a, struct gc_arena *gc)
+{
+    size_t nchars = 1;
+    size_t maxlen = 0;
+    size_t i;
+    struct buffer buf;
+    char *work = NULL;
+
+    if (!a)
+    {
+        return NULL;
+    }
+
+    for (i = 0; i < a->argc; ++i)
+    {
+        const char *arg = a->argv[i];
+        const size_t len = strlen(arg);
+        nchars += len + 3;
+        if (len > maxlen)
+        {
+            maxlen = len;
+        }
+    }
+
+    work = gc_malloc(maxlen + 1, false, gc);
+    check_malloc_return(work);
+    buf = alloc_buf_gc(nchars, gc);
+
+    for (i = 0; i < a->argc; ++i)
+    {
+        const char *arg = a->argv[i];
+        strcpy(work, arg);
+        string_mod(work, CC_PRINT, CC_DOUBLE_QUOTE | CC_CRLF, '_');
+        if (i)
+        {
+            buf_printf(&buf, " ");
+        }
+        if (argv_element_needs_quotes(work))
+        {
+            buf_printf(&buf, "\"%s\"", work);
+        }
+        else
+        {
+            buf_printf(&buf, "%s", work);
+        }
+    }
+
+    return wide_string(BSTR(&buf), gc);
+}
+
 char *
 utf16to8(const wchar_t *utf16, struct gc_arena *gc)
 {
diff --git a/src/openvpn/win32-util.h b/src/openvpn/win32-util.h
index fb83762..c2ad64b 100644
--- a/src/openvpn/win32-util.h
+++ b/src/openvpn/win32-util.h
@@ -24,11 +24,15 @@
 #ifndef OPENVPN_WIN32_UTIL_H
 #define OPENVPN_WIN32_UTIL_H
 
+#include "argv.h"
 #include "buffer.h"
 
 /* Convert a string from UTF-8 to UCS-2 */
 WCHAR *wide_string(const char *utf8, struct gc_arena *gc);
 
+/* Build a CreateProcess() command line from argv */
+WCHAR *wide_cmd_line(const struct argv *a, struct gc_arena *gc);
+
 /* Convert a string from UTF-16 to UTF-8 */
 char *utf16to8(const wchar_t *utf16, struct gc_arena *gc);
 
diff --git a/src/openvpn/win32.c b/src/openvpn/win32.c
index d527160..80ec7e8 100644
--- a/src/openvpn/win32.c
+++ b/src/openvpn/win32.c
@@ -936,73 +936,6 @@
     }
 }
 
-/* special to cmd.exe, which CreateProcess() uses to run .bat/.cmd (VU#123335) */
-#define CMD_QUOTE_TRIGGERS " &|<>^%()!"
-
-static bool
-argv_element_needs_quotes(const char *str)
-{
-    for (const char *c = str; *c != '\0'; ++c)
-    {
-        if (strchr(CMD_QUOTE_TRIGGERS, *c) != NULL)
-        {
-            return true;
-        }
-    }
-    return false;
-}
-
-static WCHAR *
-wide_cmd_line(const struct argv *a, struct gc_arena *gc)
-{
-    size_t nchars = 1;
-    size_t maxlen = 0;
-    size_t i;
-    struct buffer buf;
-    char *work = NULL;
-
-    if (!a)
-    {
-        return NULL;
-    }
-
-    for (i = 0; i < a->argc; ++i)
-    {
-        const char *arg = a->argv[i];
-        const size_t len = strlen(arg);
-        nchars += len + 3;
-        if (len > maxlen)
-        {
-            maxlen = len;
-        }
-    }
-
-    work = gc_malloc(maxlen + 1, false, gc);
-    check_malloc_return(work);
-    buf = alloc_buf_gc(nchars, gc);
-
-    for (i = 0; i < a->argc; ++i)
-    {
-        const char *arg = a->argv[i];
-        strcpy(work, arg);
-        string_mod(work, CC_PRINT, CC_DOUBLE_QUOTE | CC_CRLF, '_');
-        if (i)
-        {
-            buf_printf(&buf, " ");
-        }
-        if (argv_element_needs_quotes(work))
-        {
-            buf_printf(&buf, "\"%s\"", work);
-        }
-        else
-        {
-            buf_printf(&buf, "%s", work);
-        }
-    }
-
-    return wide_string(BSTR(&buf), gc);
-}
-
 /*
  * Attempt to simulate fork/execve on Windows
  */
diff --git a/tests/unit_tests/openvpn/test_argv.c b/tests/unit_tests/openvpn/test_argv.c
index b1e3261..5b6e26e 100644
--- a/tests/unit_tests/openvpn/test_argv.c
+++ b/tests/unit_tests/openvpn/test_argv.c
@@ -14,6 +14,10 @@
 #include "buffer.h"
 #include "test_common.h"
 
+#ifdef _WIN32
+#include "win32-util.h"
+#endif
+
 /* Defines for use in the tests and the mock parse_line() */
 #define PATH1      "/s p a c e"
 #define PATH2      "/foo bar/baz"
@@ -248,6 +252,56 @@
     argv_free(&a);
 }
 
+#ifdef _WIN32
+/*
+ * An argument is quoted if and only if it holds a space or a character that
+ * cmd.exe would act on when CreateProcess() runs a .bat/.cmd target.
+ */
+static void
+wide_cmd_line__quotes_only_what_cmd_would_reinterpret(void **state)
+{
+    static const struct
+    {
+        const char *arg;
+        const WCHAR *expected;
+    } cases[] = {
+        /* nothing special - must stay unquoted, or existing scripts break */
+        { "CN=user1", L"script.bat 0 CN=user1" },
+        /* the batch delimiters are deliberately not triggers */
+        { "CN=a,b;c=d", L"script.bat 0 CN=a,b;c=d" },
+        /* a space has always forced quoting */
+        { "O=Ctrl, CN=y", L"script.bat 0 \"O=Ctrl, CN=y\"" },
+        /* cmd.exe operators */
+        { "CN=x&ver", L"script.bat 0 \"CN=x&ver\"" },
+        { "CN=x|ver", L"script.bat 0 \"CN=x|ver\"" },
+        { "CN=x>f", L"script.bat 0 \"CN=x>f\"" },
+        { "CN=x<f", L"script.bat 0 \"CN=x<f\"" },
+        { "CN=x^f", L"script.bat 0 \"CN=x^f\"" },
+        { "CN=x%f", L"script.bat 0 \"CN=x%f\"" },
+        { "CN=x(f)", L"script.bat 0 \"CN=x(f)\"" },
+        { "CN=x!f", L"script.bat 0 \"CN=x!f\"" },
+        /* a double quote is replaced, so quoting cannot be broken out of */
+        { "CN=a\"b", L"script.bat 0 CN=a_b" },
+    };
+
+    for (size_t i = 0; i < SIZE(cases); i++)
+    {
+        struct gc_arena gc = gc_new();
+        struct argv a = argv_new();
+
+        argv_printf(&a, "%s %d %s", "script.bat", 0, cases[i].arg);
+        assert_int_equal(a.argc, 3);
+
+        WCHAR *cmd_line = wide_cmd_line(&a, &gc);
+        assert_non_null(cmd_line);
+        assert_int_equal(wcscmp(cmd_line, cases[i].expected), 0);
+
+        argv_free(&a);
+        gc_free(&gc);
+    }
+}
+#endif /* _WIN32 */
+
 int
 main(void)
 {
@@ -267,6 +321,9 @@
         cmocka_unit_test(argv_str__multiple_argv__correct_output),
         cmocka_unit_test(argv_insert_head__non_empty_argv__head_added),
         cmocka_unit_test(argv_insert_head__empty_argv__head_only),
+#ifdef _WIN32
+        cmocka_unit_test(wide_cmd_line__quotes_only_what_cmd_would_reinterpret),
+#endif
     };
 
     return cmocka_run_group_tests_name("argv", tests, NULL, NULL);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.