|
| 1 | +From 7874e572b5aac5a418551dc5e3935c1e74bf6f1f Mon Sep 17 00:00:00 2001 |
| 2 | +From: John Wolfe <john.wolfe@broadcom.com> |
| 3 | +Date: Mon, 5 May 2025 15:58:03 -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 | 23 +++++- |
| 26 | + vgauth/serviceImpl/alias.c | 74 ++++++++++++++++++- |
| 27 | + vgauth/serviceImpl/service.c | 27 +++++++ |
| 28 | + vgauth/serviceImpl/serviceInt.h | 1 + |
| 29 | + 7 files changed, 160 insertions(+), 3 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 6c58f3f4b..3299eb26c 100644 |
| 85 | +--- a/vgauth/common/prefs.h |
| 86 | ++++ b/vgauth/common/prefs.h |
| 87 | +@@ -167,6 +167,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 3beede2e8..340aa0411 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,12 +356,29 @@ Usercheck_UsernameIsLegal(const gchar *userName) |
| 111 | + * restricted list for local usernames. |
| 112 | + */ |
| 113 | + size_t len; |
| 114 | +- char *illegalChars = "<>/"; |
| 115 | ++ size_t i = 0; |
| 116 | ++ int backSlashCnt = 0; |
| 117 | ++ /* |
| 118 | ++ * As user names are used to generate its alias store file name/path, it |
| 119 | ++ * should not contain path traversal characters ('/' and '\'). |
| 120 | ++ */ |
| 121 | ++ char *illegalChars = "<>/\\"; |
| 122 | + |
| 123 | + len = strlen(userName); |
| 124 | +- if (strcspn(userName, illegalChars) != len) { |
| 125 | ++ if (len > MAX_USER_NAME_LEN) { |
| 126 | + return FALSE; |
| 127 | + } |
| 128 | ++ |
| 129 | ++ while ((i += strcspn(userName + i, illegalChars)) < len) { |
| 130 | ++ /* |
| 131 | ++ * One backward slash is allowed for domain\username separator. |
| 132 | ++ */ |
| 133 | ++ if (userName[i] != '\\' || ++backSlashCnt > 1) { |
| 134 | ++ return FALSE; |
| 135 | ++ } |
| 136 | ++ ++i; |
| 137 | ++ } |
| 138 | ++ |
| 139 | + return TRUE; |
| 140 | + } |
| 141 | + |
| 142 | +diff --git a/vgauth/serviceImpl/alias.c b/vgauth/serviceImpl/alias.c |
| 143 | +index 4e170202c..c7040ebff 100644 |
| 144 | +--- a/vgauth/serviceImpl/alias.c |
| 145 | ++++ b/vgauth/serviceImpl/alias.c |
| 146 | +@@ -41,6 +41,7 @@ |
| 147 | + #include "certverify.h" |
| 148 | + #include "VGAuthProto.h" |
| 149 | + #include "vmxlog.h" |
| 150 | ++#include "VGAuthUtil.h" |
| 151 | + |
| 152 | + // puts the identity store in an easy to find place |
| 153 | + #undef WIN_TEST_MODE |
| 154 | +@@ -66,6 +67,7 @@ |
| 155 | + #define ALIASSTORE_FILE_PREFIX "user-" |
| 156 | + #define ALIASSTORE_FILE_SUFFIX ".xml" |
| 157 | + |
| 158 | ++static gboolean allowSymlinks = FALSE; |
| 159 | + static gchar *aliasStoreRootDir = DEFAULT_ALIASSTORE_ROOT_DIR; |
| 160 | + |
| 161 | + #ifdef _WIN32 |
| 162 | +@@ -252,6 +254,12 @@ mapping file layout: |
| 163 | + |
| 164 | + */ |
| 165 | + |
| 166 | ++#ifdef _WIN32 |
| 167 | ++#define ISPATHSEP(c) ((c) == '\\' || (c) == '/') |
| 168 | ++#else |
| 169 | ++#define ISPATHSEP(c) ((c) == '/') |
| 170 | ++#endif |
| 171 | ++ |
| 172 | + |
| 173 | + /* |
| 174 | + ****************************************************************************** |
| 175 | +@@ -466,6 +474,7 @@ ServiceLoadFileContentsWin(const gchar *fileName, |
| 176 | + gunichar2 *fileNameW = NULL; |
| 177 | + BOOL ok; |
| 178 | + DWORD bytesRead; |
| 179 | ++ gchar *realPath = NULL; |
| 180 | + |
| 181 | + *fileSize = 0; |
| 182 | + *contents = NULL; |
| 183 | +@@ -622,6 +631,22 @@ ServiceLoadFileContentsWin(const gchar *fileName, |
| 184 | + goto done; |
| 185 | + } |
| 186 | + |
| 187 | ++ if (!allowSymlinks) { |
| 188 | ++ /* |
| 189 | ++ * Check if fileName is real path. |
| 190 | ++ */ |
| 191 | ++ if ((realPath = ServiceFileGetPathByHandle(hFile)) == NULL) { |
| 192 | ++ err = VGAUTH_E_FAIL; |
| 193 | ++ goto done; |
| 194 | ++ } |
| 195 | ++ if (Util_Utf8CaseCmp(realPath, fileName) != 0) { |
| 196 | ++ Warning("%s: Real path (%s) is not same as file path (%s)\n", |
| 197 | ++ __FUNCTION__, realPath, fileName); |
| 198 | ++ err = VGAUTH_E_FAIL; |
| 199 | ++ goto done; |
| 200 | ++ } |
| 201 | ++ } |
| 202 | ++ |
| 203 | + /* |
| 204 | + * Now finally read the contents. |
| 205 | + */ |
| 206 | +@@ -650,6 +675,7 @@ done: |
| 207 | + CloseHandle(hFile); |
| 208 | + } |
| 209 | + g_free(fileNameW); |
| 210 | ++ g_free(realPath); |
| 211 | + |
| 212 | + return err; |
| 213 | + } |
| 214 | +@@ -672,6 +698,7 @@ ServiceLoadFileContentsPosix(const gchar *fileName, |
| 215 | + gchar *buf; |
| 216 | + gchar *bp; |
| 217 | + int fd = -1; |
| 218 | ++ gchar realPath[PATH_MAX] = { 0 }; |
| 219 | + |
| 220 | + *fileSize = 0; |
| 221 | + *contents = NULL; |
| 222 | +@@ -817,6 +844,23 @@ ServiceLoadFileContentsPosix(const gchar *fileName, |
| 223 | + goto done; |
| 224 | + } |
| 225 | + |
| 226 | ++ if (!allowSymlinks) { |
| 227 | ++ /* |
| 228 | ++ * Check if fileName is real path. |
| 229 | ++ */ |
| 230 | ++ if (realpath(fileName, realPath) == NULL) { |
| 231 | ++ Warning("%s: realpath() failed. errno (%d)\n", __FUNCTION__, errno); |
| 232 | ++ err = VGAUTH_E_FAIL; |
| 233 | ++ goto done; |
| 234 | ++ } |
| 235 | ++ if (g_strcmp0(realPath, fileName) != 0) { |
| 236 | ++ Warning("%s: Real path (%s) is not same as file path (%s)\n", |
| 237 | ++ __FUNCTION__, realPath, fileName); |
| 238 | ++ err = VGAUTH_E_FAIL; |
| 239 | ++ goto done; |
| 240 | ++ } |
| 241 | ++ } |
| 242 | ++ |
| 243 | + /* |
| 244 | + * All confidence checks passed; read the bits. |
| 245 | + */ |
| 246 | +@@ -2803,8 +2847,13 @@ ServiceAliasRemoveAlias(const gchar *reqUserName, |
| 247 | + |
| 248 | + /* |
| 249 | + * We don't verify the user exists in a Remove operation, to allow |
| 250 | +- * cleanup of deleted user's stores. |
| 251 | ++ * cleanup of deleted user's stores, but we do check whether the |
| 252 | ++ * user name is legal or not. |
| 253 | + */ |
| 254 | ++ if (!Usercheck_UsernameIsLegal(userName)) { |
| 255 | ++ Warning("%s: Illegal user name '%s'\n", __FUNCTION__, userName); |
| 256 | ++ return VGAUTH_E_FAIL; |
| 257 | ++ } |
| 258 | + |
| 259 | + if (!CertVerify_IsWellFormedPEMCert(pemCert)) { |
| 260 | + return VGAUTH_E_INVALID_CERTIFICATE; |
| 261 | +@@ -3036,6 +3085,16 @@ ServiceAliasQueryAliases(const gchar *userName, |
| 262 | + } |
| 263 | + #endif |
| 264 | + |
| 265 | ++ /* |
| 266 | ++ * We don't verify the user exists in a Query operation to allow |
| 267 | ++ * cleaning up after a deleted user, but we do check whether the |
| 268 | ++ * user name is legal or not. |
| 269 | ++ */ |
| 270 | ++ if (!Usercheck_UsernameIsLegal(userName)) { |
| 271 | ++ Warning("%s: Illegal user name '%s'\n", __FUNCTION__, userName); |
| 272 | ++ return VGAUTH_E_FAIL; |
| 273 | ++ } |
| 274 | ++ |
| 275 | + err = AliasLoadAliases(userName, num, aList); |
| 276 | + if (VGAUTH_E_OK != err) { |
| 277 | + Warning("%s: failed to load Aliases for '%s'\n", __FUNCTION__, userName); |
| 278 | +@@ -3294,6 +3353,7 @@ ServiceAliasInitAliasStore(void) |
| 279 | + VGAuthError err = VGAUTH_E_OK; |
| 280 | + gboolean saveBadDir = FALSE; |
| 281 | + char *defaultDir = NULL; |
| 282 | ++ size_t len; |
| 283 | + |
| 284 | + #ifdef _WIN32 |
| 285 | + { |
| 286 | +@@ -3324,6 +3384,10 @@ ServiceAliasInitAliasStore(void) |
| 287 | + defaultDir = g_strdup(DEFAULT_ALIASSTORE_ROOT_DIR); |
| 288 | + #endif |
| 289 | + |
| 290 | ++ allowSymlinks = Pref_GetBool(gPrefs, |
| 291 | ++ VGAUTH_PREF_ALLOW_SYMLINKS, |
| 292 | ++ VGAUTH_PREF_GROUP_NAME_SERVICE, |
| 293 | ++ FALSE); |
| 294 | + /* |
| 295 | + * Find the alias store directory. This allows an installer to put |
| 296 | + * it somewhere else if necessary. |
| 297 | +@@ -3337,6 +3401,14 @@ ServiceAliasInitAliasStore(void) |
| 298 | + VGAUTH_PREF_GROUP_NAME_SERVICE, |
| 299 | + defaultDir); |
| 300 | + |
| 301 | ++ /* |
| 302 | ++ * Remove the trailing separator if any from aliasStoreRootDir path. |
| 303 | ++ */ |
| 304 | ++ len = strlen(aliasStoreRootDir); |
| 305 | ++ if (ISPATHSEP(aliasStoreRootDir[len - 1])) { |
| 306 | ++ aliasStoreRootDir[len - 1] = '\0'; |
| 307 | ++ } |
| 308 | ++ |
| 309 | + Log("Using '%s' for alias store root directory\n", aliasStoreRootDir); |
| 310 | + |
| 311 | + g_free(defaultDir); |
| 312 | +diff --git a/vgauth/serviceImpl/service.c b/vgauth/serviceImpl/service.c |
| 313 | +index d4716526c..e053ed0fa 100644 |
| 314 | +--- a/vgauth/serviceImpl/service.c |
| 315 | ++++ b/vgauth/serviceImpl/service.c |
| 316 | +@@ -28,6 +28,7 @@ |
| 317 | + #include "VGAuthUtil.h" |
| 318 | + #ifdef _WIN32 |
| 319 | + #include "winUtil.h" |
| 320 | ++#include <glib.h> |
| 321 | + #endif |
| 322 | + |
| 323 | + static ServiceStartListeningForIOFunc startListeningIOFunc = NULL; |
| 324 | +@@ -283,9 +284,35 @@ static gchar * |
| 325 | + ServiceUserNameToPipeName(const char *userName) |
| 326 | + { |
| 327 | + gchar *escapedName = ServiceEncodeUserName(userName); |
| 328 | ++#ifdef _WIN32 |
| 329 | ++ /* |
| 330 | ++ * Adding below pragma only in windows to suppress the compile time warning |
| 331 | ++ * about unavailability of g_uuid_string_random() since compiler flag |
| 332 | ++ * GLIB_VERSION_MAX_ALLOWED is defined to GLIB_VERSION_2_34. |
| 333 | ++ * TODO: Remove below pragma when GLIB_VERSION_MAX_ALLOWED is bumped up to |
| 334 | ++ * or greater than GLIB_VERSION_2_52. |
| 335 | ++ */ |
| 336 | ++#pragma warning(suppress : 4996) |
| 337 | ++ gchar *uuidStr = g_uuid_string_random(); |
| 338 | ++ /* |
| 339 | ++ * Add a unique suffix to avoid a name collision with an existing named pipe |
| 340 | ++ * created by someone else (intentionally or by accident). |
| 341 | ++ * This is not needed for Linux; name collisions on sockets are already |
| 342 | ++ * avoided there since (1) file system paths to VGAuthService sockets are in |
| 343 | ++ * a directory that is writable only by root and (2) VGAuthService unlinks a |
| 344 | ++ * socket path before binding it to a newly created socket. |
| 345 | ++ */ |
| 346 | ++ gchar *pipeName = g_strdup_printf("%s-%s-%s", |
| 347 | ++ SERVICE_PUBLIC_PIPE_NAME, |
| 348 | ++ escapedName, |
| 349 | ++ uuidStr); |
| 350 | ++ |
| 351 | ++ g_free(uuidStr); |
| 352 | ++#else |
| 353 | + gchar *pipeName = g_strdup_printf("%s-%s", |
| 354 | + SERVICE_PUBLIC_PIPE_NAME, |
| 355 | + escapedName); |
| 356 | ++#endif |
| 357 | + |
| 358 | + g_free(escapedName); |
| 359 | + return pipeName; |
| 360 | +diff --git a/vgauth/serviceImpl/serviceInt.h b/vgauth/serviceImpl/serviceInt.h |
| 361 | +index 5f420192b..f4f88547d 100644 |
| 362 | +--- a/vgauth/serviceImpl/serviceInt.h |
| 363 | ++++ b/vgauth/serviceImpl/serviceInt.h |
| 364 | +@@ -441,6 +441,7 @@ VGAuthError ServiceFileVerifyAdminGroupOwnedByHandle(const HANDLE hFile); |
| 365 | + VGAuthError ServiceFileVerifyEveryoneReadableByHandle(const HANDLE hFile); |
| 366 | + VGAuthError ServiceFileVerifyUserAccessByHandle(const HANDLE hFile, |
| 367 | + const char *userName); |
| 368 | ++gchar *ServiceFileGetPathByHandle(HANDLE hFile); |
| 369 | + #else |
| 370 | + VGAuthError ServiceFileVerifyFileOwnerAndPerms(const char *fileName, |
| 371 | + const char *userName, |
| 372 | +-- |
| 373 | +2.43.5 |
| 374 | + |
0 commit comments