Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in Updater
DescriptionRace condition in Updater
ComponentUpdater
Bug ClassRace
Tracker497837188
Fix commit58d213992ecc (chromium/src) +104/-18
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
chrome/updater/app/server/win/com_classes_util.cc
modified
TEST
chrome/updater/app/server/win/com_classes_util_unittest.cc
modified
if
chrome/updater/update_service_impl_impl.cc
modified
TEST
chrome/updater/util/win_util_unittest.cc
modified
TEST
chrome/updater/win/manifest_util_unittest.cc
modified

Files Changed

  • chrome/updater/app/server/win/com_classes_util.cc
  • chrome/updater/app/server/win/com_classes_util_unittest.cc
  • chrome/updater/update_service_impl_impl.cc
  • chrome/updater/util/win_util.cc
  • chrome/updater/util/win_util_unittest.cc
  • chrome/updater/win/manifest_util.cc
  • chrome/updater/win/manifest_util_unittest.cc
From 58d213992eccdf37fe81c2119ec08c5475cec1a6 Mon Sep 17 00:00:00 2001
From: S Ganesh <ganesh@chromium.org>
Date: Wed, 13 May 2026 20:37:46 -0700
Subject: [PATCH] updater: Harden process temp selection and path validation

Update elevated process secure temporary directory selection to rely
directly on ::IsUserAnAdmin() and base::DIR_SYSTEM_TEMP, bypassing
base::IsUserDefaultAdmin() under UAC-elevated contexts.

Harden installer temporary directory creation to use CreateSecureTempDir
on Windows, ensuring application installations initiated under high-IL
or SYSTEM privilege do not register parameters in user-writable temp.

Additionally, sanitize manifest run-path evaluations by blocking
absolute or parent directory traversal paths, and raise bounds controls
on the COM RPC installer path validation layer to reject traversal
paths.

Path separator representation is normalized strictly during parent
traversal validations (\ -> /), ensuring robust platform-independent
behavior under both POSIX and Windows.

Bug: b:497837188
Change-Id: If71aa0aeb45914d3c87c5e370ab100c48444e4c4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7842593
Commit-Queue: S Ganesh <ganesh@chromium.org>
Reviewed-by: Sorin Jianu <sorin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1630430}
---

diff --git a/chrome/updater/app/server/win/com_classes_util.cc b/chrome/updater/app/server/win/com_classes_util.cc
index f76f672..3d184b83 100644
--- a/chrome/updater/app/server/win/com_classes_util.cc
+++ b/chrome/updater/app/server/win/com_classes_util.cc
@@ -112,9 +112,11 @@
     const wchar_t* installer_path) {
   const std::optional<std::string> installer_path_s =
       ValidateStringEmptyNotOk(installer_path, kMaxStringLen);
-  return installer_path_s ? std::make_optional(base::FilePath(
-                                base::UTF8ToWide(*installer_path_s)))
-                          : std::nullopt;
+  if (!installer_path_s) {
+    return std::nullopt;
+  }
+  const base::FilePath path(base::UTF8ToWide(*installer_path_s));
+  return path.ReferencesParent() ? std::nullopt : std::make_optional(path);
 }
 
 std::optional<std::string> ValidateInstallArgs(const wchar_t* install_args) {
diff --git a/chrome/updater/app/server/win/com_classes_util_unittest.cc b/chrome/updater/app/server/win/com_classes_util_unittest.cc
index d8ae732..4747d06 100644
--- a/chrome/updater/app/server/win/com_classes_util_unittest.cc
+++ b/chrome/updater/app/server/win/com_classes_util_unittest.cc
@@ -94,6 +94,10 @@
   ASSERT_FALSE(ValidateInstallerPath(std::wstring(0x4001, 'a').c_str()));
   ASSERT_EQ(ValidateInstallerPath(L"installerpathisvalid").value().value(),
             L"installerpathisvalid");
+  ASSERT_FALSE(ValidateInstallerPath(L"C:\\temp\\..\\setup.exe"));
+  ASSERT_FALSE(ValidateInstallerPath(L"..\\setup.exe"));
+  ASSERT_FALSE(ValidateInstallerPath(L"C:/temp/../setup.exe"));
+  ASSERT_FALSE(ValidateInstallerPath(L"../setup.exe"));
 }
 
 TEST(ComClassesUtil, ValidateInstallArgs) {
diff --git a/chrome/updater/update_service_impl_impl.cc b/chrome/updater/update_service_impl_impl.cc
index ed44746..03a73970 100644
--- a/chrome/updater/update_service_impl_impl.cc
+++ b/chrome/updater/update_service_impl_impl.cc
@@ -1443,18 +1443,25 @@
           [](const AppInfo& app_info, const base::FilePath& installer_path,
              const std::string& install_args, const std::string& install_data,
              base::RepeatingCallback<void(const UpdateState&)> state_update) {
+#if BUILDFLAG(IS_WIN)
+            std::optional<base::ScopedTempDir> temp_dir_owner =
+                CreateSecureTempDir();
+            if (!temp_dir_owner) {
+              return InstallerResult(
+                  {.category = update_client::ErrorCategory::kInstall,
+                   .code = kErrorCreatingTempDir,
+                   .extra = HRESULTFromLastError()});
+            }
+            base::ScopedTempDir temp_dir = std::move(*temp_dir_owner);
+#else
             base::ScopedTempDir temp_dir;
             if (!temp_dir.CreateUniqueTempDir()) {
               return InstallerResult(
                   {.category = update_client::ErrorCategory::kInstall,
                    .code = kErrorCreatingTempDir,
-#if BUILDFLAG(IS_WIN)
-                   .extra = HRESULTFromLastError()
-#else
-                   .extra = logging::GetLastSystemErrorCode()
-#endif  // BUILDFLAG(IS_WIN)
-                  });
+                   .extra = logging::GetLastSystemErrorCode()});
             }
