diff --git a/src/openvpn/buffer.c b/src/openvpn/buffer.c
index 5ce2e39..5d00415 100644
--- a/src/openvpn/buffer.c
+++ b/src/openvpn/buffer.c
@@ -943,11 +943,11 @@
     {
         return true;
     }
-    if ((flags & CC_AT) && c == '@')
+    if ((flags & CC_PERCENT) && c == '%')
     {
         return true;
     }
-    if ((flags & CC_EQUAL) && c == '=')
+    if ((flags & CC_EXCLAMATION) && c == '!')
     {
         return true;
     }
diff --git a/src/openvpn/buffer.h b/src/openvpn/buffer.h
index 4471697..0a02e94 100644
--- a/src/openvpn/buffer.h
+++ b/src/openvpn/buffer.h
@@ -1660,8 +1660,8 @@
 #define CC_SINGLE_QUOTE  (1 << 21) /**< single quote */
 #define CC_DOUBLE_QUOTE  (1 << 22) /**< double quote */
 #define CC_REVERSE_QUOTE (1 << 23) /**< reverse quote */
-#define CC_AT            (1 << 24) /**< at sign */
-#define CC_EQUAL         (1 << 25) /**< equal sign */
+#define CC_PERCENT       (1 << 24) /**< percent sign */
+#define CC_EXCLAMATION   (1 << 25) /**< exclamation mark */
 #define CC_LESS_THAN     (1 << 26) /**< less than sign */
 #define CC_GREATER_THAN  (1 << 27) /**< greater than sign */
 #define CC_PIPE          (1 << 28) /**< pipe */
diff --git a/src/openvpn/win32-util.c b/src/openvpn/win32-util.c
index 4e3819c..e2c8810 100644
--- a/src/openvpn/win32-util.c
+++ b/src/openvpn/win32-util.c
@@ -94,7 +94,10 @@
     {
         const char *arg = a->argv[i];
         strcpy(work, arg);
-        string_mod(work, CC_PRINT, CC_DOUBLE_QUOTE | CC_CRLF, '_');
+        /* cmd.exe expands %VAR% and !VAR! even inside quotes, so a value like
+         * %X509_0_O% could turn back into a quote and start a new command.
+         * Replace those along with the double quotes and CRLF. */
+        string_mod(work, CC_PRINT, CC_DOUBLE_QUOTE | CC_CRLF | CC_PERCENT | CC_EXCLAMATION, '_');
         if (i)
         {
             buf_printf(&buf, " ");
diff --git a/tests/unit_tests/openvpn/test_argv.c b/tests/unit_tests/openvpn/test_argv.c
index 5b6e26e..1fc73d8 100644
--- a/tests/unit_tests/openvpn/test_argv.c
+++ b/tests/unit_tests/openvpn/test_argv.c
@@ -271,15 +271,13 @@
         { "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 */
+        /* cmd.exe operators that quoting neutralizes */
         { "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" },
     };
@@ -300,6 +298,49 @@
         gc_free(&gc);
     }
 }
+
+/*
+ * cmd.exe expands %VAR% and !VAR! even inside quotes, and OpenVPN puts
+ * peer-controlled data (e.g. certificate subject fields) on the command line,
+ * so these characters are replaced to stop a value from expanding back into a
+ * quote and command operator.
+ */
+static void
+wide_cmd_line__replaces_cmd_expansion(void **state)
+{
+    static const struct
+    {
+        const char *arg;
+        const WCHAR *expected;
+    } cases[] = {
+        /* a bare percent or bang is replaced */
+        { "CN=x%f", L"script.bat 0 CN=x_f" },
+        { "CN=x!f", L"script.bat 0 CN=x_f" },
+        /* a closed expansion token is replaced, so nothing expands */
+        { "CN=%X509_0_O%", L"script.bat 0 CN=_X509_0_O_" },
+        { "CN=!X509_0_O!", L"script.bat 0 CN=_X509_0_O_" },
+        /* the reported bypass: quotes in one field are already replaced, and
+         * neutralizing % stops %X509_0_O% from re-injecting them */
+        { "O=BREAK\"&whoami&\", CN=%X509_0_O%",
+          L"script.bat 0 \"O=BREAK_&whoami&_, CN=_X509_0_O_\"" },
+    };
+
+    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
@@ -323,6 +364,7 @@
         cmocka_unit_test(argv_insert_head__empty_argv__head_only),
 #ifdef _WIN32
         cmocka_unit_test(wide_cmd_line__quotes_only_what_cmd_would_reinterpret),
+        cmocka_unit_test(wide_cmd_line__replaces_cmd_expansion),
 #endif
     };
 
