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)
| ... | ... |
@@ -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 */ |