Browse code

Properly handle null bytes and invalid characters in control messages

This makes OpenVPN more picky in accepting control message in two aspects:
- Characters are checked in the whole buffer and not until the first
NUL byte
- if the message contains invalid characters, we no longer continue
evaluating a fixed up version of the message but rather stop
processing it completely.

Previously it was possible to get invalid characters to end up in log
files or on a terminal.

This also prepares the logic a bit in the direction of having a proper
framing of control messages separated by null bytes instead of relying
on the TLS framing for that. All OpenVPN implementations write the 0
bytes between control commands.

This patch also include several improvement suggestion from Reynir
(thanks!).

CVE: 2024-5594

Reported-By: Reynir Björnsson <reynir@reynir.dk>
Change-Id: I0d926f910637dabc89bf5fa919dc6beef1eb46d9
Signed-off-by: Arne Schwabe <arne@rfc2549.org>
Acked-by: Antonio Quartulli <a@unstable.cc>

Message-Id: <20240619103004.56460-1-gert@greenie.muc.de>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg28791.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>
(cherry picked from commit 414f428fa29694090ec4c46b10a8aba419c85659)

Arne Schwabe authored on 2024/05/27 22:02:41
Showing 4 changed files
... ...
@@ -1113,6 +1113,23 @@ string_mod(char *str, const unsigned int inclusive, const unsigned int exclusive
1113 1113
     return ret;
1114 1114
 }
1115 1115
 
1116
+bool
1117
+string_check_buf(struct buffer *buf, const unsigned int inclusive, const unsigned int exclusive)
1118
+{
1119
+    ASSERT(buf);
1120
+
1121
+    for (int i = 0; i < BLEN(buf); i++)
1122
+    {
1123
+        char c = BSTR(buf)[i];
1124
+
1125
+        if (!char_inc_exc(c, inclusive, exclusive))
1126
+        {
1127
+            return false;
1128
+        }
1129
+    }
1130
+    return true;
1131
+}
1132
+
1116 1133
 const char *
