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