|
| 1 | +From 2a2607c6bd94ae22a937fd2adde7472d9a6d506c Mon Sep 17 00:00:00 2001 |
| 2 | +From: John Wolfe < [email protected]> |
| 3 | +Date: Mon, 5 May 2025 16:10:07 -0700 |
| 4 | +Subject: [PATCH] Validate user names and file paths |
| 5 | + |
| 6 | +Prevent usage of illegal characters in user names and file paths. |
| 7 | +Also, disallow unexpected symlinks in file paths. |
| 8 | + |
| 9 | +This patch contains changes to common source files not applicable |
| 10 | +to open-vm-tools. |
| 11 | + |
| 12 | +All files being updated should be consider to have the copyright to |
| 13 | +be updated to: |
| 14 | + |
| 15 | + * Copyright (c) XXXX-2025 Broadcom. All Rights Reserved. |
| 16 | + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. |
| 17 | + |
| 18 | +The 2025 Broadcom copyright information update is not part of this |
| 19 | +patch set to allow the patch to be easily applied to previous |
| 20 | +open-vm-tools source releases. |
| 21 | +--- |
| 22 | + vgauth/common/VGAuthUtil.c | 33 +++++++++ |
| 23 | + vgauth/common/VGAuthUtil.h | 2 + |
| 24 | + vgauth/common/prefs.h | 3 + |
| 25 | + vgauth/common/usercheck.c | 28 +++++-- |
| 26 | + vgauth/serviceImpl/alias.c | 74 ++++++++++++++++++- |
| 27 | + vgauth/serviceImpl/service.c | 27 +++++++ |
| 28 | + vgauth/serviceImpl/serviceInt.h | 1 + |
| 29 | + 7 files changed, 160 insertions(+), 8 deletions(-) |
| 30 | + |
| 31 | +diff --git a/vgauth/common/VGAuthUtil.c b/vgauth/common/VGAuthUtil.c |
| 32 | +index 76383c462..9c2adb8d0 100644 |
| 33 | +--- a/vgauth/common/VGAuthUtil.c |
| 34 | ++++ b/vgauth/common/VGAuthUtil.c |
| 35 | +@@ -309,3 +309,36 @@ Util_Assert(const char *cond, |
| 36 | + #endif |
| 37 | + g_assert(0); |
| 38 | + } |
| 39 | ++ |
| 40 | ++ |
| 41 | ++/* |
| 42 | ++ ****************************************************************************** |
| 43 | ++ * Util_Utf8CaseCmp -- */ /** |
| 44 | ++ * |
| 45 | ++ * Case insensitive comparison for utf8 strings which can have non-ascii |
| 46 | ++ * characters. |
| 47 | ++ * |
| 48 | ++ * @param[in] str1 Null terminated utf8 string. |
| 49 | ++ * @param[in] str2 Null terminated utf8 string. |
| 50 | ++ * |
| 51 | ++ ****************************************************************************** |
| 52 | ++ */ |
| 53 | ++ |
| 54 | ++int |
| 55 | ++Util_Utf8CaseCmp(const gchar *str1, |
| 56 | ++ const gchar *str2) |
| 57 | ++{ |
| 58 | ++ int ret; |
| 59 | ++ gchar *str1Case; |
| 60 | ++ gchar *str2Case; |
| 61 | ++ |
| 62 | ++ str1Case = g_utf8_casefold(str1, -1); |
| 63 | ++ str2Case = g_utf8_casefold(str2, -1); |
| 64 | ++ |
| 65 | ++ ret = g_strcmp0(str1Case, str2Case); |
| 66 | ++ |
| 67 | ++ g_free(str1Case); |
| 68 | ++ g_free(str2Case); |
| 69 | ++ |
| 70 | ++ return ret; |
| 71 | ++} |
| 72 | +diff --git a/vgauth/common/VGAuthUtil.h b/vgauth/common/VGAuthUtil.h |
| 73 | +index f7f3aa216..ef32a91da 100644 |
| 74 | +--- a/vgauth/common/VGAuthUtil.h |
| 75 | ++++ b/vgauth/common/VGAuthUtil.h |
| 76 | +@@ -105,4 +105,6 @@ gboolean Util_CheckExpiration(const GTimeVal *start, unsigned int duration); |
| 77 | + |
| 78 | + void Util_Assert(const char *cond, const char *file, int lineNum); |
| 79 | + |
| 80 | ++int Util_Utf8CaseCmp(const gchar *str1, const gchar *str2); |
| 81 | ++ |
| 82 | + #endif |
| 83 | +diff --git a/vgauth/common/prefs.h b/vgauth/common/prefs.h |
| 84 | +index ff116928c..7cddb3e17 100644 |
| 85 | +--- a/vgauth/common/prefs.h |
| 86 | ++++ b/vgauth/common/prefs.h |
| 87 | +@@ -165,6 +165,9 @@ msgCatalog = /etc/vmware-tools/vgauth/messages |
| 88 | + /** Where the localized version of the messages were installed. */ |
| 89 | + #define VGAUTH_PREF_LOCALIZATION_DIR "msgCatalog" |
| 90 | + |
| 91 | ++/** If symlinks or junctions are allowed in alias store file path */ |
| 92 | ++#define VGAUTH_PREF_ALLOW_SYMLINKS "allowSymlinks" |
| 93 | ++ |
| 94 | + /* |
| 95 | + * Pref values |
| 96 | + */ |
| 97 | +diff --git a/vgauth/common/usercheck.c b/vgauth/common/usercheck.c |
| 98 | +index 31eeb5a77..145f1f056 100644 |
| 99 | +--- a/vgauth/common/usercheck.c |
| 100 | ++++ b/vgauth/common/usercheck.c |
| 101 | +@@ -78,6 +78,8 @@ |
| 102 | + * Solaris as well, but that path is untested. |
| 103 | + */ |
| 104 | + |
| 105 | ++#define MAX_USER_NAME_LEN 256 |
| 106 | ++ |
| 107 | + /* |
| 108 | + * A single retry works for the LDAP case, but try more often in case NIS |
| 109 | + * or something else has a related issue. Note that a bad username/uid won't |
| 110 | +@@ -354,17 +356,29 @@ Usercheck_UsernameIsLegal(const gchar *userName) |
| 111 | + * |
| 112 | + */ |
| 113 | + size_t len; |
| 114 | +-#ifdef _WIN32 |
| 115 | +- // allow '\' in for Windows domain usernames |
| 116 | +- char *illegalChars = "<>/"; |
| 117 | +-#else |
| 118 | +- char *illegalChars = "\\<>/"; |
| 119 | +-#endif |
| 120 | ++ size_t i = 0; |
| 121 | ++ int backSlashCnt = 0; |
| 122 | ++ /* |
| 123 | ++ * As user names are used to generate its alias store file name/path, it |
| 124 | ++ * should not contain path traversal characters ('/' and '\'). |
| 125 | ++ */ |
| 126 | ++ char *illegalChars = "<>/\\"; |
| 127 | + |
| 128 | + len = strlen(userName); |
| 129 | +- if (strcspn(userName, illegalChars) != len) { |
| 130 | ++ if (len > MAX_USER_NAME_LEN) { |
| 131 | + return FALSE; |
| 132 | + } |
| 133 | ++ |
| 134 | ++ while ((i += strcspn(userName + i, illegalChars)) < len) { |
| 135 | ++ /* |
| 136 | ++ * One backward slash is allowed for domain\username separator. |
| 137 | ++ */ |
| 138 | ++ if (userName[i] != '\\' || ++backSlashCnt > 1) { |
| 139 | ++ return FALSE; |
| 140 | ++ } |
| 141 | ++ ++i; |
| 142 | ++ } |
| 143 | ++ |
| 144 | + return TRUE; |
| 145 | + } |
| 146 | + |
| 147 | +diff --git a/vgauth/serviceImpl/alias.c b/vgauth/serviceImpl/alias.c |
| 148 | +index b28351eea..687d1b373 100644 |
| 149 | +--- a/vgauth/serviceImpl/alias.c |
| 150 | ++++ b/vgauth/serviceImpl/alias.c |
| 151 | +@@ -41,6 +41,7 @@ |
| 152 | + #include "certverify.h" |
| 153 | + #include "VGAuthProto.h" |
| 154 | + #include "vmxlog.h" |
| 155 | ++#include "VGAuthUtil.h" |
| 156 | + |
| 157 | + // puts the identity store in an easy to find place |
| 158 | + #undef WIN_TEST_MODE |
| 159 | +@@ -66,6 +67,7 @@ |
| 160 | + #define ALIASSTORE_FILE_PREFIX "user-" |
| 161 | + #define ALIASSTORE_FILE_SUFFIX ".xml" |
| 162 | + |
| 163 | ++static gboolean allowSymlinks = FALSE; |
| 164 | + static gchar *aliasStoreRootDir = DEFAULT_ALIASSTORE_ROOT_DIR; |
| 165 | + |
| 166 | + #ifdef _WIN32 |
| 167 | +@@ -252,6 +254,12 @@ mapping file layout: |
| 168 | + |
| 169 | + */ |
| 170 | + |
| 171 | ++#ifdef _WIN32 |
| 172 | ++#define ISPATHSEP(c) ((c) == '\\' || (c) == '/') |
| 173 | ++#else |
| 174 | ++#define ISPATHSEP(c) ((c) == '/') |
| 175 | ++#endif |
| 176 | ++ |
| 177 | + |
| 178 | + /* |
| 179 | + ****************************************************************************** |
| 180 | +@@ -466,6 +474,7 @@ ServiceLoadFileContentsWin(const gchar *fileName, |
| 181 | + gunichar2 *fileNameW = NULL; |
| 182 | + BOOL ok; |
| 183 | + DWORD bytesRead; |
| 184 | ++ gchar *realPath = NULL; |
| 185 | + |
| 186 | + *fileSize = 0; |
| 187 | + *contents = NULL; |
| 188 | +@@ -622,6 +631,22 @@ ServiceLoadFileContentsWin(const gchar *fileName, |
| 189 | + goto done; |
| 190 | + } |
| 191 | + |
| 192 | ++ if (!allowSymlinks) { |
| 193 | ++ /* |
| 194 | ++ * Check if fileName is real path. |
| 195 | ++ */ |
| 196 | ++ if ((realPath = ServiceFileGetPathByHandle(hFile)) == NULL) { |
| 197 | ++ err = VGAUTH_E_FAIL; |
| 198 | ++ goto done; |
| 199 | ++ } |
| 200 | ++ if (Util_Utf8CaseCmp(realPath, fileName) != 0) { |
| 201 | ++ Warning("%s: Real path (%s) is not same as file path (%s)\n", |
| 202 | ++ __FUNCTION__, realPath, fileName); |
| 203 | ++ err = VGAUTH_E_FAIL; |
| 204 | ++ goto done; |
| 205 | ++ } |
| 206 | ++ } |
| 207 | ++ |
| 208 | + /* |
| 209 | + * Now finally read the contents. |
| 210 | + */ |
| 211 | +@@ -650,6 +675,7 @@ done: |
| 212 | + CloseHandle(hFile); |
| 213 | + } |
| 214 | + g_free(fileNameW); |
| 215 | ++ g_free(realPath); |
| 216 | + |
| 217 | + return err; |
| 218 | + } |
| 219 | +@@ -672,6 +698,7 @@ ServiceLoadFileContentsPosix(const gchar *fileName, |
| 220 | + gchar *buf; |
| 221 | + gchar *bp; |
| 222 | + int fd = -1; |
| 223 | ++ gchar realPath[PATH_MAX] = { 0 }; |
| 224 | + |
| 225 | + *fileSize = 0; |
| 226 | + *contents = NULL; |
| 227 | +@@ -817,6 +844,23 @@ ServiceLoadFileContentsPosix(const gchar *fileName, |
| 228 | + goto done; |
| 229 | + } |
| 230 | + |
| 231 | ++ if (!allowSymlinks) { |
| 232 | ++ /* |
| 233 | ++ * Check if fileName is real path. |
| 234 | ++ */ |
| 235 | ++ if (realpath(fileName, realPath) == NULL) { |
| 236 | ++ Warning("%s: realpath() failed. errno (%d)\n", __FUNCTION__, errno); |
| 237 | ++ err = VGAUTH_E_FAIL; |
| 238 | ++ goto done; |
| 239 | ++ } |
| 240 | ++ if (g_strcmp0(realPath, fileName) != 0) { |
| 241 | ++ Warning("%s: Real path (%s) is not same as file path (%s)\n", |
| 242 | ++ __FUNCTION__, realPath, fileName); |
| 243 | ++ err = VGAUTH_E_FAIL; |
| 244 | ++ goto done; |
| 245 | ++ } |
| 246 | ++ } |
| 247 | ++ |
| 248 | + /* |
| 249 | + * All sanity checks passed; read the bits. |
| 250 | + */ |
| 251 | +@@ -2803,8 +2847,13 @@ ServiceAliasRemoveAlias(const gchar *reqUserName, |
| 252 | + |
| 253 | + /* |
| 254 | + * We don't verify the user exists in a Remove operation, to allow |
| 255 | +- * cleanup of deleted user's stores. |
| 256 | ++ * cleanup of deleted user's stores, but we do check whether the |
| 257 | ++ * user name is legal or not. |
| 258 | + */ |
| 259 | ++ if (!Usercheck_UsernameIsLegal(userName)) { |
| 260 | ++ Warning("%s: Illegal user name '%s'\n", __FUNCTION__, userName); |
| 261 | ++ return VGAUTH_E_FAIL; |
| 262 | ++ } |
| 263 | + |
| 264 | + if (!CertVerify_IsWellFormedPEMCert(pemCert)) { |
| 265 | + return VGAUTH_E_INVALID_CERTIFICATE; |
| 266 | +@@ -3036,6 +3085,16 @@ ServiceAliasQueryAliases(const gchar *userName, |
| 267 | + } |
| 268 | + #endif |
| 269 | + |
| 270 | ++ /* |
| 271 | ++ * We don't verify the user exists in a Query operation to allow |
| 272 | ++ * cleaning up after a deleted user, but we do check whether the |
| 273 | ++ * user name is legal or not. |
| 274 | ++ */ |
| 275 | ++ if (!Usercheck_UsernameIsLegal(userName)) { |
| 276 | ++ Warning("%s: Illegal user name '%s'\n", __FUNCTION__, userName); |
| 277 | ++ return VGAUTH_E_FAIL; |
| 278 | ++ } |
| 279 | ++ |
| 280 | + err = AliasLoadAliases(userName, num, aList); |
| 281 | + if (VGAUTH_E_OK != err) { |
| 282 | + Warning("%s: failed to load Aliases for '%s'\n", __FUNCTION__, userName); |
| 283 | +@@ -3294,6 +3353,7 @@ ServiceAliasInitAliasStore(void) |
| 284 | + VGAuthError err = VGAUTH_E_OK; |
| 285 | + gboolean saveBadDir = FALSE; |
| 286 | + char *defaultDir = NULL; |
| 287 | ++ size_t len; |
| 288 | + |
| 289 | + #ifdef _WIN32 |
| 290 | + { |
| 291 | +@@ -3324,6 +3384,10 @@ ServiceAliasInitAliasStore(void) |
| 292 | + defaultDir = g_strdup(DEFAULT_ALIASSTORE_ROOT_DIR); |
| 293 | + #endif |
| 294 | + |
| 295 | ++ allowSymlinks = Pref_GetBool(gPrefs, |
| 296 | ++ VGAUTH_PREF_ALLOW_SYMLINKS, |
| 297 | ++ VGAUTH_PREF_GROUP_NAME_SERVICE, |
| 298 | ++ FALSE); |
| 299 | + /* |
| 300 | + * Find the alias store directory. This allows an installer to put |
| 301 | + * it somewhere else if necessary. |
| 302 | +@@ -3337,6 +3401,14 @@ ServiceAliasInitAliasStore(void) |
| 303 | + VGAUTH_PREF_GROUP_NAME_SERVICE, |
| 304 | + defaultDir); |
| 305 | + |
| 306 | ++ /* |
| 307 | ++ * Remove the trailing separator if any from aliasStoreRootDir path. |
| 308 | ++ */ |
| 309 | ++ len = strlen(aliasStoreRootDir); |
| 310 | ++ if (ISPATHSEP(aliasStoreRootDir[len - 1])) { |
| 311 | ++ aliasStoreRootDir[len - 1] = '\0'; |
| 312 | ++ } |
| 313 | ++ |
| 314 | + Log("Using '%s' for alias store root directory\n", aliasStoreRootDir); |
| 315 | + |
| 316 | + g_free(defaultDir); |
| 317 | +diff --git a/vgauth/serviceImpl/service.c b/vgauth/serviceImpl/service.c |
| 318 | +index d4716526c..e053ed0fa 100644 |
| 319 | +--- a/vgauth/serviceImpl/service.c |
| 320 | ++++ b/vgauth/serviceImpl/service.c |
| 321 | +@@ -28,6 +28,7 @@ |
| 322 | + #include "VGAuthUtil.h" |
| 323 | + #ifdef _WIN32 |
| 324 | + #include "winUtil.h" |
| 325 | ++#include <glib.h> |
| 326 | + #endif |
| 327 | + |
| 328 | + static ServiceStartListeningForIOFunc startListeningIOFunc = NULL; |
| 329 | +@@ -283,9 +284,35 @@ static gchar * |
| 330 | + ServiceUserNameToPipeName(const char *userName) |
| 331 | + { |
| 332 | + gchar *escapedName = ServiceEncodeUserName(userName); |
| 333 | ++#ifdef _WIN32 |
| 334 | ++ /* |
| 335 | ++ * Adding below pragma only in windows to suppress the compile time warning |
| 336 | ++ * about unavailability of g_uuid_string_random() since compiler flag |
| 337 | ++ * GLIB_VERSION_MAX_ALLOWED is defined to GLIB_VERSION_2_34. |
| 338 | ++ * TODO: Remove below pragma when GLIB_VERSION_MAX_ALLOWED is bumped up to |
| 339 | ++ * or greater than GLIB_VERSION_2_52. |
| 340 | ++ */ |
| 341 | ++#pragma warning(suppress : 4996) |
| 342 | ++ gchar *uuidStr = g_uuid_string_random(); |
| 343 | ++ /* |
| 344 | ++ * Add a unique suffix to avoid a name collision with an existing named pipe |
| 345 | ++ * created by someone else (intentionally or by accident). |
| 346 | ++ * This is not needed for Linux; name collisions on sockets are already |
| 347 | ++ * avoided there since (1) file system paths to VGAuthService sockets are in |
| 348 | ++ * a directory that is writable only by root and (2) VGAuthService unlinks a |
| 349 | ++ * socket path before binding it to a newly created socket. |
| 350 | ++ */ |
| 351 | ++ gchar *pipeName = g_strdup_printf("%s-%s-%s", |
| 352 | ++ SERVICE_PUBLIC_PIPE_NAME, |
| 353 | ++ escapedName, |
| 354 | ++ uuidStr); |
| 355 | ++ |
| 356 | ++ g_free(uuidStr); |
| 357 | ++#else |
| 358 | + gchar *pipeName = g_strdup_printf("%s-%s", |
| 359 | + SERVICE_PUBLIC_PIPE_NAME, |
| 360 | + escapedName); |
| 361 | ++#endif |
| 362 | + |
| 363 | + g_free(escapedName); |
| 364 | + return pipeName; |
| 365 | +diff --git a/vgauth/serviceImpl/serviceInt.h b/vgauth/serviceImpl/serviceInt.h |
| 366 | +index ef49f42c2..c37f42fa6 100644 |
| 367 | +--- a/vgauth/serviceImpl/serviceInt.h |
| 368 | ++++ b/vgauth/serviceImpl/serviceInt.h |
| 369 | +@@ -441,6 +441,7 @@ VGAuthError ServiceFileVerifyAdminGroupOwnedByHandle(const HANDLE hFile); |
| 370 | + VGAuthError ServiceFileVerifyEveryoneReadableByHandle(const HANDLE hFile); |
| 371 | + VGAuthError ServiceFileVerifyUserAccessByHandle(const HANDLE hFile, |
| 372 | + const char *userName); |
| 373 | ++gchar *ServiceFileGetPathByHandle(HANDLE hFile); |
| 374 | + #else |
| 375 | + VGAuthError ServiceFileVerifyFileOwnerAndPerms(const char *fileName, |
| 376 | + const char *userName, |
| 377 | +-- |
| 378 | +2.43.5 |
| 379 | + |
0 commit comments