Browse code

Validate DNS parameters

This adds validation of following DNS options:

--dns search-domains
--dns server N resolve-domains
--dns server N sni

--dhcp-option DOMAIN
--dhcp-option ADAPTER_DOMAIN_SUFFIX
--dhcp-option DOMAIN-SEARCH

On Linux (and similar platforms), those options are written to a tmp file,
which is later sourced by a script running as root. Since options are
controlled by the server, it is possible for a malicious server to
execute script injection attack by pushing something like

--dns search-domains x;id

in which case "id" command will be executed as a root.

On Windows, the value of DOMAIN/ADAPTER_DOMAIN_SUFFIX is passed to
a powershell script. A malicious server could push:

--dhcp-option DOMAIN a';Restart-Computer'

and if openvpn is not using DHCP (this is the default, with dco-win driver)
and running without interactive service, that powershell command will be
executed.

Validation is performed in a way that value only contains following
symbols:

[A-Za-z0-9.-_\x80-\0xff]

Reported-By: Stanislav Fort <disclosure@aisle.com>
CVE: 2025-10680
Change-Id: I09209ccd785cc368b2fcf467a3d211fbd41005c6
Signed-off-by: Lev Stipakov <lev@openvpn.net>
Acked-by: Gert Doering <gert@greenie.muc.de>
Gerrit URL: https://gerrit.openvpn.net/c/openvpn/+/1213
Message-Id: <20250924201601.25304-1-gert@greenie.muc.de>
URL: https://sourceforge.net/p/openvpn/mailman/message/59238367/
Signed-off-by: Gert Doering <gert@greenie.muc.de>

Lev Stipakov authored on 2025/09/25 05:15:56
Showing 5 changed files
... ...
@@ -61,6 +61,7 @@ openvpn_SOURCES = \
61 61
 	dco_win.c dco_win.h \
62 62
 	dhcp.c dhcp.h \
63 63
 	dns.c dns.h \
64
+	domain_helper.h \
64 65
 	env_set.c env_set.h \
65 66
 	errlevel.h \
66 67
 	error.c error.h \
... ...
@@ -30,6 +30,7 @@
30 30
 #include "socket_util.h"
31 31
 #include "options.h"
32 32
 #include "run_command.h"
33
+#include "domain_helper.h"
33 34
 
34 35
 #ifdef _WIN32
35 36
 #include "win32.h"
... ...
@@ -143,7 +144,7 @@ dns_server_addr_parse(struct dns_server *server, const char *addr)
143 143
     return true;
144 144
 }
145 145
 
146
-void
146
+bool
147 147
 dns_domain_list_append(struct dns_domain **entry, char **domains, struct gc_arena *gc)