1117 1134
 string_mod_const(const char *str,
1118 1135
                  const unsigned int inclusive,
... ...
@@ -945,6 +945,17 @@ bool string_class(const char *str, const unsigned int inclusive, const unsigned
945 945
 
946 946
 bool string_mod(char *str, const unsigned int inclusive, const unsigned int exclusive, const char replace);
947 947
 
948
+/**
949
+ * Check a buffer if it only consists of allowed characters.
950
+ *
951
+ * @param buf The buffer to be checked.
952
+ * @param inclusive The character classes that are allowed.
953
+ * @param exclusive Character classes that are not allowed even if they are also in inclusive.
954
+ * @return True if the string consists only of allowed characters, false otherwise.
955
+ */
956
+bool
957
+string_check_buf(struct buffer *buf, const unsigned int inclusive, const unsigned int exclusive);
958
+
948 959
 const char *string_mod_const(const char *str,
949 960
                              const unsigned int inclusive,
950 961
                              const unsigned int exclusive,
... ...
@@ -230,6 +230,51 @@ check_tls(struct context *c)
230 230
     }
231 231
 }
232 232
 
233
+static void
234
+parse_incoming_control_channel_command(struct context *c, struct buffer *buf)
235
+{
236
+    if (buf_string_match_head_str(buf, "AUTH_FAILED"))
237
+    {
238
+        receive_auth_failed(c, buf);
239
+    }
240
+    else if (buf_string_match_head_str(buf, "PUSH_"))
241
+    {
242
+        incoming_push_message(c, buf);
243
+    }
244
+    else if (buf_string_match_head_str(buf, "RESTART"))
245
+    {
246
+        server_pushed_signal(c, buf, true, 7);
247
+    }
248
+    else if (buf_string_match_head_str(buf, "HALT"))
249
+    {
250
+        server_pushed_signal(c, buf, false, 4);
251
+    }
252
+    else if (buf_string_match_head_str(buf, "INFO_PRE"))
253
+    {
254
+        server_pushed_info(c, buf, 8);
255
+    }
256
+    else if (buf_string_match_head_str(buf, "INFO"))
257
+    {
258
+        server_pushed_info(c, buf, 4);
259
+    }
260
+    else if (buf_string_match_head_str(buf, "CR_RESPONSE"))
261
+    {
262
+        receive_cr_response(c, buf);
263
+    }
264
+    else if (buf_string_match_head_str(buf, "AUTH_PENDING"))
265
+    {
266
+        receive_auth_pending(c, buf);
267
+    }
268
+    else if (buf_string_match_head_str(buf, "EXIT"))
269
+    {
270
+        receive_exit_message(c);
271
+    }
272
+    else
273
+    {
274
+        msg(D_PUSH_ERRORS, "WARNING: Received unknown control message: %s", BSTR(buf));
275
+    }
276
+}
277
+
233 278
 /*
234 279
  * Handle incoming configuration
235 280
  * messages on the control channel.
... ...
@@ -245,51 +290,41 @@ check_incoming_control_channel(struct context *c)
245 245
     struct buffer buf = alloc_buf_gc(len, &gc);
246 246
     if (tls_rec_payload(c->c2.tls_multi, &buf))
247 247
     {
248
-        /* force null termination of message */
249
-        buf_null_terminate(&buf);
250
-
251
-        /* enforce character class restrictions */
252
-        string_mod(BSTR(&buf), CC_PRINT, CC_CRLF, 0);
253 248
 
254
-        if (buf_string_match_head_str(&buf, "AUTH_FAILED"))
249
+        while (BLEN(&buf) > 1)
255 250
         {
256
-            receive_auth_failed(c, &buf);
257
-        }
258
-        else if (buf_string_match_head_str(&buf, "PUSH_"))
259
-        {
260
-            incoming_push_message(c, &buf);
261
-        }
262
-        else if (buf_string_match_head_str(&buf, "RESTART"))
263
-        {
264
-            server_pushed_signal(c, &buf, true, 7);
265
-        }
266
-        else if (buf_string_match_head_str(&buf, "HALT"))
267
-        {
268
-            server_pushed_signal(c, &buf, false, 4);
269
-        }
270
-        else if (buf_string_match_head_str(&buf, "INFO_PRE"))
271
-        {
272
-            server_pushed_info(c, &buf, 8);
273
-        }
274
-        else if (buf_string_match_head_str(&buf, "INFO"))
275
-        {
276
-            server_pushed_info(c, &buf, 4);
277
-        }
278
-        else if (buf_string_match_head_str(&buf, "CR_RESPONSE"))
279
-        {
280
-            receive_cr_response(c, &buf);
281
-        }
282
-        else if (buf_string_match_head_str(&buf, "AUTH_PENDING"))
283
-        {
284
-            receive_auth_pending(c, &buf);
285
-        }
286
-        else if (buf_string_match_head_str(&buf, "EXIT"))
287
-        {
288
-            receive_exit_message(c);
289
-        }
290
-        else
291
-        {
292
-            msg(D_PUSH_ERRORS, "WARNING: Received unknown control message: %s", BSTR(&buf));
251
+            /* commands on the control channel are seperated by 0x00 bytes.
252
+             * cmdlen does not include the 0 byte of the string */
253
+            int cmdlen = (int)strnlen(BSTR(&buf), BLEN(&buf));
254
+
255
+            if (cmdlen < BLEN(&buf))
256
+            {
257
+                /* include the NUL byte and ensure NUL termination */
258
+                int cmdlen = (int)strlen(BSTR(&buf)) + 1;
259
+
260
+                /* Construct a buffer that only holds the current command and
261
+                 * its closing NUL byte */
262
+                struct buffer cmdbuf = alloc_buf_gc(cmdlen, &gc);
263
+                buf_write(&cmdbuf, BPTR(&buf), cmdlen);
264
+
265
+                /* check we have only printable characters or null byte in the
266
+                 * command string and no newlines */
267
+                if (!string_check_buf(&buf, CC_PRINT | CC_NULL, CC_CRLF))
268
+                {
269
+                    msg(D_PUSH_ERRORS, "WARNING: Received control with invalid characters: %s",
270
+                        format_hex(BPTR(&buf), BLEN(&buf), 256, &gc));
271
+                }
272
+                else
273
+                {
274
+                    parse_incoming_control_channel_command(c, &cmdbuf);
275
+                }
276
+            }
277
+            else
278
+            {
279
+                msg(D_PUSH_ERRORS, "WARNING: Ignoring control channel "
280
+                    "message command without NUL termination");
281
+            }
282
+            buf_advance(&buf, cmdlen);
293 283
         }
294 284
     }
295 285
     else
... ...
@@ -259,6 +259,112 @@ test_buffer_gc_realloc(void **state)
259 259
     gc_free(&gc);
260 260
 }
261 261
 
