CVE-2026-110150
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTchrome/installer/setup/install_component_unittest.cc |
modified |
Files Changed
chrome/installer/setup/install_component_unittest.ccchrome/installer/setup/install_component.cc
Patch
From 933b8e77ec40258df410a4495d0c574833331aa1 Mon Sep 17 00:00:00 2001 From: Marwan Tammam <[email protected]> Date: Tue, 22 Sep 2026 00:16:41 -0700 Subject: [PATCH] [InstallComponent] Stage the CRX under SystemTemp setup.exe --install-component stages a copy of the caller-supplied CRX before verifying it. On a system-level install that staging directory sits under the Chrome install directory and inherits its ACL, which grants BUILTIN\Users read access. Stage the directory under SystemTemp instead so that only admins have access and users don't inherit ACL. Fixed: 561334760 Change-Id: I33d4d21b9682ed3689ee9de58f755c3f0120fd67 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8407915 Reviewed-by: Greg Thompson <[email protected]> Auto-Submit: Marwan Tammam <[email protected]> Commit-Queue: Greg Thompson <[email protected]> Cr-Commit-Position: refs/heads/main@{#1702400} --- diff --git a/chrome/installer/setup/install_component.cc b/chrome/installer/setup/install_component.cc index d2062e1..5375b2f 100644 --- a/chrome/installer/setup/install_component.cc +++ b/chrome/installer/setup/install_component.cc @@ -10,16 +10,17 @@ #include <memory> #include <optional> #include <string> -#include <string_view> #include <utility> #include <vector> #include "base/base64.h" #include "base/check.h" #include "base/containers/span.h" +#include "base/files/file.h" #include "base/files/file_enumerator.h" #include "base/files/file_path.h" #include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" #include "base/json/json_file_value_serializer.h" #include "base/logging.h" #include "base/values.h" @@ -60,33 +61,30 @@ const InstallerState& installer_state, base::span<const ComponentConfig> supported_components, crx_file::VerifierFormat verifier_format) { - // Validate source file: must not be empty and must not reference parent dirs. - CHECK(!source_file.empty() && !source_file.ReferencesParent()); + // Source file must be absolute, non-empty and must not reference parent dirs. + CHECK(source_file.IsAbsolute() && !source_file.ReferencesParent()); - // TOCTOU-Safe Staged copy into admin-protected temp directory. - SelfCleaningTempDir temp_path; - if (!temp_path.Initialize(installer_state.target_path().DirName(), - kInstallTempDir)) { - PLOG(ERROR) << "Failed to initialize temporary directory"; + // Reject anything that is not a regular non-empty file. + base::File::Info source_info; + if (base::GetFileInfo(source_file, &source_info)) { + CHECK(!source_info.is_directory && source_info.size > 0); + } + + // The staged CRX is a copy of a caller-chosen file made before the file has + // been validated. We stage it in a secure temporary directory (SystemTemp on + // Windows when running as SYSTEM or default admin, or the users's private + // Temp directory) to ensure unprivileged users cannot read back files they + // cannot open themselves. + base::ScopedTempDir staged_crx_dir; + if (!staged_crx_dir.CreateUniqueTempDir(FILE_PATH_LITERAL("CrxStaging"))) { + PLOG(ERROR) << "Failed to create CRX staging directory"; return installer::INSTALL_COMPONENT_FAILED_INTERNAL; } - FileConductor file_conductor(temp_path.path()); - absl::Cleanup undo_on_failure = [&file_conductor] { - VLOG(1) << "Failure occurred, calling FileConductor::Undo()"; - file_conductor.Undo(); - }; - - base::FilePath staged_crx_dir; - if (!base::CreateTemporaryDirInDir(temp_path.path(), L"CrxStaging", - &staged_crx_dir)) { - PLOG(ERROR) << "Failed to create CRX staging subdirectory in " - << temp_path.path(); - return installer::INSTALL_COMPONENT_FAILED_INTERNAL; - } - - base::FilePath staged_file = staged_crx_dir.Append(source_file.BaseName()); - if (!file_conductor.CopyEntry(source_file, staged_file)) { + base::FilePath staged_file = + staged_crx_dir.GetPath().Append(source_file.BaseName()); + if (!base::CopyFile(source_file, staged_file)) { + PLOG(ERROR) << "Failed to copy CRX to staging directory"; return installer::INSTALL_COMPONENT_FAILED_INTERNAL; } @@ -127,8 +125,20 @@ return installer::INSTALL_COMPONENT_FAILED_SIGNATURE; } - // Fresh directory for unpacking the CRX payload to prevent filename - // collisions with the staged CRX file. + // Temp directory for unpacking the CRX payload, under the installer's dir. + SelfCleaningTempDir temp_path; + if (!temp_path.Initialize(installer_state.target_path().DirName(), + kInstallTempDir)) { + PLOG(ERROR) << "Failed to initialize temporary directory"; + return installer::INSTALL_COMPONENT_FAILED_INTERNAL; + } + + FileConductor file_conductor(temp_path.path()); + absl::Cleanup undo_on_failure = [&file_conductor] { + VLOG(1) << "Failure occurred, calling FileConductor::Undo()"; + file_conductor.Undo(); + }; + base::FilePath unpack_dir; if (!base::CreateTemporaryDirInDir(temp_path.path(), L"Unpacked", &unpack_dir)) { @@ -143,6 +153,11 @@ return installer::INSTALL_COMPONENT_INVALID_INPUT; } + // The staged copy is no longer needed once its contents have been extracted. + if (!staged_crx_dir.Delete()) { + VLOG(1) << "Failed to delete staged CRX directory"; + } + // Read manifest.json to get version and validate name. base::FilePath manifest_path = unpack_dir.Append(kManifestFilename); if (!base::PathExists(manifest_path)) { diff --git a/chrome/installer/setup/install_component_unittest.cc b/chrome/installer/setup/install_component_unittest.cc index 98891f15c..85175c3 100644 --- a/chrome/installer/setup/install_component_unittest.cc +++ b/chrome/installer/setup/install_component_unittest.cc @@ -4,9 +4,10 @@ #include "chrome/installer/setup/install_component.h" +#include <windows.h> + #include <stdint.h> -#include <memory> #include <optional> #include <string> @@ -16,6 +17,7 @@ #include "base/files/scoped_temp_dir.h" #include "base/path_service.h" #include "base/version.h" +#include "base/win/security_descriptor.h" #include "chrome/installer/setup/installer_state.h" #include "chrome/installer/util/util_constants.h" #include "components/crx_file/crx_verifier.h" @@ -509,4 +511,31 @@ EXPECT_FALSE(base::PathExists(v394_incomplete.AppendASCII("stale.dll"))); } +// Verifies that the installed component directory inherits its ACL from the +// parent application directory. This ensures that components installed by an +// elevated process remain accessible to standard unprivileged user processes. +TEST(InstallComponentTest, InstalledComponentDirectoryInheritsAcl) { + InstallerState installer_state(InstallerState::SYSTEM_LEVEL); + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + installer_state.set_target_path_for_testing(temp_dir.GetPath()); + + base::FilePath src_file = GetTestCrxPath(); + ASSERT_TRUE(base::PathExists(src_file)); + ASSERT_EQ(INSTALL_COMPONENT_SUCCESS, + InstallComponentForTesting( + src_file, installer_state, kTestComponents, + crx_file::VerifierFormat::CRX3_WITH_TEST_PUBLISHER_PROOF)); + + base::FilePath installed_dir = + temp_dir.GetPath().AppendASCII(kTestDeveloperCrxId).AppendASCII("394"); + ASSERT_TRUE(base::PathExists(installed_dir)); + + std::optional<base::win::SecurityDescriptor> descriptor = + base::win::SecurityDescriptor::FromFile(installed_dir, + DACL_SECURITY_INFORMATION); + ASSERT_TRUE(descriptor.has_value()); + EXPECT_FALSE(descriptor->dacl_protected()); +} + } // namespace installer
Regression Test / PoC
diff --git a/chrome/installer/setup/install_component_unittest.cc b/chrome/installer/setup/install_component_unittest.cc
index 98891f15c..85175c3 100644
--- a/chrome/installer/setup/install_component_unittest.cc
+++ b/chrome/installer/setup/install_component_unittest.cc
@@ -4,9 +4,10 @@
#include "chrome/installer/setup/install_component.h"
+#include <windows.h>
+
#include <stdint.h>
-#include <memory>
#include <optional>
#include <string>
@@ -16,6 +17,7 @@
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/version.h"
+#include "base/win/security_descriptor.h"
#include "chrome/installer/setup/installer_state.h"
#include "chrome/installer/util/util_constants.h"
#include "components/crx_file/crx_verifier.h"
@@ -509,4 +511,31 @@
EXPECT_FALSE(base::PathExists(v394_incomplete.AppendASCII("stale.dll")));
}
+// Verifies that the installed component directory inherits its ACL from the
+// parent application directory. This ensures that components installed by an
+// elevated process remain accessible to standard unprivileged user processes.
+TEST(InstallComponentTest, InstalledComponentDirectoryInheritsAcl) {
+ InstallerState installer_state(InstallerState::SYSTEM_LEVEL);
+ base::ScopedTempDir temp_dir;
+ ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+ installer_state.set_target_path_for_testing(temp_dir.GetPath());
+
+ base::FilePath src_file = GetTestCrxPath();
+ ASSERT_TRUE(base::PathExists(src_file));
+ ASSERT_EQ(INSTALL_COMPONENT_SUCCESS,
+ InstallComponentForTesting(
+ src_file, installer_state, kTestComponents,
+ crx_file::VerifierFormat::CRX3_WITH_TEST_PUBLISHER_PROOF));
+
+ base::FilePath installed_dir =
+ temp_dir.GetPath().AppendASCII(kTestDeveloperCrxId).AppendASCII("394");
+ ASSERT_TRUE(base::PathExists(installed_dir));
+
+ std::optional<base::win::SecurityDescriptor> descriptor =
+ base::win::SecurityDescriptor::FromFile(installed_dir,
+ DACL_SECURITY_INFORMATION);
+ ASSERT_TRUE(descriptor.has_value());
+ EXPECT_FALSE(descriptor->dacl_protected());
+}
+
} // namespace installer
Original Bug Report
Arbitrary file read as SYSTEM via the install-component app command
Report description
Arbitrary file read as SYSTEM via the install-component app command
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://source.chromium.org/chromium/chromium/src/+/8a2276d251f79ab09d691583ec76d36907ea2140
The problem
Please describe the technical details of the vulnerability
Summary
An unprivileged user can make Chrome’s setup.exe, running as LocalSystem, copy a file of the caller’s choosing into a directory that user can read. The installer registers install-component as a Google Update app command, so IAppCommandWeb::execute passes the caller’s %1 to setup.exe --install-component=<path>. That path is copied before it is validated and without impersonating the caller, into a staging directory that inherits BUILTIN\Users read access from the Chrome install tree. The copy is a plain ::CopyFile, so it reaches any file the service can open.
Reproduction steps
- Log in as a standard user, medium integrity, not elevated.
- Build with
%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:exe /platform:x64 /out:poc.exe poc.cs. That compiler ships with Windows. - Run
poc.exe. With no argument it targetsC:\Windows\System32\winevt\Logs\Security.evtx. The install and its staging directory are read from the registry. - The PoC opens the target directly first, and that open must fail.
- Pass a second argument to save the bytes, then run
Get-WinEvent -Path <saved file>on them.
The privileged copy occurred in 5 of 5 runs, exit 83 each time. Reading it back races the installer’s cleanup and won in 2 of those 5.
Observed versus expected
caller : SENTINEL\ojanc
target : C:\Windows\System32\winevt\Logs\Security.evtx
direct open DENIED: UnauthorizedAccessException [control fired]
install-component registered by {401C381F-E0DE-4B85-8BD8-3F3F14FBDA57}
staging root C:\Program Files\Google\Chrome Dev\Temp exists before the call: False
got IAppCommandWeb from {401C381F-E0DE-4B85-8BD8-3F3F14FBDA57}
status : 4
exit code : 83
staged copy written by the SYSTEM process and read back:
path : C:\Program Files\Google\Chrome Dev\Temp\CrxStaging35692_542632880\Security.evtx
size : 20975616 bytes
bytes read: 20975616
complete : True
sha256 : 0DF7BDC2A69245756D03A75ED3216D4140A1F900D17BBFD6E80E6E29385D8432
head hex : 456C6646696C65000C000000000000000B0000000000000021FE260000000000
head text : ElfFile.................!.&.....
saved to : captured.evtx
RESULT: this user read a file the OS denies it.
poc.png is a screenshot of this run, ending with four records read straight out of the captured log. A full parse of that same file returns 33,982 audit records spanning 2026-09-11 to 2026-09-14, including 1,041 successful logons, 299 failed logons, 1,021 special-privilege assignments and 12 distinct security principals, one of them a second local account. Get-WinEvent -LogName Security in the same session fails with “Attempted to perform an unauthorized operation”. Exit code 83 is INSTALL_COMPONENT_FAILED_SIGNATURE, so the copy completes before the signature check. Aimed at C:\Users\<other account>\NTUSER.DAT, the same PoC returns that account’s registry hive complete, 262144 of 262144 bytes, regf magic, sha256 8D8406D8B5FACDDAC98BD7B02FE64A7B4B3122B6C367479F9C0BABF0A5EE1184, identical across runs days apart. This user cannot list that directory.
Chrome should fail the copy with access denied, since the caller cannot read it.
Root cause
install_component.cc:64 checks the caller’s path only for emptiness and parent references. Line 89 copies it into a fresh staging directory through FileConductor::CopyEntry, which reaches a plain ::CopyFile. crx_file::Verify runs only afterwards, so the privileged read precedes any examination of the file. grep -rn Impersonat chrome/installer/ returns nothing.
The elevation service performs the same operation and impersonates for it. Its comment at elevated_recovery_impl.cc:192 states the reason, “Copy |from_crx_path| impersonated. This is to prevent us from copying files that may not be accessible to the calling COM user.”
LegacyAppCommandWebImpl::execute runs the command for whichever caller asks. functional_spec.md:595 opens the legacy COM classes to non-admin callers, and functional_spec.md:1905 names what the design relies on against “the threat of a non-admin attacker”, a secure location for the executable and an HKLM-defined command line. Neither constrains %1, which selects the file rather than the program.
SelfCleaningTempDir::Initialize creates the staging root with base::CreateDirectory and no security descriptor, so it inherits BUILTIN\Users:(I)(OI)(CI)(IO)(GR,GE) from C:\Program Files\Google\Chrome Dev. The comment at install_component.cc:66 calls this a “TOCTOU-Safe Staged copy into admin-protected temp directory”, which is not what a default install produces.
All line numbers are at abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2.
Suggested fix
Impersonate the RPC caller around the open, the way elevated_recovery_impl.cc does, and copy from the resulting handle. Creating the staging directory with an explicit DACL that omits BUILTIN\Users would independently close the read-back.
Impact analysis
Attack scenario
The attacker is an ordinary interactive user on a machine with system-level Chrome, able to run code as themselves but unable to elevate. Files whose ACL grants access only to SYSTEM and Administrators are beyond them.
They call IAppCommandWeb::execute with the path they want, then read the staged copy before the installer deletes it. Nothing is asked of the victim, and Chrome need not be running.
They obtain any file SYSTEM can open and they cannot, demonstrated on the Windows Security audit log, which holds logon and privilege records for every account on the host, and on a second account’s registry hive.
Preconditions
A system-level Google Chrome install carrying commit 8a2276d251f79 (2026-09-04). AddInstallComponentWorkItems registers the command only for a system-level install, with no channel gate, so any branded system-level install carries it once the commit ships. Today that means Dev.
The reproducing account is medium integrity, not elevated, with BUILTIN\Administrators deny-only.
The cause
What version of Chrome have you found the security issue in?
Chrome Dev 155.0.8048.0 with GoogleUpdater 152.0.7933.0 on Windows 11 (26200)
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Privilege Escalation
How would you like to be publicly acknowledged for your report?
M. Fauzan Wijaya (Gh05t666nero)
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://source.chromium.org/chromium/chromium/src/+/8a2276d251f79ab09d691583ec76d36907ea2140
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:chrome/elevation_service/elevated_recovery_impl.cc;l=192
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:chrome/installer/setup/install_component.cc;l=64
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:chrome/installer/setup/install_component.cc;l=66
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:chrome/installer/setup/install_component.cc;l=89
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:chrome/installer/setup/install_component.cc;l=96
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:chrome/installer/setup/install_worker.cc;l=573
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:chrome/installer/util/file_conductor.cc;l=184
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:chrome/installer/util/file_conductor.cc;l=81
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:chrome/installer/util/self_cleaning_temp_dir.cc;l=61
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:chrome/updater/app/server/win/com_classes_legacy.cc;l=1306
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:docs/updater/functional_spec.md;l=1905
- https://source.chromium.org/chromium/chromium/src/+/abc1fed3550d23acd53d4d7ed0b0c3cf9f4d27e2:docs/updater/functional_spec.md;l=595