High CVSS 8.8 firefox Authorization Error 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionMozilla Firefox's update mechanism allowed a medium-integrity user process to interfere with the SYSTEM-level updater by manipulating the file-locking behavior. By injecting code into the user-privileged process, an attacker could bypass intended access controls, allowing SYSTEM-level file operations on paths controlled by a non-privileged user and enabling privilege escalation.
ComponentToolkit
Bug ClassAuthorization Error
Tracker1917536
Fix commit72e0b1b4f11b (firefox) +102/-82
CWECWE-22
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedDong-uk Kim (@justlikebono)
Disclosed2025-04-29

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.lock file (with FILE_FLAG_DELETE_ON_CLOSE) before spawning the elevated updater; the spawned process inferred elevation from the presence of that file via GetFileAttributesW.
`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.

Key insight
A SYSTEM/elevated updater inferred its own privilege level from the presence/locking of a user-controllable lock file instead of the OS token, letting a medium-integrity process forge that signal and redirect SYSTEM file operations; the fix checks Administrators/LocalSystem token membership directly.

Attack Path

  1. 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”).
  2. Manipulate the signal They manipulate the update_elevated.lock file’s existence or file-locking behavior under the patch directory that the elevated updater inspects.
  3. Mis-decision The elevated/SYSTEM updater derives gIsElevated/gUseSecureOutputPath from that forged filesystem state (inferred exact primitive; the diff shows the signal being replaced).
  4. Redirected privileged op SYSTEM-level file operations (output/status files, callback handling) proceed along user-controlled paths rather than the secure path.
  5. Primitive Local privilege escalation: a non-privileged user induces SYSTEM-level file operations on attacker-chosen paths.

Impact Assessment

This is a Windows-only local elevation-of-privilege defect in the updater, which runs as SYSTEM (Maintenance Service) or elevated Administrator. The attacker needs an existing medium-integrity foothold (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

FunctionChangeNotes
if
toolkit/mozapps/update/updater/updater.cpp
modified
if
toolkit/xre/WinTokenUtils.cpp
modified

Files Changed

  • toolkit/mozapps/update/updater/updater-common.build
  • toolkit/mozapps/update/updater/updater.cpp
  • toolkit/xre/WinTokenUtils.cpp
  • toolkit/xre/WinTokenUtils.h

Audit Directions

  • Filesystem-as-privilege-signal
    Audit 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 helpers
    Review other privileged helper processes that infer caller/self state from IPC or filesystem artifacts a lower-integrity process can forge.
  • Path control under SYSTEM
    Examine where gUseSecureOutputPath/status-file paths are constructed to ensure no SYSTEM write targets a user-controllable path.
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
Loading diff…