CVE-2025-2817
Overview
Background
- `update_elevated.lock`
- The old mechanism for an updater instance to decide whether it is the elevated child: an unelevated updater created a
update_elevated.lockfile (withFILE_FLAG_DELETE_ON_CLOSE) before spawning the elevated updater; the spawned process inferred elevation from the presence of that file viaGetFileAttributesW. - `gIsElevated` / `gUseSecureOutputPath`
- Globals that gate elevated behavior and the use of secure output (status) file paths.
- `EXIT_WHEN_ELEVATED`
- A macro that closes a handle and, if elevated, returns a code to prevent the elevated process from launching the callback application.
Root Cause Analysis
The updater determined its own elevation state from filesystem state rather than from the OS security token. gIsElevated was set by GetFileAttributesW(elevatedLockFilePath) != INVALID_FILE_ATTRIBUTES and gUseSecureOutputPath by whether update_elevated.lock could be removed. That lock file lives under the patch directory, a path influenced by the (non-privileged) user, and its existence/locking is observable and manipulable by a medium-integrity user process. By injecting into the user-privileged updater or otherwise manipulating the lock file’s presence and file-locking (e.g. holding or pre-creating it, or defeating the NS_tremove), an attacker could make the SYSTEM/elevated updater mis-decide its elevation and secure-output behavior — effectively steering SYSTEM-level file operations along user-controlled paths, i.e. privilege escalation (the advisory’s CWE-22/elevation framing stems from this path control). The decision crossed a trust boundary: a privileged process trusting a signal a lower-privileged user could forge.
The patch replaces the file-based signal with an authoritative token-membership check. NS_main now calls mozilla::UserHasAdminPrivileges() and mozilla::UserIsLocalSystem() (new helpers in WinTokenUtils.cpp built on the generalized IsMemberOfSidType wrapping CheckTokenMembership for WinBuiltinAdministratorsSid / WinLocalSystemSid) and sets gIsElevated = isAdmin || isLocalSystem, with gUseSecureOutputPath = sUsingService || gIsElevated. All creation, deletion, and handle management of update_elevated.lock (the elevatedLockFilePath buffer, CreateFileW(... FILE_FLAG_DELETE_ON_CLOSE), the CloseHandle(elevatedFileHandle) calls, and the lock-file parameter to LaunchCallbackAndPostProcessApps/EXIT_WHEN_ELEVATED) is removed; EXIT_WHEN_ELEVATED now keys off gIsElevated. The updater no longer trusts a user-writable file to know whether it is privileged.
Attack Path
- Foothold The attacker runs code at medium integrity in the user’s context, and/or injects into the user-privileged (launcher) updater process (per the advisory’s “injecting code into the user-privileged process”).
- Manipulate the signal
They manipulate the
update_elevated.lockfile’s existence or file-locking behavior under the patch directory that the elevated updater inspects. - Mis-decision
The elevated/SYSTEM updater derives
gIsElevated/gUseSecureOutputPathfrom that forged filesystem state (inferred exact primitive; the diff shows the signal being replaced). - Redirected privileged op SYSTEM-level file operations (output/status files, callback handling) proceed along user-controlled paths rather than the secure path.
- Primitive Local privilege escalation: a non-privileged user induces SYSTEM-level file operations on attacker-chosen paths.
Impact Assessment
PR:L, local vector, CVSS 8.8) and the ability to influence the lock file / inject into the user process; the gain is SYSTEM-level file operations on user-controlled paths, i.e. full privilege escalation. The fix is a hard authorization check, so no residual memory-safety primitive is implied.Changed Functions
| Function | Change | Notes |
|---|---|---|
iftoolkit/mozapps/update/updater/updater.cpp |
modified | |
iftoolkit/xre/WinTokenUtils.cpp |
modified |
Files Changed
toolkit/mozapps/update/updater/updater-common.buildtoolkit/mozapps/update/updater/updater.cpptoolkit/xre/WinTokenUtils.cpptoolkit/xre/WinTokenUtils.h
Audit Directions
- Filesystem-as-privilege-signalAudit the updater and Maintenance Service for any other decision (secure output path, callback launching, service fallback) derived from presence/locking of files in user-writable directories.
- Trust boundaries in elevated helpersReview other privileged helper processes that infer caller/self state from IPC or filesystem artifacts a lower-integrity process can forge.
- Path control under SYSTEMExamine where
gUseSecureOutputPath/status-file paths are constructed to ensure no SYSTEM write targets a user-controllable path.
Patch
diff --git a/toolkit/mozapps/update/updater/updater-common.build b/toolkit/mozapps/update/updater/updater-common.build
index 6c6d0adf6f5..600c03de7cd 100644
--- a/toolkit/mozapps/update/updater/updater-common.build
+++ b/toolkit/mozapps/update/updater/updater-common.build
@@ -36,6 +36,7 @@ if CONFIG["MOZ_VERIFY_MAR_SIGNATURE"]:
if CONFIG["OS_ARCH"] == "WINNT":
have_progressui = 1
srcs += [
+ "/toolkit/xre/WinTokenUtils.cpp",
"loaddlls.cpp",
"progressui_win.cpp",
]
diff --git a/toolkit/mozapps/update/updater/updater.cpp b/toolkit/mozapps/update/updater/updater.cpp
index 89fa8387176..f490a8a4524 100644
--- a/toolkit/mozapps/update/updater/updater.cpp
+++ b/toolkit/mozapps/update/updater/updater.cpp
@@ -55,6 +55,7 @@
#ifdef XP_WIN
# include "mozilla/Maybe.h"
# include "mozilla/WinHeaderOnlyUtils.h"
+# include "mozilla/WinTokenUtils.h"
# include <climits>
#endif // XP_WIN
@@ -130,14 +131,14 @@ BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath,
// Closes the handle if valid and if the updater is elevated returns with the
// return code specified. This prevents multiple launches of the callback
// application by preventing the elevated process from launching the callback.
-# define EXIT_WHEN_ELEVATED(path, handle, retCode) \
- { \
- if (handle != INVALID_HANDLE_VALUE) { \
- CloseHandle(handle); \
- } \
- if (NS_tremove(path) && errno != ENOENT) { \
- return retCode; \
- } \
+# define EXIT_WHEN_ELEVATED(handle, retCode) \
+ { \
+ if (handle != INVALID_HANDLE_VALUE) { \
+ CloseHandle(handle); \
+ } \
+ if (gIsElevated) { \
+ return retCode; \
+ } \
}
#endif
@@ -2881,7 +2882,6 @@ int LaunchCallbackAndPostProcessApps(int argc, NS_tchar** argv,
int callbackIndex
#ifdef XP_WIN
,
- const WCHAR* elevatedLockFilePath,
HANDLE updateLockFileHandle
#elif XP_MACOSX
,
@@ -2940,7 +2940,7 @@ int LaunchCallbackAndPostProcessApps(int argc, NS_tchar** argv,
LOG(("Not launching Windows post update process because !gSucceeded"));
}
- EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 0);
+ EXIT_WHEN_ELEVATED(updateLockFileHandle, 0);
#elif XP_MACOSX
if (!gIsElevated) {
if (gSucceeded) {
@@ -3123,18 +3123,31 @@ int NS_main(int argc, NS_tchar** argv) {
gPatchDirPath[MAXPATHLEN - 1] = NS_T('\0');
#ifdef XP_WIN
- NS_tchar elevatedLockFilePath[MAXPATHLEN] = {NS_T('\0')};
- NS_tsnprintf(elevatedLockFilePath,
- sizeof(elevatedLockFilePath) / sizeof(elevatedLockFilePath[0]),
- NS_T("%s\\update_elevated.lock"), gPatchDirPath);
- gUseSecureOutputPath =
- sUsingService || (NS_tremove(elevatedLockFilePath) && errno != ENOENT);
-
- // Even if a file has no sharing access, you can still get its attributes
- // If we are running elevated, this file will exist, having been opened by
- // the unelevated updater that started this one.
- gIsElevated =
- GetFileAttributesW(elevatedLockFilePath) != INVALID_FILE_ATTRIBUTES;
+ auto isAdmin = mozilla::UserHasAdminPrivileges();
+ if (isAdmin.isErr()) {
+ fprintf(stderr,
+ "Failed to query if the current process has admin privileges.\n");
+ return 1;
+ }
+ auto isLocalSystem = mozilla::UserIsLocalSystem();
+ if (isLocalSystem.isErr()) {
+ fprintf(
+ stderr,
+ "Failed to query if the current process has LocalSystem privileges.\n");
+ return 1;
+ }
+
+ // While is it technically redundant to check LocalSystem in addition to Admin
+ // given the former contains privileges of the latter, we have opt to verify
+ // both. A few reasons for this decision include the off chance that the
+ // Windows security model changes in the future and weird system setups where
+ // someone has modified the group lists in surprising ways.
+ //
+ // We use this to detect if we were launched from the Maintenance Service
+ // under LocalSystem or UAC under the user's account, and therefore can
+ // proceed with an install to `Program Files` or `Program Files(x86)`.
+ gIsElevated = isAdmin.unwrap() || isLocalSystem.unwrap();
+ gUseSecureOutputPath = sUsingService || gIsElevated;
#elif defined(XP_MACOSX)
// This is only ever true on macOS and Windows. We don't currently have a
// way of elevating on other platforms.
@@ -3587,26 +3600,9 @@ int NS_main(int argc, NS_tchar** argv) {
(noServiceFallback || forceServiceFallback))) {
LOG(("Can't open lock file - seems like we need elevation"));
- HANDLE elevatedFileHandle;
- if (NS_tremove(elevatedLockFilePath) && errno != ENOENT) {
- LOG(("Unable to create elevated lock file! Exiting"));
- output_finish();
- return 1;
- }
-
- elevatedFileHandle = CreateFileW(
- elevatedLockFilePath, GENERIC_READ | GENERIC_WRITE, 0, nullptr,
- OPEN_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, nullptr);
- if (elevatedFileHandle == INVALID_HANDLE_VALUE) {
- LOG(("Unable to create elevated lock file! Exiting"));
- output_finish();
- return 1;
- }
-
auto cmdLine = mozilla::MakeCommandLine(argc - 1, argv + 1);
if (!cmdLine) {
LOG(("Failed to make command line! Exiting"));
- CloseHandle(elevatedFileHandle);
output_finish();
return 1;
}
@@ -3846,17 +3842,6 @@ int NS_main(int argc, NS_tchar** argv) {
// And we don't have a good way of accepting the prompt in
// automation.
sinfo.lpVerb = L"open";
- // This handle is what lets the updater that we spawn below know
- // that it's the elevated updater. We are going to close it so that
- // it doesn't know that and will run un-elevated. Doing this make
- // this makes for an imperfect test of the service fallback
- // functionality because it changes how the (usually) elevated
- // updater runs. One of the effects of this is that the secure
- // output files will not be used. So that functionality won't really
- // be covered by testing. But we can't really have the updater run
- // elevated, because that would require a UAC, which we have no way
- // to deal with in automation.
- CloseHandle(elevatedFileHandle);
// We need to let go of the update lock to let the un-elevated
// updater we are about to spawn update.
if (updateLockFileHandle != INVALID_HANDLE_VALUE) {
@@ -3927,8 +3912,6 @@ int NS_main(int argc, NS_tchar** argv) {
}
}
- CloseHandle(elevatedFileHandle);
-
if (updateLockFileHandle != INVALID_HANDLE_VALUE) {
CloseHandle(updateLockFileHandle);
}
@@ -4016,7 +3999,7 @@ int NS_main(int argc, NS_tchar** argv) {
WriteStatusFile(WRITE_ERROR_APPLY_DIR_PATH);
LOG(("NS_main: unable to find apply to dir: " LOG_S, gWorkingDirPath));
output_finish();
- EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
+ EXIT_WHEN_ELEVATED(updateLockFileHandle, 1);
if (argc > callbackIndex) {
LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex,
sUsingService);
@@ -4070,7 +4053,7 @@ int NS_main(int argc, NS_tchar** argv) {
WriteStatusFile(WRITE_ERROR_CALLBACK_PATH);
LOG(("NS_main: unable to find callback file: " LOG_S, targetPath));
output_finish();
- EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
+ EXIT_WHEN_ELEVATED(updateLockFileHandle, 1);
if (argc > callbackIndex) {
LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex,
sUsingService);
@@ -4119,7 +4102,7 @@ int NS_main(int argc, NS_tchar** argv) {
// Don't attempt to launch the callback when the callback path is
// longer than expected.
- EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
+ EXIT_WHEN_ELEVATED(updateLockFileHandle, 1);
return 1;
}
@@ -4136,7 +4119,7 @@ int NS_main(int argc, NS_tchar** argv) {
" into place at " LOG_S,
argv[callbackIndex], gCallbackBackupPath));
output_finish();
- EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
+ EXIT_WHEN_ELEVATED(updateLockFileHandle, 1);
LaunchCallbackApp(argv[callbackIndex], argc - callbackIndex,
argv + callbackIndex, sUsingService);
return 1;
@@ -4211,7 +4194,7 @@ int NS_main(int argc, NS_tchar** argv) {
gCallbackBackupPath));
}
output_finish();
- EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
+ EXIT_WHEN_ELEVATED(updateLockFileHandle, 1);
LaunchCallbackApp(argv[5], argc - callbackIndex,
argv + callbackIndex, sUsingService);
return 1;
@@ -4322,7 +4305,6 @@ int NS_main(int argc, NS_tchar** argv) {
int retVal = LaunchCallbackAndPostProcessApps(argc, argv, callbackIndex
#ifdef XP_WIN
,
- elevatedLockFilePath,
updateLockFileHandle
#elif XP_MACOSX
,
diff --git a/toolkit/xre/WinTokenUtils.cpp b/toolkit/xre/WinTokenUtils.cpp
index 4774ec6dad6..590df338912 100644
--- a/toolkit/xre/WinTokenUtils.cpp
+++ b/toolkit/xre/WinTokenUtils.cpp
@@ -5,23 +5,21 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "WinTokenUtils.h"
-#include "nsWindowsHelpers.h"
using namespace mozilla;
// If |aToken| is nullptr, CheckTokenMembership uses the calling thread's
// primary token to check membership for.
-static LauncherResult<bool> IsMemberOfAdministrators(
- const nsAutoHandle& aToken) {
- BYTE adminsGroupSid[SECURITY_MAX_SID_SIZE];
- DWORD adminsGroupSidSize = sizeof(adminsGroupSid);
- if (!CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, adminsGroupSid,
- &adminsGroupSidSize)) {
+static LauncherResult<bool> IsMemberOfSidType(
+ const nsAutoHandle& aToken, const WELL_KNOWN_SID_TYPE aWellKnownSid) {
+ BYTE sid[SECURITY_MAX_SID_SIZE];
+ DWORD sidSize = sizeof(sid);
+ if (!CreateWellKnownSid(aWellKnownSid, nullptr, sid, &sidSize)) {
return LAUNCHER_ERROR_FROM_LAST();
}
BOOL isMember;
- if (!CheckTokenMembership(aToken, adminsGroupSid, &isMember)) {
+ if (!CheckTokenMembership(aToken, sid, &isMember)) {
return LAUNCHER_ERROR_FROM_LAST();
}
return !!isMember;
@@ -45,28 +43,33 @@ static LauncherResult<bool> IsUacEnabled() {
namespace mozilla {
LauncherResult<bool> IsAdminWithoutUac() {
- // To check whether the process was launched with Administrator priviledges
- // or not, we cannot simply check the integrity level of the current process
+ // To check whether the process was launched with Administrator privileges or
+ // not, we cannot simply check the integrity level of the current process
// because the launcher process spawns the browser process with the medium
// integrity level even though the launcher process is high integrity level.
- // We check whether the thread's token contains Administratos SID or not
+ // We check whether the thread's token contains Administrators SID or not
// instead.
- LauncherResult<bool> containsAdminGroup =
- IsMemberOfAdministrators(nsAutoHandle());
- if (containsAdminGroup.isErr()) {
- return containsAdminGroup.propagateErr();
- }
+ return UserHasAdminPrivileges().andThen(
+ [](bool containsAdminGroup) -> LauncherResult<bool> {
+ if (!containsAdminGroup) {
+ // We don't have Administrator privileges, no need to check if UAC is
+ // enabled.
+ return false;
+ }
- if (!containsAdminGroup.unwrap()) {
- return false;
- }
+ // We have Administrator privileges, now check if we have them while UAC
+ // is disabled.
+ return IsUacEnabled().map(
+ [](bool isUacEnabled) { return !isUacEnabled; });
+ });
+}
- LauncherResult<bool> isUacEnabled = IsUacEnabled();
- if (isUacEnabled.isErr()) {
- return isUacEnabled.propagateErr();
- }
+LauncherResult<bool> UserHasAdminPrivileges() {
+ return IsMemberOfSidType(nsAutoHandle(), WinBuiltinAdministratorsSid);
+}
- return !isUacEnabled.unwrap();
+LauncherResult<bool> UserIsLocalSystem() {
+ return IsMemberOfSidType(nsAutoHandle(), WinLocalSystemSid);
}
} // namespace mozilla
diff --git a/toolkit/xre/WinTokenUtils.h b/toolkit/xre/WinTokenUtils.h
index 48b9ec5a8b1..6c8ec61eb29 100644
--- a/toolkit/xre/WinTokenUtils.h
+++ b/toolkit/xre/WinTokenUtils.h
@@ -11,8 +11,42 @@
namespace mozilla {
+/**
+ * Windows UAC can be disabled via the registry. This checks if the user has
+ * Administrator privileges and UAC has been disabled.
+ *
+ * @return `Ok(true)` when the current process has Administrator privileges
+ * *and* UAC has been disabled, otherwise `Ok(false)` or
+ * `Err(WindowsError)`.
+ */
LauncherResult<bool> IsAdminWithoutUac();
+/**
+ * Checks if the current process has Administrator privileges.
+ *
+ * @return `Ok(true)` when the current process has Administrator privileges,
+ * otherwise `Ok(false)` or `Err(WindowsError)`.
+ *
+ */
+LauncherResult<bool> UserHasAdminPrivileges();
+
+/**
+ * Checks if the current process user is LocalSystem.
+ *
+ * LocalSystem (or just SYSTEM in Task Manager) is a high privileged account
+ * similar to Administrator. Unlike Administrator which runs under a User
+ * context, LocalSystem runs under a System context and has some privileges
+ * beyond that of Administrator. We use LocalSystem for privileges necessary for
+ * our updater.
+ *
+ * Note that LocalSystem is not equivalent to the lower privileged LocalSerivce
+ * account.
+ *
+ * @return `Ok(true)` when the current process user is LocalSystem, otherwise
+ * `Ok(false)` or `Err(WindowsError)`.
+ */
+LauncherResult<bool> UserIsLocalSystem();
+
} // namespace mozilla
#endif // mozilla_WinTokenUtils_h