148 148
 {
149 149
     /* Fast forward to the end of the list */
... ...
@@ -155,11 +156,19 @@ dns_domain_list_append(struct dns_domain **entry, char **domains, struct gc_aren
155 155
     /* Append all domains to the end of the list */
156 156
     while (*domains)
157 157
     {
158
+        char *domain = *domains++;
159
+        if (!validate_domain(domain))
160
+        {
161
+            return false;
162
+        }
163
+
158 164
         ALLOC_OBJ_CLEAR_GC(*entry, struct dns_domain, gc);
159 165
         struct dns_domain *new = *entry;
160
-        new->name = *domains++;
166
+        new->name = domain;
161 167
         entry = &new->next;
162 168
     }
169
+
170
+    return true;
163 171
 }
164 172
 
165 173
 bool
... ...
@@ -141,13 +141,14 @@ bool dns_server_priority_parse(long *priority, const char *str, bool pulled);
141 141
 struct dns_server *dns_server_get(struct dns_server **entry, long priority, struct gc_arena *gc);
142 142
 
143 143
 /**
144
- * Appends DNS domain parameters to a linked list.
144
+ * Appends safe DNS domain parameters to a linked list.
145 145
  *
146 146
  * @param   entry       Address of the first list entry pointer
147 147
  * @param   domains     Address of the first domain parameter
148 148
  * @param   gc          The gc the new list items should be allocated in
149
+ * @return              True if domains were appended and don't contain invalid characters
149 150
  */
150
-void dns_domain_list_append(struct dns_domain **entry, char **domains, struct gc_arena *gc);
151
+bool dns_domain_list_append(struct dns_domain **entry, char **domains, struct gc_arena *gc);
151 152
 
152 153
 /**
153 154
  * Parses a string IPv4 or IPv6 address and optional colon separated port,
154 155
new file mode 100644
... ...
@@ -0,0 +1,45 @@
0
+/*
1
+ *  OpenVPN -- An application to securely tunnel IP networks
2
+ *             over a single UDP port, with support for SSL/TLS-based
3
+ *             session authentication and key exchange,
4
+ *             packet encryption, packet authentication, and
5
+ *             packet compression.
6
+ *
7
+ *  Copyright (C) 2025 Lev Stipakov <lev@openvpn.net>
8
+ *
9
+ *  This program is free software; you can redistribute it and/or modify
10
+ *  it under the terms of the GNU General Public License version 2
11
+ *  as published by the Free Software Foundation.
12
+ *
13
+ *  This program is distributed in the hope that it will be useful,
14
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
+ *  GNU General Public License for more details.
17
+ *
18
+ *  You should have received a copy of the GNU General Public License along
19
+ *  with this program; if not, write to the Free Software Foundation, Inc.,
20
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
+ */
22
+
23
+static inline bool
24
+is_allowed_domain_ascii(unsigned char c)
25
+{
26
+    return (c >= 'A' && c <= 'Z')
27
+           || (c >= 'a' && c <= 'z')
28
+           || (c >= '0' && c <= '9')
29
+           || c == '.' || c == '-' || c == '_' || c >= 0x80;
30
+}
31
+
32
+static inline bool
33
+validate_domain(const char *domain)
34
+{
35
+    for (const char *ch = domain; *ch; ++ch)
36
+    {
37
+        if (!is_allowed_domain_ascii((unsigned char)*ch))
38
+        {
39
+            return false;
40
+        }
41
+    }
42
+
43
+    return true;
44
+}
... ...
@@ -61,6 +61,7 @@
61 61
 #include "dco.h"
62 62
 #include "options_util.h"
63 63
 #include "tun_afunix.h"
64
+#include "domain_helper.h"
64 65
 
65 66
 #include <ctype.h>
66 67
 
... ...
@@ -5877,8 +5878,12 @@ check_dns_option(struct options *options, char *p[], const msglvl_t msglevel, bo
5877 5877
 {
5878 5878
     if (streq(p[1], "search-domains") && p[2])
5879 5879
     {
5880
-        dns_domain_list_append(&options->dns_options.search_domains, &p[2],
5881
-                               &options->dns_options.gc);
5880
+        if (!dns_domain_list_append(&options->dns_options.search_domains, &p[2],
5881
+                                    &options->dns_options.gc))
5882
+        {
5883
+            msg(msglevel, "--dns %s contain invalid characters", p[1]);
5884
+            return false;
5885
+        }
5882 5886
     }
5883 5887
     else if (streq(p[1], "server") && p[2] && p[3] && p[4])
5884 5888
     {
... ...
@@ -5906,7 +5911,11 @@ check_dns_option(struct options *options, char *p[], const msglvl_t msglevel, bo
5906 5906
         }
5907 5907
         else if (streq(p[3], "resolve-domains"))
5908 5908
         {
5909
-            dns_domain_list_append(&server->domains, &p[4], &options->dns_options.gc);
5909
+            if (!dns_domain_list_append(&server->domains, &p[4], &options->dns_options.gc))
5910
+            {
5911
+                msg(msglevel, "--dns server %ld: %s contain invalid characters", priority, p[3]);
5912
+                return false;
5913
+            }
5910 5914
         }
5911 5915
         else if (streq(p[3], "dnssec") && !p[5])
5912 5916
         {
... ...
@@ -5950,6 +5959,11 @@ check_dns_option(struct options *options, char *p[], const msglvl_t msglevel, bo
5950 5950
         }
5951 5951
         else if (streq(p[3], "sni") && !p[5])
5952 5952
         {
5953
+            if (!validate_domain(p[4]))
5954
+            {
5955
+                msg(msglevel, "--dns server %ld: %s contains invalid characters", priority, p[3]);
5956
+                return false;
5957
+            }
5953 5958
             server->sni = p[4];
5954 5959
         }
5955 5960
         else
... ...
@@ -8551,11 +8565,23 @@ add_option(struct options *options, char *p[], bool is_inline, const char *file,
8551 8551
 
8552 8552
         if ((streq(p[1], "DOMAIN") || streq(p[1], "ADAPTER_DOMAIN_SUFFIX")) && p[2] && !p[3])
8553 8553
         {
8554
+            if (!validate_domain(p[2]))
8555
+            {
8556
+                msg(msglevel, "--dhcp-option %s contains invalid characters", p[1]);
8557
+                goto err;
8558
+            }
8559
+
8554 8560
             dhcp->domain = p[2];
8555 8561
             dhcp_optional = true;
8556 8562
         }
8557 8563
         else if (streq(p[1], "DOMAIN-SEARCH") && p[2] && !p[3])
8558 8564
         {
8565
+            if (!validate_domain(p[2]))
8566
+            {
8567
+                msg(msglevel, "--dhcp-option %s contains invalid characters", p[1]);
8568
+                goto err;
8569
+            }
8570
+
8559 8571
             if (dhcp->domain_search_list_len < N_SEARCH_LIST_LEN)
8560 8572
             {
8561 8573
                 dhcp->domain_search_list[dhcp->domain_search_list_len++] = p[2];