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