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
... ...
@@ -277,11 +277,23 @@ plugin_init_item(struct plugin *p, const struct plugin_option *o)
277 277
 
278 278
 #else  /* ifndef _WIN32 */
279 279
 
280
-    rel = !platform_absolute_pathname(p->so_pathname);
281
-    p->module = LoadLibraryW(wide_string(p->so_pathname, &gc));
280
+    WCHAR *wpath = wide_string(p->so_pathname, &gc);
281
+    WCHAR normalized_plugin_path[MAX_PATH] = {0};
282
+    /* Normalize the plugin path, converting any relative paths to absolute paths. */
283
+    if (!GetFullPathNameW(wpath, MAX_PATH, normalized_plugin_path, NULL))
284
+    {
285
+        msg(M_ERR, "PLUGIN_INIT: could not load plugin DLL: %ls. Failed to normalize plugin path.", wpath);
286
+    }
287
+
288
+    if (!plugin_in_trusted_dir(normalized_plugin_path))
289
+    {
290
+        msg(M_FATAL, "PLUGIN_INIT: could not load plugin DLL: %ls. The DLL is not in a trusted directory.", normalized_plugin_path);
291
+    }
292
+
293
+    p->module = LoadLibraryW(normalized_plugin_path);
282 294
     if (!p->module)
283 295
     {
284
-        msg(M_ERR, "PLUGIN_INIT: could not load plugin DLL: %s", p->so_pathname);
296
+        msg(M_ERR, "PLUGIN_INIT: could not load plugin DLL: %ls", normalized_plugin_path);
285 297
     }
286 298
 
287 299
 #define PLUGIN_SYM(var, name, flags) dll_resolve_symbol(p->module, (void *)&p->var, name, p->so_pathname, flags)
... ...
@@ -1525,27 +1525,24 @@ openvpn_swprintf(wchar_t *const str, const size_t size, const wchar_t *const for
1525 1525
     return (len >= 0 && len < size);
1526 1526
 }
1527 1527
 
1528
-static BOOL
1529
-get_install_path(WCHAR *path, DWORD size)
1528
+bool
1529
+get_openvpn_reg_value(const WCHAR *key, WCHAR *value, DWORD size)
1530 1530
 {
1531 1531
     WCHAR reg_path[256];
1532
-    HKEY key;
1533
-    BOOL res = FALSE;
1532
+    HKEY hkey;
1534 1533
     openvpn_swprintf(reg_path, _countof(reg_path), L"SOFTWARE\\" PACKAGE_NAME);
1535 1534
 
1536
-    LONG status = RegOpenKeyExW(HKEY_LOCAL_MACHINE, reg_path, 0, KEY_READ, &key);
1535
+    LONG status = RegOpenKeyExW(HKEY_LOCAL_MACHINE, reg_path, 0, KEY_READ, &hkey);
1537 1536
     if (status != ERROR_SUCCESS)
1538 1537
     {
1539
-        return res;
1538
+        return false;
1540 1539
     }
1541 1540
 
1542
-    /* The default value of REG_KEY is the install path */
1543
-    status = RegGetValueW(key, NULL, NULL, RRF_RT_REG_SZ, NULL, (LPBYTE)path, &size);
1544
-    res = status == ERROR_SUCCESS;
1541
+    status = RegGetValueW(hkey, NULL, key, RRF_RT_REG_SZ, NULL, (LPBYTE)value, &size);
1545 1542
 
1546
-    RegCloseKey(key);
1543
+    RegCloseKey(hkey);
1547 1544
 
1548
-    return res;
1545
+    return status == ERROR_SUCCESS;
1549 1546
 }
1550 1547
 
1551 1548
 static void
... ...
@@ -1554,7 +1551,7 @@ set_openssl_env_vars()
1554 1554
     const WCHAR *ssl_fallback_dir = L"C:\\Windows\\System32";
1555 1555
 
1556 1556
     WCHAR install_path[MAX_PATH] = { 0 };
