Browse code

win32: Enforce loading of plugins from a trusted directory

Currently, there's a risk associated with allowing plugins to be loaded
from any location. This update ensures plugins are only loaded from a
trusted directory, which is either:

- HKLM\SOFTWARE\OpenVPN\plugin_dir (or if the key is missing,
then HKLM\SOFTWARE\OpenVPN, which is installation directory)

- System directory

Loading from UNC paths is disallowed.

Note: This change affects only Windows environments.

CVE: 2024-27903

Change-Id: I154a4aaad9242c9253a64312a14c5fd2ea95f40d
Reported-by: Vladimir Tokarev <vtokarev@microsoft.com>
Signed-off-by: Lev Stipakov <lev@openvpn.net>
Acked-by: Selva Nair <selva.nair@gmail.com>
Message-Id: <20240319135355.1279-2-lev@openvpn.net>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg28416.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>
(cherry picked from commit aaea545d8a940f761898d736b68bcb067d503b1d)

Lev Stipakov authored on 2024/03/19 22:53:45
Showing 3 changed files
... ...
@@ -279,11 +279,23 @@ plugin_init_item(struct plugin *p, const struct plugin_option *o)
279 279
 
280 280
 #else  /* ifndef _WIN32 */
281 281
 
282
-    rel = !platform_absolute_pathname(p->so_pathname);
283
-    p->module = LoadLibraryW(wide_string(p->so_pathname, &gc));
282
+    WCHAR *wpath = wide_string(p->so_pathname, &gc);
283
+    WCHAR normalized_plugin_path[MAX_PATH] = {0};
284
+    /* Normalize the plugin path, converting any relative paths to absolute paths. */
285
+    if (!GetFullPathNameW(wpath, MAX_PATH, normalized_plugin_path, NULL))
286
+    {
287
+        msg(M_ERR, "PLUGIN_INIT: could not load plugin DLL: %ls. Failed to normalize plugin path.", wpath);
288
+    }
289
+
290
+    if (!plugin_in_trusted_dir(normalized_plugin_path))
291
+    {
292
+        msg(M_FATAL, "PLUGIN_INIT: could not load plugin DLL: %ls. The DLL is not in a trusted directory.", normalized_plugin_path);
293
+    }
294
+
295
+    p->module = LoadLibraryW(normalized_plugin_path);
284 296
     if (!p->module)
285 297
     {
286
-        msg(M_ERR, "PLUGIN_INIT: could not load plugin DLL: %s", p->so_pathname);
298
+        msg(M_ERR, "PLUGIN_INIT: could not load plugin DLL: %ls", normalized_plugin_path);
287 299
     }
288 300
 
289 301
 #define PLUGIN_SYM(var, name, flags) dll_resolve_symbol(p->module, (void *)&p->var, name, p->so_pathname, flags)
... ...
@@ -1532,27 +1532,24 @@ openvpn_swprintf(wchar_t *const str, const size_t size, const wchar_t *const for
1532 1532
     return (len >= 0 && len < size);
1533 1533
 }
1534 1534
 
1535
-static BOOL
1536
-get_install_path(WCHAR *path, DWORD size)
1535
+bool
1536
+get_openvpn_reg_value(const WCHAR *key, WCHAR *value, DWORD size)
1537 1537
 {
1538 1538
     WCHAR reg_path[256];
1539
-    HKEY key;
1540
-    BOOL res = FALSE;
1539
+    HKEY hkey;
1541 1540
     openvpn_swprintf(reg_path, _countof(reg_path), L"SOFTWARE\\" PACKAGE_NAME);
1542 1541
 
1543
-    LONG status = RegOpenKeyExW(HKEY_LOCAL_MACHINE, reg_path, 0, KEY_READ, &key);
1542
+    LONG status = RegOpenKeyExW(HKEY_LOCAL_MACHINE, reg_path, 0, KEY_READ, &hkey);
1544 1543
     if (status != ERROR_SUCCESS)
1545 1544
     {
1546
-        return res;
1545
+        return false;
1547 1546
     }
1548 1547
 
1549
-    /* The default value of REG_KEY is the install path */
1550
-    status = RegGetValueW(key, NULL, NULL, RRF_RT_REG_SZ, NULL, (LPBYTE)path, &size);
1551
-    res = status == ERROR_SUCCESS;
1548
+    status = RegGetValueW(hkey, NULL, key, RRF_RT_REG_SZ, NULL, (LPBYTE)value, &size);
1552 1549
 
1553
-    RegCloseKey(key);
1550
+    RegCloseKey(hkey);
1554 1551
 
1555
-    return res;
1552
+    return status == ERROR_SUCCESS;
1556 1553
 }
1557 1554
 
1558 1555
 static void
