CVE-2026-87457
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/updater/app/server/win/com_classes_util.cc |
modified | |
TESTchrome/updater/app/server/win/com_classes_util_unittest.cc |
modified | |
ifchrome/updater/update_service_impl_impl.cc |
modified | |
TESTchrome/updater/util/win_util_unittest.cc |
modified | |
TESTchrome/updater/win/manifest_util_unittest.cc |
modified |
Files Changed
chrome/updater/app/server/win/com_classes_util.ccchrome/updater/app/server/win/com_classes_util_unittest.ccchrome/updater/update_service_impl_impl.ccchrome/updater/util/win_util.ccchrome/updater/util/win_util_unittest.ccchrome/updater/win/manifest_util.ccchrome/updater/win/manifest_util_unittest.cc
Patch
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(
Regression Test / PoC
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;
Original Bug Report
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.ccchrome/updater/win/installer/installer.ccchrome/updater/win/manifest_util.ccchrome/updater/win/installer_api.ccchrome/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
- Incorrect Admin Identification: In
base/files/file_util_win.cc,internal::IsUserDefaultAdmin()checks if the process token elevation type is exactlyTokenElevationTypeDefault. However, a standard administrator who elevated via a UAC prompt has an elevation type ofTokenElevationTypeFull. Thus, the function incorrectly returnsfalsefor elevated UAC processes. - Insecure Temp Directory Fallback: Because
IsUserDefaultAdmin()returnsfalse,GetSecureTempDirectory()falls back toGetTempDir(), which resolves to the user’s Medium-IL writable%TEMP%directory (e.g.,C:\Users\<user>\AppData\Local\Temp). - 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
- Running as a standard Medium-IL user, set up a directory watcher (e.g.,
ReadDirectoryChangesW) on the user’s%TEMP%folder. - Wait for or trigger a system-scope Chrome offline installation that prompts for UAC elevation.
- When the watcher detects a new extraction directory (e.g.,
Google{PID}_{rand}), rapidly place an oplock onbin\updater.exe(for High-IL exploit) orOfflineManifest.gup(for SYSTEM exploit) as they are created. - Overwrite
bin\updater.exewith a custom payload, OR editOfflineManifest.gupto point theruncommand to an attacker-controlled payload path via directory traversal (..\..\payload.exe). - 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
- Fix Admin Detection: Update
internal::IsUserDefaultAdmin()inbase/files/file_util_win.ccto accurately reflect high-privilege execution. This can be done by allowingTokenElevationTypeFullwhen checking elevation type, or preferably, by querying the token’sTokenIntegrityLevelto explicitly verify if the process is running at High or SYSTEM integrity. - 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 (..). - Validate Manifest Executables: In
chrome/updater/win/manifest_util.cc, sanitize therunattribute fromOfflineManifest.gupto 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.