1557
-    if (!get_install_path(install_path, _countof(install_path)))
1557
+    if (!get_openvpn_reg_value(NULL, install_path, _countof(install_path)))
1558 1558
     {
1559 1559
         /* if we cannot find installation path from the registry,
1560 1560
          * use Windows directory as a fallback
... ...
@@ -1633,4 +1630,60 @@ win32_sleep(const int n)
1633 1633
         }
1634 1634
     }
1635 1635
 }
1636
+
1637
+bool
1638
+plugin_in_trusted_dir(const WCHAR *plugin_path)
1639
+{
1640
+    /* UNC paths are not allowed */
1641
+    if (wcsncmp(plugin_path, L"\\\\", 2) == 0)
1642
+    {
1643
+        msg(M_WARN, "UNC paths for plugins are not allowed.");
1644
+        return false;
1645
+    }
1646
+
1647
+    WCHAR plugin_dir[MAX_PATH] = { 0 };
1648
+
1649
+    /* Attempt to retrieve the trusted plugin directory path from the registry,
1650
+     * using installation path as a fallback */
1651
+    if (!get_openvpn_reg_value(L"plugin_dir", plugin_dir, _countof(plugin_dir))
1652
+        && !get_openvpn_reg_value(NULL, plugin_dir, _countof(plugin_dir)))
1653
+    {
1654
+        msg(M_WARN, "Installation path could not be determined.");
1655
+    }
1656
+
1657
+    /* Get the system directory */
1658
+    WCHAR system_dir[MAX_PATH] = { 0 };
1659
+    if (GetSystemDirectoryW(system_dir, _countof(system_dir)) == 0)
1660
+    {
1661
+        msg(M_NONFATAL | M_ERRNO, "Failed to get system directory.");
1662
+    }
1663
+
1664
+    if ((wcslen(plugin_dir) == 0) && (wcslen(system_dir) == 0))
1665
+    {
1666
+        return false;
1667
+    }
1668
+
1669
+    WCHAR normalized_plugin_dir[MAX_PATH] = { 0 };
1670
+
1671
+    /* Normalize the plugin dir */
1672
+    if (wcslen(plugin_dir) > 0)
1673
+    {
1674
+        if (!GetFullPathNameW(plugin_dir, MAX_PATH, normalized_plugin_dir, NULL))
1675
+        {
1676
+            msg(M_NONFATAL | M_ERRNO, "Failed to normalize plugin dir.");
1677
+            return false;
1678
+        }
1679
+    }
1680
+
1681
+    /* Check if the plugin path resides within the plugin/install directory */
1682
+    if ((wcslen(normalized_plugin_dir) > 0) && (wcsnicmp(normalized_plugin_dir,
1683
+                                                         plugin_path, wcslen(normalized_plugin_dir)) == 0))
1684
+    {
1685
+        return true;
1686
+    }
1687
+
1688
+    /* Fallback to the system directory */
1689
+    return wcsnicmp(system_dir, plugin_path, wcslen(system_dir)) == 0;
1690
+}
1691
+
1636 1692
 #endif /* ifdef _WIN32 */
... ...
@@ -333,5 +333,32 @@ openvpn_swprintf(wchar_t *const str, const size_t size, const wchar_t *const for
333 333
 /* Sleep that can be interrupted by signals and exit event */
334 334
 void win32_sleep(const int n);
335 335
 
336
+/**
337
+ * @brief Fetches a registry value for OpenVPN registry key.
338
+ *
339
+ * @param key Registry value name to fetch.
340
+ * @param value Buffer to store the fetched string value.
341
+ * @param size Size of `value` buffer in bytes.
342
+ * @return `true` if successful, `false` otherwise.
343
+ */
344
+bool
345
+get_openvpn_reg_value(const WCHAR *key, WCHAR *value, DWORD size);
346
+
347
+/**
348
+ * @brief Checks if a plugin is located in a trusted directory.
349
+ *
350
+ * Verifies the plugin's path against a trusted directory, which is:
351
+ *
352
+ * - "plugin_dir" registry value or installation path, if the registry key is missing
353
+ * - system directory
354
+ *
355
+ * UNC paths are explicitly disallowed.
356
+ *
357
+ * @param plugin_path Normalized path to the plugin.
358
+ * @return \c true if the plugin is in a trusted directory and not a UNC path; \c false otherwise.
359
+ */
360
+bool
361
+plugin_in_trusted_dir(const WCHAR *plugin_path);
362
+
336 363
 #endif /* ifndef OPENVPN_WIN32_H */
337 364
 #endif /* ifdef _WIN32 */