... ...
@@ -1561,7 +1558,7 @@ set_openssl_env_vars()
1561 1561
     const WCHAR *ssl_fallback_dir = L"C:\\Windows\\System32";
1562 1562
 
1563 1563
     WCHAR install_path[MAX_PATH] = { 0 };
1564
-    if (!get_install_path(install_path, _countof(install_path)))
1564
+    if (!get_openvpn_reg_value(NULL, install_path, _countof(install_path)))
1565 1565
     {
1566 1566
         /* if we cannot find installation path from the registry,
1567 1567
          * use Windows directory as a fallback
... ...
@@ -1597,4 +1594,59 @@ set_openssl_env_vars()
1597 1597
     }
1598 1598
 }
1599 1599
 
1600
+bool
1601
+plugin_in_trusted_dir(const WCHAR *plugin_path)
1602
+{
1603
+    /* UNC paths are not allowed */
1604
+    if (wcsncmp(plugin_path, L"\\\\", 2) == 0)
1605
+    {
1606
+        msg(M_WARN, "UNC paths for plugins are not allowed.");
1607
+        return false;
1608
+    }
1609
+
1610
+    WCHAR plugin_dir[MAX_PATH] = { 0 };
1611
+
1612
+    /* Attempt to retrieve the trusted plugin directory path from the registry,
1613
+     * using installation path as a fallback */
1614
+    if (!get_openvpn_reg_value(L"plugin_dir", plugin_dir, _countof(plugin_dir))
1615
+        && !get_openvpn_reg_value(NULL, plugin_dir, _countof(plugin_dir)))
1616
+    {
1617
+        msg(M_WARN, "Installation path could not be determined.");
1618
+    }
1619
+
1620
+    /* Get the system directory */
1621
+    WCHAR system_dir[MAX_PATH] = { 0 };
1622
+    if (GetSystemDirectoryW(system_dir, _countof(system_dir)) == 0)
1623
+    {
1624
+        msg(M_NONFATAL | M_ERRNO, "Failed to get system directory.");
1625
+    }
1626
+
1627
+    if ((wcslen(plugin_dir) == 0) && (wcslen(system_dir) == 0))
1628
+    {
1629
+        return false;
1630
+    }
1631
+
1632
+    WCHAR normalized_plugin_dir[MAX_PATH] = { 0 };
1633
+
1634
+    /* Normalize the plugin dir */
1635
+    if (wcslen(plugin_dir) > 0)
1636
+    {
1637
+        if (!GetFullPathNameW(plugin_dir, MAX_PATH, normalized_plugin_dir, NULL))
1638
+        {
1639
+            msg(M_NONFATAL | M_ERRNO, "Failed to normalize plugin dir.");
1640
+            return false;
1641
+        }
1642
+    }
1643
+
1644
+    /* Check if the plugin path resides within the plugin/install directory */
1645
+    if ((wcslen(normalized_plugin_dir) > 0) && (wcsnicmp(normalized_plugin_dir,
1646
+                                                         plugin_path, wcslen(normalized_plugin_dir)) == 0))
1647
+    {
1648
+        return true;
1649
+    }
1650
+
1651
+    /* Fallback to the system directory */
1652
+    return wcsnicmp(system_dir, plugin_path, wcslen(system_dir)) == 0;
1653
+}
1654
+
1600 1655
 #endif /* ifdef _WIN32 */
... ...
@@ -335,5 +335,32 @@ openvpn_execve(const struct argv *a, const struct env_set *es, const unsigned in
335 335
 bool
336 336
 openvpn_swprintf(wchar_t *const str, const size_t size, const wchar_t *const format, ...);
337 337
 
338
+/**
339
+ * @brief Fetches a registry value for OpenVPN registry key.
340
+ *
341
+ * @param key Registry value name to fetch.
342
+ * @param value Buffer to store the fetched string value.
343
+ * @param size Size of `value` buffer in bytes.
344
+ * @return `true` if successful, `false` otherwise.
345
+ */
346
+bool
347
+get_openvpn_reg_value(const WCHAR *key, WCHAR *value, DWORD size);
348
+
349
+/**
350
+ * @brief Checks if a plugin is located in a trusted directory.
351
+ *
352
+ * Verifies the plugin's path against a trusted directory, which is:
353
+ *
354
+ * - "plugin_dir" registry value or installation path, if the registry key is missing
355
+ * - system directory
356
+ *
357
+ * UNC paths are explicitly disallowed.
358
+ *
359
+ * @param plugin_path Normalized path to the plugin.
360
+ * @return \c true if the plugin is in a trusted directory and not a UNC path; \c false otherwise.
361
+ */
362
+bool
363
+plugin_in_trusted_dir(const WCHAR *plugin_path);
364
+
338 365
 #endif /* ifndef OPENVPN_WIN32_H */
339 366
 #endif /* ifdef _WIN32 */