262
+static void
263
+test_character_class(void **state)
264
+{
265
+    char buf[256];
266
+    strcpy(buf, "There is \x01 a nice 1234 year old tr\x7f ee!");
267
+    assert_false(string_mod(buf, CC_PRINT, 0, '@'));
268
+    assert_string_equal(buf, "There is @ a nice 1234 year old tr@ ee!");
269
+
270
+    strcpy(buf, "There is \x01 a nice 1234 year old tr\x7f ee!");
271
+    assert_true(string_mod(buf, CC_ANY, 0, '@'));
272
+    assert_string_equal(buf, "There is \x01 a nice 1234 year old tr\x7f ee!");
273
+
274
+    /* 0 as replace removes characters */
275
+    strcpy(buf, "There is \x01 a nice 1234 year old tr\x7f ee!");
276
+    assert_false(string_mod(buf, CC_PRINT, 0, '\0'));
277
+    assert_string_equal(buf, "There is  a nice 1234 year old tr ee!");
278
+
279
+    strcpy(buf, "There is \x01 a nice 1234 year old tr\x7f ee!");
280
+    assert_false(string_mod(buf, CC_PRINT, CC_DIGIT, '@'));
281
+    assert_string_equal(buf, "There is @ a nice @@@@ year old tr@ ee!");
282
+
283
+    strcpy(buf, "There is \x01 a nice 1234 year old tr\x7f ee!");
284
+    assert_false(string_mod(buf, CC_ALPHA, CC_DIGIT, '.'));
285
+    assert_string_equal(buf, "There.is...a.nice......year.old.tr..ee.");
286
+
287
+    strcpy(buf, "There is \x01 a 'nice' \"1234\"\n year old \ntr\x7f ee!");
288
+    assert_false(string_mod(buf, CC_ALPHA|CC_DIGIT|CC_NEWLINE|CC_SINGLE_QUOTE, CC_DOUBLE_QUOTE|CC_BLANK, '.'));
289
+    assert_string_equal(buf, "There.is...a.'nice'..1234.\n.year.old.\ntr..ee.");
290
+
291
+    strcpy(buf, "There is a \\'nice\\' \"1234\" [*] year old \ntree!");
292
+    assert_false(string_mod(buf, CC_PRINT, CC_BACKSLASH|CC_ASTERISK, '.'));
293
+    assert_string_equal(buf, "There is a .'nice.' \"1234\" [.] year old .tree!");
294
+}
295
+
296
+
297
+static void
298
+test_character_string_mod_buf(void **state)
299
+{
300
+    struct gc_arena gc = gc_new();
301
+
302
+    struct buffer buf = alloc_buf_gc(1024, &gc);
303
+
304
+    const char test1[] =  "There is a nice 1234\x00 year old tree!";
305
+    buf_write(&buf, test1, sizeof(test1));
306
+
307
+    /* allow the null bytes and string but not the ! */
308
+    assert_false(string_check_buf(&buf, CC_ALNUM | CC_SPACE | CC_NULL, 0));
309
+
310
+    /* remove final ! and null byte to pass */
311
+    buf_inc_len(&buf, -2);
312
+    assert_true(string_check_buf(&buf, CC_ALNUM | CC_SPACE | CC_NULL, 0));
313
+
314
+    /* Check excluding digits works */
315
+    assert_false(string_check_buf(&buf, CC_ALNUM | CC_SPACE | CC_NULL, CC_DIGIT));
316
+    gc_free(&gc);
317
+}
318
+
319
+static void
320
+test_snprintf(void **state)
321
+{
322
+    /* we used to have a custom openvpn_snprintf function because some
323
+     * OS (the comment did not specify which) did not always put the
324
+     * null byte there. So we unit test this to be sure.
325
+     *
326
+     * This probably refers to the MSVC behaviour, see also
327
+     * https://stackoverflow.com/questions/7706936/is-snprintf-always-null-terminating
328
+     */
329
+
330
+    /* Instead of trying to trick the compiler here, disable the warnings
331
+     * for this unit test. We know that the results will be truncated
332
+     * and we want to test that */
333
+#if defined(__GNUC__)
334
+/* some clang version do not understand -Wformat-truncation, so ignore the
335
+ * warning to avoid warnings/errors (-Werror) about unknown pragma/option */
336
+#if defined(__clang__)
337
+#pragma clang diagnostic push
338
+#pragma clang diagnostic ignored "-Wunknown-warning-option"
339
+#endif
340
+#pragma GCC diagnostic push
341
+#pragma GCC diagnostic ignored "-Wformat-truncation"
342
+#endif
343
+
344
+    char buf[10] = { 'a' };
345
+    int ret = 0;
346
+
347
+    ret = snprintf(buf, sizeof(buf), "0123456789abcde");
348
+    assert_int_equal(ret, 15);
349
+    assert_int_equal(buf[9], '\0');
350
+
351
+    memset(buf, 'b', sizeof(buf));
352
+    ret = snprintf(buf, sizeof(buf), "- %d - %d -", 77, 88);
353
+    assert_int_equal(ret, 11);
354
+    assert_int_equal(buf[9], '\0');
355
+
356
+    memset(buf, 'c', sizeof(buf));
357
+    ret = snprintf(buf, sizeof(buf), "- %8.2f", 77.8899);
358
+    assert_int_equal(ret, 10);
359
+    assert_int_equal(buf[9], '\0');
360
+
361
+#if defined(__GNUC__)
362
+#pragma GCC diagnostic pop
363
+#if defined(__clang__)
364
+#pragma clang diagnostic pop
365
+#endif
366
+#endif
367
+}
262 368
 
263 369
 int
264 370
 main(void)
... ...
@@ -289,6 +395,9 @@ main(void)
289 289
         cmocka_unit_test(test_buffer_free_gc_one),
290 290
         cmocka_unit_test(test_buffer_free_gc_two),
291 291
         cmocka_unit_test(test_buffer_gc_realloc),
292
+        cmocka_unit_test(test_character_class),
293
+        cmocka_unit_test(test_character_string_mod_buf),
294
+        cmocka_unit_test(test_snprintf)
292 295
     };
293 296
 
294 297
     return cmocka_run_group_tests_name("buffer", tests, NULL, NULL);