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)
| ... | ... |
@@ -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);
|