Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Extensions
DescriptionMemory corruption in Extensions
ComponentExtensions
Sub-componentExtensions
Bug ClassMemory Corruption
Tracker501875966
Fix commit3368fb99c051 (chromium/src) +39/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
TEST_F
chrome/browser/extensions/extension_service_unittest.cc
modified

Files Changed

  • chrome/browser/extensions/extension_service_unittest.cc
  • extensions/browser/crx_installer.cc
  • chrome/browser/ash/policy/core/device_local_account_browsertest.cc
From 3368fb99c051b20be363061459d330119285dfd5 Mon Sep 17 00:00:00 2001
From: Devlin Cronin <rdevlin.cronin@chromium.org>
Date: Tue, 15 Sep 2026 13:31:18 -0700
Subject: [PATCH] [Extensions] Abort install for downloads when there's no window

When a CRX package installation is initiated via user download,
CrxInstaller::ConfirmInstall expects confirmation through an install
prompt client unless the extension is pre-approved. If no active tab
or browser window is available (such as on Android or when a tab is
closed during download), CreateExtensionInstallPrompt returns a null
prompt client. Previously, a null client caused the confirmation branch
check to evaluate to false, falling through to complete the installation
directly without prompt verification.

This wasn't that big a deal, since
a) the extension had no granted permissions, meaning it would end up
   immediately disabled in most cases, and
b) this was only triggerable if there was an offstore installation flow
   permitted (itself only allowed via enterprise policy)

But we should still fix it.

Update CrxInstaller::ConfirmInstall to validate the prompt client state,
aborting unapproved user-downloaded installations with USER_ABORTED if
no client is present.

Bug: 501875966
Change-Id: I1b973ec26095d07858553d3ae569c9e1c78a6c9e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8394279
Reviewed-by: Sergey Poromov <poromov@chromium.org>
Reviewed-by: Eva Su <evasu@chromium.org>
Commit-Queue: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1698344}
---
diff --git a/extensions/browser/crx_installer.cc b/extensions/browser/crx_installer.cc
index f683c18..4c5ec385 100644
--- a/extensions/browser/crx_installer.cc
+++ b/extensions/browser/crx_installer.cc
@@ -802,6 +802,18 @@
   current_version_ = base::Version(ExtensionPrefs::Get(browser_context_)
                                        ->GetVersionString(extension()->id()));
 
