Skip to content

Commit d4921ba

Browse files
schwabecron2
authored andcommitted
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 414f428)
1 parent fccae1f commit d4921ba

File tree

3 files changed

+112
-35
lines changed

3 files changed

+112
-35
lines changed

src/openvpn/buffer.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,23 @@ string_mod(char *str, const unsigned int inclusive, const unsigned int exclusive
10851085
return ret;
10861086
}
10871087

1088+
bool
1089+
string_check_buf(struct buffer *buf, const unsigned int inclusive, const unsigned int exclusive)
1090+
{
1091+
ASSERT(buf);
1092+
1093+
for (int i = 0; i < BLEN(buf); i++)
1094+
{
1095+
char c = BSTR(buf)[i];
1096+
1097+
if (!char_inc_exc(c, inclusive, exclusive))
1098+
{
1099+
return false;
1100+
}
1101+
}
1102+
return true;
1103+
}
1104+
10881105
const char *
10891106
string_mod_const(const char *str,
10901107
const unsigned int inclusive,

src/openvpn/buffer.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,31 @@ bool string_class(const char *str, const unsigned int inclusive, const unsigned
933933

934934
bool string_mod(char *str, const unsigned int inclusive, const unsigned int exclusive, const char replace);
935935

936+
/**
937+
* Check a buffer if it only consists of allowed characters.
938+
*
939+
* @param buf The buffer to be checked.
940+
* @param inclusive The character classes that are allowed.
941+
* @param exclusive Character classes that are not allowed even if they are also in inclusive.
942+
* @return True if the string consists only of allowed characters, false otherwise.
943+
*/
944+
bool
945+
string_check_buf(struct buffer *buf, const unsigned int inclusive, const unsigned int exclusive);
946+
947+
/**
948+
* Returns a copy of a string with certain classes of characters of it replaced with a specified
949+
* character.
950+
*
951+
* If replace is 0, characters are skipped instead of replaced.
952+
*
953+
* @param str The input string to be modified.
954+
* @param inclusive Character classes not to be replaced.
955+
* @param exclusive Character classes to be replaced even if they are also in inclusive.
956+
* @param replace The character to replace the specified character classes with.
957+
* @param gc The garbage collector arena to allocate memory from.
958+
*
959+
* @return The modified string with characters replaced within the specified range.
960+
*/
936961
const char *string_mod_const(const char *str,
937962
const unsigned int inclusive,
938963
const unsigned int exclusive,

src/openvpn/forward.c

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,43 @@ check_tls_errors_nco(struct context *c)
184184

185185
#if P2MP
186186

187+
static void
188+
parse_incoming_control_channel_command(struct context *c, struct buffer *buf)
189+
{
190+
if (buf_string_match_head_str(buf, "AUTH_FAILED"))
191+
{
192+
receive_auth_failed(c, buf);
193+
}
194+
else if (buf_string_match_head_str(buf, "PUSH_"))
195+
{
196+
incoming_push_message(c, buf);
197+
}
198+
else if (buf_string_match_head_str(buf, "RESTART"))
199+
{
200+
server_pushed_signal(c, buf, true, 7);
201+
}
202+
else if (buf_string_match_head_str(buf, "HALT"))
203+
{
204+
server_pushed_signal(c, buf, false, 4);
205+
}
206+
else if (buf_string_match_head_str(buf, "INFO_PRE"))
207+
{
208+
server_pushed_info(c, buf, 8);
209+
}
210+
else if (buf_string_match_head_str(buf, "INFO"))
211+
{
212+
server_pushed_info(c, buf, 4);
213+
}
214+
else if (buf_string_match_head_str(buf, "CR_RESPONSE"))
215+
{
216+
receive_cr_response(c, buf);
217+
}
218+
else
219+
{
220+
msg(D_PUSH_ERRORS, "WARNING: Received unknown control message: %s", BSTR(buf));
221+
}
222+
}
223+
187224
/*
188225
* Handle incoming configuration
189226
* messages on the control channel.
@@ -199,43 +236,41 @@ check_incoming_control_channel(struct context *c)
199236
struct buffer buf = alloc_buf_gc(len, &gc);
200237
if (tls_rec_payload(c->c2.tls_multi, &buf))
201238
{
202-
/* force null termination of message */
203-
buf_null_terminate(&buf);
204-
205-
/* enforce character class restrictions */
206-
string_mod(BSTR(&buf), CC_PRINT, CC_CRLF, 0);
207239

208-
if (buf_string_match_head_str(&buf, "AUTH_FAILED"))
209-
{
210-
receive_auth_failed(c, &buf);
211-
}
212-
else if (buf_string_match_head_str(&buf, "PUSH_"))
213-
{
214-
incoming_push_message(c, &buf);
215-
}
216-
else if (buf_string_match_head_str(&buf, "RESTART"))
217-
{
218-
server_pushed_signal(c, &buf, true, 7);
219-
}
220-
else if (buf_string_match_head_str(&buf, "HALT"))
221-
{
222-
server_pushed_signal(c, &buf, false, 4);
223-
}
224-
else if (buf_string_match_head_str(&buf, "INFO_PRE"))
225-
{
226-
server_pushed_info(c, &buf, 8);
227-
}
228-
else if (buf_string_match_head_str(&buf, "INFO"))
240+
while (BLEN(&buf) > 1)
229241
{
230-
server_pushed_info(c, &buf, 4);
231-
}
232-
else if (buf_string_match_head_str(&buf, "CR_RESPONSE"))
233-
{
234-
receive_cr_response(c, &buf);
235-
}
236-
else
237-
{
238-
msg(D_PUSH_ERRORS, "WARNING: Received unknown control message: %s", BSTR(&buf));
242+
/* commands on the control channel are seperated by 0x00 bytes.
243+
* cmdlen does not include the 0 byte of the string */
244+
int cmdlen = (int)strnlen(BSTR(&buf), BLEN(&buf));
245+
246+
if (cmdlen < BLEN(&buf))
247+
{
248+
/* include the NUL byte and ensure NUL termination */
249+
int cmdlen = (int)strlen(BSTR(&buf)) + 1;
250+
251+
/* Construct a buffer that only holds the current command and
252+
* its closing NUL byte */
253+
struct buffer cmdbuf = alloc_buf_gc(cmdlen, &gc);
254+
buf_write(&cmdbuf, BPTR(&buf), cmdlen);
255+
256+
/* check we have only printable characters or null byte in the
257+
* command string and no newlines */
258+
if (!string_check_buf(&buf, CC_PRINT | CC_NULL, CC_CRLF))
259+
{
260+
msg(D_PUSH_ERRORS, "WARNING: Received control with invalid characters: %s",
261+
format_hex(BPTR(&buf), BLEN(&buf), 256, &gc));
262+
}
263+
else
264+
{
265+
parse_incoming_control_channel_command(c, &cmdbuf);
266+
}
267+
}
268+
else
269+
{
270+
msg(D_PUSH_ERRORS, "WARNING: Ignoring control channel "
271+
"message command without NUL termination");
272+
}
273+
buf_advance(&buf, cmdlen);
239274
}
240275
}
241276
else

0 commit comments

Comments
 (0)