Medium watch CVSS 8.1 chrome Race 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in Updater
DescriptionRace condition in Updater
ComponentUpdater
Sub-componentUpdater
Bug ClassRace
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497837188
Fix commit58d213992ecc (chromium/src) +104/-18
CWECWE-367 (TOCTOU race)
CVSS vectorCVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-05-14 (public git)
Disclosed2026-09-08
Patch gap117 days — fix public in source before the advisory

Background

Chromium Updater
the elevated/SYSTEM helper (Google Update/Omaha successor) that downloads and runs application installers, exposed to callers over a COM RPC surface.
`CreateSecureTempDir`
an updater helper (chrome/updater/util/win_util.cc) that allocates a working temp directory into which installer parameters and payloads are staged before execution.
`base::DIR_SYSTEM_TEMP` vs `base::GetTempDir`
the former resolves to a system-secured temp location writable only by administrators, the latter to the (potentially user-writable) per-user temp directory.
`base::FilePath::ReferencesParent()` / `IsAbsolute()`
path predicates that detect .. traversal components and absolute (rooted) paths respectively.

Root Cause Analysis

When the updater ran under a UAC-elevated or SYSTEM context, CreateSecureTempDir unconditionally called base::CreateNewTempDirectory, which resolves under the per-user temp directory (base::GetTempDir) that a lower-privileged local user can also write to. Because the elevated process staged installer parameters into that user-writable directory, a concurrent local attacker could substitute or tamper with the staged contents — a classic staging-directory race between the privileged writer and the unprivileged file — before the elevated installer consumed them. Separately, ValidateInstallerPath (com_classes_util.cc) and the offline-manifest reader in manifest_util.cc accepted installer/run paths verbatim, so .. traversal or absolute paths could redirect execution outside the intended install directory.

The fix routes elevated temp allocation to base::DIR_SYSTEM_TEMP (keyed on ::IsUserAnAdmin()) via base::CreateTemporaryDirInDir, and rejects any installer path or manifest run value that IsAbsolute() or ReferencesParent(), closing both the untrusted-staging-location and path-traversal vectors. The manifest change additionally normalizes separators so \ and / traversal are treated identically.

Key insight
The core mistake was staging privileged installer inputs in a user-writable temp directory (and trusting caller-supplied paths without traversal checks); the fix pins elevated temp creation to a system-secured parent and adds IsAbsolute()/ReferencesParent() rejection at both the COM RPC path validator and the manifest run-path evaluator.

Attack Path

  1. Privileged updater stages in a shared temp With the pre-patch code, when the updater runs elevated it allocates its working directory under the per-user temp via base::CreateNewTempDirectory, a location an unprivileged local user can also write into.
  2. Local user wins the staging race A concurrent, lower-privileged process on the same machine tampers with or replaces the installer parameters/payload staged in that predictable, writable directory before the elevated installer consumes them (matching the “Race condition in Updater” advisory class).
  3. Path traversal reinforces redirection Alternatively, a crafted COM RPC installer_path or an offline manifest run value containing .. or an absolute path (e.g. ..\\..\\payload.exe) is accepted by ValidateInstallerPath/ReadInstallCommandFromManifest, pointing execution outside the intended offline app directory.
  4. Elevated execution of attacker-influenced input The updater then launches an installer using the tampered temp contents or redirected path, running attacker-controlled input at high integrity/SYSTEM (inferred).

Impact Assessment

An unprivileged local user could influence what a UAC-elevated or SYSTEM updater process reads and executes, either by racing writes into the user-writable staging directory or by supplying traversal/absolute paths through the COM RPC and offline-manifest surfaces. Successful exploitation yields local privilege escalation (inferred); the advisory classifies this as a race condition in the Updater component and does not state in-the-wild exploitation. Preconditions are local code execution on the machine and the updater performing an elevated install operation.

Changed Functions

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

Files Changed

  • chrome/updater/util/win_util_unittest.cc
  • chrome/updater/app/server/win/com_classes_util.cc
  • chrome/updater/app/server/win/com_classes_util_unittest.cc
  • chrome/updater/win/manifest_util_unittest.cc
  • chrome/updater/util/win_util.cc
  • chrome/updater/update_service_impl_impl.cc
  • chrome/updater/win/manifest_util.cc

Audit Directions

  • Privileged writes to user-writable temp
    Audit every elevated/SYSTEM updater or service code path that creates temp/staging directories, confirming it uses base::DIR_SYSTEM_TEMP (or an ACL-restricted parent) rather than base::GetTempDir/CreateNewTempDirectory.
  • Unvalidated caller-supplied paths
    Review all COM RPC and IPC entry points and manifest/config parsers that turn strings into base::FilePath, ensuring IsAbsolute() and ReferencesParent() are checked before the path is appended to a trusted base directory.
  • Separator normalization gaps
    Check path-traversal validators for handling of both \ and /, since Windows accepts mixed separators and a check on only one form can be bypassed.
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/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/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/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…

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