+  // User-downloaded installations require an install prompt client to confirm
+  // permissions unless pre-approved (e.g. from the webstore). If no prompt
+  // client is available (for example, if no browser window or tab is open),
+  // abort the install to avoid installing the extension silently. Non-user
+  // download flows (such as external providers) have their own separate
+  // acknowledgment lifecycles and may use CreateSilent() without a client.
+  if (was_triggered_by_user_download() && !client_ && !approved_) {
+    ReportFailureFromUIThread(CrxInstallError(
+        CrxInstallErrorType::OTHER, CrxInstallErrorDetail::USER_ABORTED));
+    return;
+  }
+
   if (client_ && (!allow_silent_install_ || !approved_) &&
       !update_from_settings_page_) {
     AddRef();  // Balanced in OnInstallPromptDone().
diff --git a/chrome/browser/ash/policy/core/device_local_account_browsertest.cc b/chrome/browser/ash/policy/core/device_local_account_browsertest.cc
index d4fe1cb..0c79820 100644
--- a/chrome/browser/ash/policy/core/device_local_account_browsertest.cc
+++ b/chrome/browser/ash/policy/core/device_local_account_browsertest.cc
@@ -1567,7 +1567,6 @@
   scoped_refptr<extensions::CrxInstaller> installer =
       extensions::CrxInstaller::CreateSilent(profile);
   installer->set_allow_silent_install(true);
-  installer->set_was_triggered_by_user_download();
   installer->set_creation_flags(extensions::Extension::FROM_WEBSTORE);
 
   {
diff --git a/chrome/browser/extensions/extension_service_unittest.cc b/chrome/browser/extensions/extension_service_unittest.cc
index 53f2e74..1fbfd24 100644
--- a/chrome/browser/extensions/extension_service_unittest.cc
+++ b/chrome/browser/extensions/extension_service_unittest.cc
@@ -119,6 +119,7 @@
 #include "extensions/browser/external_install_info.h"
 #include "extensions/browser/external_provider_interface.h"
 #include "extensions/browser/fake_safe_browsing_database_manager.h"
+#include "extensions/browser/install/crx_install_error.h"
 #include "extensions/browser/install_flag.h"
 #include "extensions/browser/load_error_reporter.h"
 #include "extensions/browser/managed_installation_mode.h"
@@ -4392,6 +4393,32 @@
   EXPECT_EQ(0u, registry()->enabled_extensions().size());
 }
 
+// Tests that a user-downloaded CRX installation without an install prompt
+// client fails and aborts the install (preventing silent installation).
+// Regression test for https://crbug.com/501875966.
+TEST_F(ExtensionServiceTest, UserDownloadWithoutPromptFails) {
+  InitializeEmptyExtensionService();
+
+  scoped_refptr<CrxInstaller> installer =
+      CrxInstaller::Create(profile(), /*client=*/nullptr);
+  installer->set_was_triggered_by_user_download();
+  installer->set_off_store_install_allow_reason(
+      CrxInstaller::OffStoreInstallAllowedInTest);
+
+  base::test::TestFuture<std::optional<CrxInstallError>> future;
+  installer->AddInstallerCallback(
+      future.GetCallback<const std::optional<CrxInstallError>&>());
+  installer->InstallCrx(data_dir().AppendASCII("good.crx"));
+
+  std::optional<CrxInstallError> install_error = future.Get();
+  EXPECT_TRUE(installer->did_handle_successfully());
+  ASSERT_TRUE(install_error.has_value());
+  EXPECT_EQ(CrxInstallErrorType::OTHER, install_error->type());
+  EXPECT_EQ(CrxInstallErrorDetail::USER_ABORTED, install_error->detail());
+
+  EXPECT_FALSE(registry()->GetInstalledExtension(kGoodCrx));
+}
+
 // Tests that extensions cannot be loaded from prefs if the policy provider
 // prohibits it. This functionality is implemented in InstalledLoader::Load().
 TEST_F(ExtensionServiceTest, ManagementPolicyProhibitsLoadFromPrefs) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ash/policy/core/device_local_account_browsertest.cc b/chrome/browser/ash/policy/core/device_local_account_browsertest.cc
index d4fe1cb..0c79820 100644
--- a/chrome/browser/ash/policy/core/device_local_account_browsertest.cc
+++ b/chrome/browser/ash/policy/core/device_local_account_browsertest.cc
@@ -1567,7 +1567,6 @@
   scoped_refptr<extensions::CrxInstaller> installer =
       extensions::CrxInstaller::CreateSilent(profile);
   installer->set_allow_silent_install(true);
-  installer->set_was_triggered_by_user_download();
   installer->set_creation_flags(extensions::Extension::FROM_WEBSTORE);
 
   {
diff --git a/chrome/browser/extensions/extension_service_unittest.cc b/chrome/browser/extensions/extension_service_unittest.cc
index 53f2e74..1fbfd24 100644
--- a/chrome/browser/extensions/extension_service_unittest.cc
+++ b/chrome/browser/extensions/extension_service_unittest.cc
@@ -119,6 +119,7 @@
 #include "extensions/browser/external_install_info.h"
 #include "extensions/browser/external_provider_interface.h"
 #include "extensions/browser/fake_safe_browsing_database_manager.h"
+#include "extensions/browser/install/crx_install_error.h"
 #include "extensions/browser/install_flag.h"
 #include "extensions/browser/load_error_reporter.h"
 #include "extensions/browser/managed_installation_mode.h"
@@ -4392,6 +4393,32 @@
   EXPECT_EQ(0u, registry()->enabled_extensions().size());
 }
 
+// Tests that a user-downloaded CRX installation without an install prompt
+// client fails and aborts the install (preventing silent installation).
+// Regression test for https://crbug.com/501875966.
+TEST_F(ExtensionServiceTest, UserDownloadWithoutPromptFails) {
+  InitializeEmptyExtensionService();
+
+  scoped_refptr<CrxInstaller> installer =
+      CrxInstaller::Create(profile(), /*client=*/nullptr);
+  installer->set_was_triggered_by_user_download();
+  installer->set_off_store_install_allow_reason(
+      CrxInstaller::OffStoreInstallAllowedInTest);
+
+  base::test::TestFuture<std::optional<CrxInstallError>> future;
+  installer->AddInstallerCallback(
+      future.GetCallback<const std::optional<CrxInstallError>&>());
+  installer->InstallCrx(data_dir().AppendASCII("good.crx"));
+
+  std::optional<CrxInstallError> install_error = future.Get();
+  EXPECT_TRUE(installer->did_handle_successfully());
+  ASSERT_TRUE(install_error.has_value());
+  EXPECT_EQ(CrxInstallErrorType::OTHER, install_error->type());
+  EXPECT_EQ(CrxInstallErrorDetail::USER_ABORTED, install_error->detail());
+
+  EXPECT_FALSE(registry()->GetInstalledExtension(kGoodCrx));
+}
+
 // Tests that extensions cannot be loaded from prefs if the policy provider
 // prohibits it. This functionality is implemented in InstalledLoader::Load().
 TEST_F(ExtensionServiceTest, ManagementPolicyProhibitsLoadFromPrefs) {
Loading diff…

Original Bug Report

reported by vm...@google.com

Silent installation of disabled extensions on Desktop Android

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.

Overview: A logic error in the extension installation flow on Desktop Android allows for the silent installation of CRX files if the browser window is closed during the download. However, because the installation bypasses the consent prompt, the browser detects an unapproved privilege increase and safely installs the extension in a disabled state.

Affected files:

  • chrome/browser/download/download_crx_util.cc
  • extensions/browser/crx_installer.cc
  • chrome/browser/download/chrome_download_manager_delegate.cc
  • components/download/internal/common/download_item_impl.cc

Estimated timestamp from git blame: 2026-02-24

Description

On Desktop Android builds (is_desktop_android=true), a logic flaw exists in how extension installations are handled when no active browser window is present. This can occur if a user initiates a download of an extension but backgrounds or closes the Chrome activity before the download completes.

The vulnerability is reachable if the user’s device is managed by the ExtensionInstallSources enterprise policy, which allows extensions to be downloaded and installed from specific non-Webstore origins.

When the background download completes, ChromeDownloadManagerDelegate::ShouldOpenDownload initiates the installation process. It attempts to construct a user consent prompt via download_crx_util::CreateExtensionInstallPrompt. Because the Chrome activity was destroyed, content::DownloadItemUtils::GetWebContents and the fallback extensions::browser_window_util::GetLastActiveBrowserWithProfile both fail to find an active window. On Android, this specific scenario intentionally returns a nullptr instead of a valid prompt interface.

The CrxInstaller receives this nullptr as its client_. Later, during CrxInstaller::ConfirmInstall (extensions/browser/crx_installer.cc), the code checks if it should display the prompt:

  if (client_ && (!allow_silent_install_ || !approved_) &&
      !update_from_settings_page_) {
    // ... show prompt
  } else {
    UpdateCreationFlagsAndCompleteInstall(kDontWithholdPermissions);
  }

Because client_ is null, the code falls into the else block, completely bypassing the installation prompt and proceeding with the installation.

While this results in a silent installation, the browser’s safety mechanisms prevent immediate code execution. In CrxInstaller::ReportSuccessFromUIThread, the explicit granting of permissions is skipped because both client_ is null and allow_silent_install_ is false. Consequently, when ExtensionRegistrar registers the new extension, ChromeExtensionRegistrarDelegate::CheckPermissionsIncrease compares the requested permissions (e.g., <all_urls>) against the empty set of granted permissions. This triggers a privilege increase warning, causing the extension to be installed with the DISABLE_PERMISSIONS_INCREASE reason.

Impact

An attacker can silently install a malicious extension onto the user’s profile without their consent. However, the extension is installed in a disabled state and cannot execute background scripts or access web content. To achieve Universal Cross-Site Scripting (UXSS) or code execution, the attacker would still need the user to manually navigate to chrome://extensions, enable the extension, and accept the subsequent permission prompt. The primary impact is silent persistence on the device.

Potential Reproduction Steps

Note: These are suggested steps based on static code analysis; our tooling agent does not have the ability to run live code.

  1. Use a Desktop Android build (is_desktop_android=true).
  2. Enroll the device with the ExtensionInstallSources enterprise policy, allowlisting an attacker-controlled origin (e.g., https://attacker.example/*).
  3. Host a self-signed CRX3 file requesting broad host permissions at the allowlisted origin.
  4. Have the user tap a link to trigger the download of the CRX.
  5. While the download is in progress, background or swipe away the Chrome activity to destroy the window while the background download service continues.
  6. Once the download completes, CrxInstaller processes the file. Due to the lack of an active window, it bypasses the prompt and silently installs the extension.
  7. Observe that the extension is added to the profile but is disabled due to a permission increase.

Proposed Fix

Modify CrxInstaller::ConfirmInstall in extensions/browser/crx_installer.cc to handle a null client_ more securely when allow_silent_install_ is false. Instead of falling through to complete the installation, the installation should be aborted or deferred until a valid browser window is available to present the prompt.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results 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