+#endif
 
             return RunApplicationInstaller(
                 app_info, installer_path, install_args,
diff --git a/chrome/updater/util/win_util.cc b/chrome/updater/util/win_util.cc
index 4899085..8d15b20 100644
--- a/chrome/updater/util/win_util.cc
+++ b/chrome/updater/util/win_util.cc
@@ -886,14 +886,25 @@
 }
 
 std::optional<base::ScopedTempDir> CreateSecureTempDir() {
-  // This function uses `base::CreateNewTempDirectory` and then a
-  // `base::ScopedTempDir` as owner, instead of just
-  // `base::ScopedTempDir::CreateUniqueTempDir`, because the former allows
-  // setting a more recognizable prefix of `COMPANY_SHORTNAME_STRING` on the
-  // temp directory.
+  // This function uses `base::CreateTemporaryDirInDir` under a secure parent
+  // (or standard temp if unelevated) and then a `base::ScopedTempDir` as owner,
+  // instead of just `base::ScopedTempDir::CreateUniqueTempDir`, because the
+  // former allows setting a more recognizable prefix of
+  // `COMPANY_SHORTNAME_STRING` on the temp directory.
+  base::FilePath parent_dir;
+  if (::IsUserAnAdmin()) {
+    if (!base::PathService::Get(base::DIR_SYSTEM_TEMP, &parent_dir)) {
+      return std::nullopt;
+    }
+  } else {
+    if (!base::GetTempDir(&parent_dir)) {
+      return std::nullopt;
+    }
+  }
+
   base::FilePath temp_dir;
-  if (!base::CreateNewTempDirectory(FILE_PATH_LITERAL(COMPANY_SHORTNAME_STRING),
-                                    &temp_dir)) {
+  if (!base::CreateTemporaryDirInDir(
+          parent_dir, FILE_PATH_LITERAL(COMPANY_SHORTNAME_STRING), &temp_dir)) {
     return std::nullopt;
   }
 
diff --git a/chrome/updater/util/win_util_unittest.cc b/chrome/updater/util/win_util_unittest.cc
index 2426f2e..c2ba486 100644
--- a/chrome/updater/util/win_util_unittest.cc
+++ b/chrome/updater/util/win_util_unittest.cc
@@ -303,8 +303,17 @@
 
 TEST(WinUtil, CreateSecureTempDir) {
   std::optional<base::ScopedTempDir> temp_dir = CreateSecureTempDir();
-  EXPECT_TRUE(temp_dir);
-  EXPECT_TRUE(temp_dir->IsValid());
+  ASSERT_TRUE(temp_dir);
+  ASSERT_TRUE(temp_dir->IsValid());
+
+  base::FilePath expected_parent;
+  if (::IsUserAnAdmin()) {
+    ASSERT_TRUE(
+        base::PathService::Get(base::DIR_SYSTEM_TEMP, &expected_parent));
+  } else {
+    ASSERT_TRUE(base::GetTempDir(&expected_parent));
+  }
+  EXPECT_TRUE(expected_parent.IsParent(temp_dir->GetPath()));
 }
 
 TEST(WinUtil, SignalShutdownEvent) {
diff --git a/chrome/updater/win/manifest_util.cc b/chrome/updater/win/manifest_util.cc
index 1560cc1..ac5adf65 100644
--- a/chrome/updater/win/manifest_util.cc
+++ b/chrome/updater/win/manifest_util.cc
@@ -136,6 +136,13 @@
   installer_version = it->manifest.version;
   installer_path = [&offline_dir, &app_id, &it] {
     const base::FilePath app_dir(offline_dir.AppendUTF8(app_id));
+    const base::FilePath run_path(
+        base::FilePath::FromUTF8Unsafe(it->manifest.run));
+    if (run_path.IsAbsolute() || run_path.ReferencesParent()) {
+      VLOG(1) << "Manifest run path is unsafe (absolute or traversal): "
+              << it->manifest.run;
+      return base::FilePath();
+    }
     const base::FilePath path(app_dir.AppendUTF8(it->manifest.run));
     return base::PathExists(path)
                ? path
diff --git a/chrome/updater/win/manifest_util_unittest.cc b/chrome/updater/win/manifest_util_unittest.cc
index ba0d1b5..99d83eb 100644
--- a/chrome/updater/win/manifest_util_unittest.cc
+++ b/chrome/updater/win/manifest_util_unittest.cc
@@ -86,6 +86,52 @@
             "      }");
 }
 
+TEST(ManifestUtil, ReadInstallCommandFromManifestUnsafePath) {
+  const std::string app_id("{CDABE316-39CD-43BA-8440-6D1E0547AEE6}");
+  const std::wstring manifest_filename(L"OfflineManifest.gup");
+  const std::wstring offline_dir_guid(
+      L"{7B3A5597-DDEA-409B-B900-4C3D2A94A75C}");
+
+  base::FilePath exe_dir;
+  ASSERT_TRUE(base::PathService::Get(base::DIR_EXE, &exe_dir));
+
+  base::ScopedTempDir scoped_offline_base_dir;
+  ASSERT_TRUE(scoped_offline_base_dir.Set(exe_dir.Append(L"Offline")));
+
+  const base::FilePath offline_dir(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/updater/app/server/win/com_classes_util_unittest.cc b/chrome/updater/app/server/win/com_classes_util_unittest.cc
index d8ae732..4747d06 100644
--- a/chrome/updater/app/server/win/com_classes_util_unittest.cc
+++ b/chrome/updater/app/server/win/com_classes_util_unittest.cc
@@ -94,6 +94,10 @@
   ASSERT_FALSE(ValidateInstallerPath(std::wstring(0x4001, 'a').c_str()));
   ASSERT_EQ(ValidateInstallerPath(L"installerpathisvalid").value().value(),
             L"installerpathisvalid");
+  ASSERT_FALSE(ValidateInstallerPath(L"C:\\temp\\..\\setup.exe"));
+  ASSERT_FALSE(ValidateInstallerPath(L"..\\setup.exe"));
+  ASSERT_FALSE(ValidateInstallerPath(L"C:/temp/../setup.exe"));
+  ASSERT_FALSE(ValidateInstallerPath(L"../setup.exe"));
 }
 
 TEST(ComClassesUtil, ValidateInstallArgs) {
diff --git a/chrome/updater/util/win_util_unittest.cc b/chrome/updater/util/win_util_unittest.cc
index 2426f2e..c2ba486 100644
--- a/chrome/updater/util/win_util_unittest.cc
+++ b/chrome/updater/util/win_util_unittest.cc
@@ -303,8 +303,17 @@
 
 TEST(WinUtil, CreateSecureTempDir) {
   std::optional<base::ScopedTempDir> temp_dir = CreateSecureTempDir();
-  EXPECT_TRUE(temp_dir);
-  EXPECT_TRUE(temp_dir->IsValid());
+  ASSERT_TRUE(temp_dir);
+  ASSERT_TRUE(temp_dir->IsValid());
+
+  base::FilePath expected_parent;
+  if (::IsUserAnAdmin()) {
+    ASSERT_TRUE(
+        base::PathService::Get(base::DIR_SYSTEM_TEMP, &expected_parent));
+  } else {
+    ASSERT_TRUE(base::GetTempDir(&expected_parent));
+  }
+  EXPECT_TRUE(expected_parent.IsParent(temp_dir->GetPath()));
 }
 
 TEST(WinUtil, SignalShutdownEvent) {
diff --git a/chrome/updater/win/manifest_util_unittest.cc b/chrome/updater/win/manifest_util_unittest.cc
index ba0d1b5..99d83eb 100644
--- a/chrome/updater/win/manifest_util_unittest.cc
+++ b/chrome/updater/win/manifest_util_unittest.cc
@@ -86,6 +86,52 @@
             "      }");
 }
 
+TEST(ManifestUtil, ReadInstallCommandFromManifestUnsafePath) {
+  const std::string app_id("{CDABE316-39CD-43BA-8440-6D1E0547AEE6}");
+  const std::wstring manifest_filename(L"OfflineManifest.gup");
+  const std::wstring offline_dir_guid(
+      L"{7B3A5597-DDEA-409B-B900-4C3D2A94A75C}");
+
+  base::FilePath exe_dir;
+  ASSERT_TRUE(base::PathService::Get(base::DIR_EXE, &exe_dir));
+
+  base::ScopedTempDir scoped_offline_base_dir;
+  ASSERT_TRUE(scoped_offline_base_dir.Set(exe_dir.Append(L"Offline")));
+
+  const base::FilePath offline_dir(
+      scoped_offline_base_dir.GetPath().Append(offline_dir_guid));
+  const base::FilePath offline_app_dir(offline_dir.AppendUTF8(app_id));
+  ASSERT_TRUE(base::CreateDirectory(offline_app_dir));
+
+  const std::string manifest_content =
+      "<response protocol=\"3.0\">"
+      " <app appid=\"{CDABE316-39CD-43BA-8440-6D1E0547AEE6}\" status=\"ok\">"
+      "  <updatecheck status=\"ok\">"
+      "   <manifest version=\"1.0\">"
+      "    <actions>"
+      "     <action event=\"install\" run=\"..\\..\\payload.exe\"/>"
+      "    </actions>"
+      "   </manifest>"
+      "  </updatecheck>"
+      " </app>"
+      "</response>";
+  ASSERT_TRUE(
+      base::WriteFile(offline_dir.Append(manifest_filename), manifest_content));
+
+  OfflineManifestSystemRequirements requirements;
+  std::string installer_version;
+  base::FilePath installer_path;
+  std::string install_args;
+  std::string install_data;
+
+  ReadInstallCommandFromManifest(offline_dir_guid, app_id, "verboselogging",
+                                 requirements, installer_version,
+                                 installer_path, install_args, install_data);
+
+  // Should reject traversal path and return empty base::FilePath
+  EXPECT_TRUE(installer_path.empty());
+}
+
 struct ManifestUtilIsArchitectureSupportedTestCase {
   const std::string current_architecture;
   const std::string arch;
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential High/SYSTEM LPE via Insecure %TEMP% Extraction in Windows Metainstaller

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: The Chrome updater metainstaller on Windows incorrectly identifies UAC-elevated admins, falling back to extracting payloads into the user-writable %TEMP% directory instead of a secure location. A local attacker can exploit this by winning a race condition to overwrite the extracted executable, achieving High-IL privilege escalation. Furthermore, tampering with the offline manifest allows for directory traversal execution via the SYSTEM-level COM service, potentially leading to SYSTEM LPE.

Affected files:

  • base/files/file_util_win.cc
  • chrome/updater/win/installer/installer.cc
  • chrome/updater/win/manifest_util.cc
  • chrome/updater/win/installer_api.cc
  • chrome/updater/app/server/win/com_classes_util.cc

Estimated timestamp from git blame: 2025-09-17

Summary

There is a potential Local Privilege Escalation (LPE) vulnerability in the Windows Chrome updater. A logic flaw causes elevated metainstallers (running as High Integrity Level via UAC) to extract their payloads into the standard user’s %TEMP% directory with insecure, inherited permissions. A standard local user (Medium-IL) can monitor this directory, win a race condition using opportunistic locks (oplocks), and overwrite the installer binaries or offline manifests. This can lead to arbitrary code execution at High-IL or SYSTEM-IL.

Note: The following analysis and steps to reproduce are theoretical and generated by an AI agent; no active exploitation code has been run to verify the end-to-end chain, but the code paths and logic flaws are confirmed in the source.

Root Cause Analysis

  1. Incorrect Admin Identification: In base/files/file_util_win.cc, internal::IsUserDefaultAdmin() checks if the process token elevation type is exactly TokenElevationTypeDefault. However, a standard administrator who elevated via a UAC prompt has an elevation type of TokenElevationTypeFull. Thus, the function incorrectly returns false for elevated UAC processes.
  2. Insecure Temp Directory Fallback: Because IsUserDefaultAdmin() returns false, GetSecureTempDirectory() falls back to GetTempDir(), which resolves to the user’s Medium-IL writable %TEMP% directory (e.g., C:\Users\<user>\AppData\Local\Temp).
  3. Insecure Directory Permissions: When the elevated metainstaller calls CreateSecureTempDir(), it creates a subdirectory inside %TEMP% using default security attributes. Due to Windows integrity mechanism inheritance, this new directory inherits the DACL from %TEMP%, granting the Medium-IL user Full Control over the directory and its contents.

Exploit Scenarios

Scenario A: High-IL LPE via Binary Overwrite The metainstaller (chrome/updater/win/installer/installer.cc) extracts the archive and its binary (bin\updater.exe) into the insecure temp directory. It then constructs the path to setup.exe and calls RunProcessAndWait(), which uses ::CreateProcess without signature validation. An attacker can use oplocks to pause the execution immediately after extraction, overwrite bin\updater.exe with a malicious executable, and release the lock. The metainstaller will blindly execute the payload as High-IL.

Scenario B: SYSTEM LPE via Manifest Tampering During an offline installation, OfflineManifest.gup is also extracted into the insecure temp directory. An attacker can overwrite this manifest to modify the run attribute to a directory traversal string (e.g., ..\..\..\payload.exe). When the installer parses the manifest (ReadInstallCommandFromManifest in chrome/updater/win/manifest_util.cc), it appends the run string to the offline directory using AppendUTF8(). Since AppendUTF8() does not normalize paths or strip traversals, the malicious path is sent via COM RPC to the SYSTEM-level updater service. The validation function for this path, ValidateInstallerPath (chrome/updater/app/server/win/com_classes_util.cc), only checks string length and emptiness, failing to catch the traversal. The SYSTEM service eventually executes the un-normalized path, granting the attacker SYSTEM LPE.

Potential Steps to Reproduce

  1. Running as a standard Medium-IL user, set up a directory watcher (e.g., ReadDirectoryChangesW) on the user’s %TEMP% folder.
  2. Wait for or trigger a system-scope Chrome offline installation that prompts for UAC elevation.
  3. When the watcher detects a new extraction directory (e.g., Google{PID}_{rand}), rapidly place an oplock on bin\updater.exe (for High-IL exploit) or OfflineManifest.gup (for SYSTEM exploit) as they are created.
  4. Overwrite bin\updater.exe with a custom payload, OR edit OfflineManifest.gup to point the run command to an attacker-controlled payload path via directory traversal (..\..\payload.exe).
  5. Release the oplock. The installer continues, executing the tampered payload as High-IL or the SYSTEM COM service executes the traversed payload as SYSTEM.

Suggested Fix

  1. Fix Admin Detection: Update internal::IsUserDefaultAdmin() in base/files/file_util_win.cc to accurately reflect high-privilege execution. This can be done by allowing TokenElevationTypeFull when checking elevation type, or preferably, by querying the token’s TokenIntegrityLevel to explicitly verify if the process is running at High or SYSTEM integrity.
  2. Harden Path Validation: In chrome/updater/app/server/win/com_classes_util.cc (ValidateInstallerPath), implement strict path normalization and ensure the resolved path does not contain directory traversal sequences (..).
  3. Validate Manifest Executables: In chrome/updater/win/manifest_util.cc, sanitize the run attribute from OfflineManifest.gup to prevent traversal outside the intended offline directory.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker