Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Extensions
DescriptionUse after free in Extensions
ComponentExtensions
Sub-componentExtensions
Bug ClassUAF
Tracker517486906
Fix commit32e8f67f7b5a (chromium/src) +389/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
for
chrome/browser/ui/extensions/extension_enable_flow.cc
modified
if
chrome/browser/ui/extensions/extension_enable_flow.cc
modified

Files Changed

  • chrome/browser/ui/extensions/extension_enable_flow_browsertest.cc
  • chrome/browser/ui/extensions/extension_enable_flow.h
  • chrome/browser/ui/extensions/extension_enable_flow.cc
From 32e8f67f7b5a58b1ea41e0bd8626b4e2f5028678 Mon Sep 17 00:00:00 2001
From: Eva Su <[email protected]>
Date: Wed, 16 Sep 2026 14:17:14 -0700
Subject: [PATCH] [Extensions] Validate supported disable reasons in ExtensionEnableFlow

ExtensionEnableFlow is intended to handle re-enabling extensions
disabled due to user action, permission increases, remote installs, or
custodian approval requirements. Previously, if an extension had other
disable reasons present, the flow could proceed and clear those reasons
upon enabling.

This change adds a validation check in ExtensionEnableFlow to ensure
that an extension only contains disable reasons supported by this flow
before proceeding. If unsupported disable reasons are present or the
extension is not found in the disabled extensions registry, the enable
flow is aborted.

Fixed: 517486906
Change-Id: I034c6479bbf24b8219c529cdd8bbfabd53386ea1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8414890
Commit-Queue: Eva Su <[email protected]>
Reviewed-by: James Cook <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1699261}
---
diff --git a/chrome/browser/ui/extensions/extension_enable_flow.cc b/chrome/browser/ui/extensions/extension_enable_flow.cc
index 4134275d..127e7295 100644
--- a/chrome/browser/ui/extensions/extension_enable_flow.cc
+++ b/chrome/browser/ui/extensions/extension_enable_flow.cc
@@ -16,6 +16,7 @@
 #include "chrome/browser/supervised_user/supervised_user_browser_utils.h"
 #include "chrome/browser/ui/extensions/extension_enable_flow_delegate.h"
 #include "extensions/browser/api/management/management_api.h"
+#include "extensions/browser/disable_reason.h"
 #include "extensions/browser/extension_prefs.h"
 #include "extensions/browser/extension_registrar.h"
 #include "extensions/browser/extension_system.h"
@@ -28,6 +29,28 @@
 using extensions::Extension;
 using extensions::InstallPromptData;
 
+namespace {
+
+// Returns whether the extension only has disable reasons that can be resolved
+// through this flow (e.g. user action, permissions increase, remote install,
+// or custodian approval).
+bool HasOnlySupportedDisableReasons(
+    const extensions::DisableReasonSet& disable_reasons) {
+  for (extensions::disable_reason::DisableReason reason : disable_reasons) {
+    if (reason != extensions::disable_reason::DISABLE_NONE &&
+        reason != extensions::disable_reason::DISABLE_USER_ACTION &&
+        reason != extensions::disable_reason::DISABLE_PERMISSIONS_INCREASE &&
+        reason != extensions::disable_reason::DISABLE_REMOTE_INSTALL &&
+        reason !=
+            extensions::disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED) {
+      return false;
+    }
+  }
+  return true;
+}
+
+}  // namespace
+
 ExtensionEnableFlow::ExtensionEnableFlow(Profile* profile,
                                          const std::string& extension_id,
                                          ExtensionEnableFlowDelegate* delegate)
@@ -89,6 +112,19 @@
   auto* registry = extensions::ExtensionRegistry::Get(profile_);
   const Extension* extension =
       registry->disabled_extensions().GetByID(extension_id_);
+  if (!extension) {
+    delegate_->ExtensionEnableFlowAborted(
+        /*user_initiated=*/false);  // |delegate_| may delete us.
+    return;
+  }
+
+  extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
+  if (!HasOnlySupportedDisableReasons(
+          prefs->GetDisableReasons(extension_id_))) {
+    delegate_->ExtensionEnableFlowAborted(
+        /*user_initiated=*/false);  // |delegate_| may delete us.
+    return;
+  }
 
   extensions::SupervisedUserExtensionsDelegate*
       supervised_user_extensions_delegate =
@@ -96,8 +132,7 @@
               ->Get(profile_)
               ->GetSupervisedUserExtensionsDelegate();
   DCHECK(supervised_user_extensions_delegate);
-  if (supervised_user::AreExtensionsPermissionsEnabled(profile_) && extension &&
-
+  if (supervised_user::AreExtensionsPermissionsEnabled(profile_) &&
       // Only ask for parent approval if the extension still requires approval.
       !supervised_user_extensions_delegate->IsExtensionAllowedByParent(
           *extension)) {
@@ -111,11 +146,8 @@
     return;
   }
 
-  bool abort =
-      !extension ||
-      // The extension might be force-disabled by policy.
-      system->management_policy()->MustRemainDisabled(extension, nullptr);
-  if (abort) {
+  // The extension might be force-disabled by policy.
+  if (system->management_policy()->MustRemainDisabled(extension, nullptr)) {
     delegate_->ExtensionEnableFlowAborted(
         /*user_initiated=*/false);  // |delegate_| may delete us.
     return;
@@ -130,7 +162,6 @@
     return;
   }
 
-  extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
   if (!prefs->DidExtensionEscalatePermissions(extension_id_)) {
     // Enable the extension immediately if its privileges weren't escalated.
     // This is a no-op if the extension was previously terminated.
@@ -244,6 +275,17 @@
     supervised_user_extensions_delegate->RecordExtensionEnablementUmaMetrics(
         /*enabled=*/true);
   }
+
+  auto* system = extensions::ExtensionSystem::Get(profile_);
+  extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
+  if (system->management_policy()->MustRemainDisabled(extension, nullptr) ||
+      !HasOnlySupportedDisableReasons(
+          prefs->GetDisableReasons(extension_id_))) {
+    delegate_->ExtensionEnableFlowAborted(
+        /*user_initiated=*/false);  // |delegate_| may delete us.
+    return;
+  }
+
   auto* registrar = extensions::ExtensionRegistrar::Get(profile_);
   registrar->GrantPermissionsAndEnableExtension(*extension);
 
diff --git a/chrome/browser/ui/extensions/extension_enable_flow.h b/chrome/browser/ui/extensions/extension_enable_flow.h
index 386e454..c5196f4 100644
--- a/chrome/browser/ui/extensions/extension_enable_flow.h
+++ b/chrome/browser/ui/extensions/extension_enable_flow.h
@@ -69,10 +69,12 @@
   // CheckPermissionAndMaybePromptUser finish the flow.
   void Run();
 
-  // Checks if there is permission escalation while the extension is
-  // disabled/terminated. If no, enables the extension and notify |delegate_|
-  // synchronously. Otherwise, creates an ExtensionInstallPrompt and asks user
-  // to confirm.
+  // Checks if the extension can be re-enabled and whether user confirmation is
+  // needed. If privileges weren't escalated and the extension only has
+  // supported disable reasons, enables the extension and notifies |delegate_|
+  // synchronously. If permissions need to be confirmed, creates an
+  // ExtensionInstallPrompt and asks user to confirm. If other disable reasons
+  // are present, aborts the flow.
   void CheckPermissionAndMaybePromptUser();
 
   // Creates an ExtensionInstallPrompt in |prompt_| with the given prompt data.
diff --git a/chrome/browser/ui/extensions/extension_enable_flow_browsertest.cc b/chrome/browser/ui/extensions/extension_enable_flow_browsertest.cc
index 54c1621..fe2075df 100644
--- a/chrome/browser/ui/extensions/extension_enable_flow_browsertest.cc
+++ b/chrome/browser/ui/extensions/extension_enable_flow_browsertest.cc
@@ -13,6 +13,7 @@
 #include "content/public/test/browser_test.h"
 #include "extensions/browser/disable_reason.h"
 #include "extensions/browser/extension_dialog_auto_confirm.h"
+#include "extensions/browser/extension_prefs.h"
 #include "extensions/browser/extension_registrar.h"
 #include "extensions/browser/extension_registry.h"
 #include "extensions/browser/extension_system.h"
@@ -90,3 +91,335 @@
     management_policy->UnregisterProvider(&test_provider);
   }
 }
+
+// Test that trying to enable an extension that is disabled due to greylist
+// fails and leaves the extension disabled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       GreylistedExtensionRemainsDisabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("greylisted-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_GREYLIST});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_GREYLIST));
+
+  extensions::ScopedTestDialogAutoConfirm auto_confirm(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::ABORTED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_GREYLIST));
+}
+
+// Test that trying to enable an extension that is disabled due to allowlist
+// enforcement fails and leaves the extension disabled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       NotAllowlistedExtensionRemainsDisabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("not-allowlisted-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_NOT_ALLOWLISTED});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_NOT_ALLOWLISTED));
+
+  extensions::ScopedTestDialogAutoConfirm auto_confirm(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::ABORTED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_NOT_ALLOWLISTED));
+}
+
+// Test that trying to enable a corrupted extension fails and leaves the
+// extension disabled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       CorruptedExtensionRemainsDisabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("corrupted-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_CORRUPTED});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_CORRUPTED));
+
+  extensions::ScopedTestDialogAutoConfirm auto_confirm(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::ABORTED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_CORRUPTED));
+}
+
+// Test that trying to enable an extension with unsupported requirements fails
+// and leaves the extension disabled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       UnsupportedRequirementExtensionRemainsDisabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("unsupported-req-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_UNSUPPORTED_REQUIREMENT});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_UNSUPPORTED_REQUIREMENT));
+
+  extensions::ScopedTestDialogAutoConfirm auto_confirm(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::ABORTED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_UNSUPPORTED_REQUIREMENT));
+}
+
+// Test that an extension disabled only by user action can be enabled directly
+// without prompting.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       UserActionDisabledExtensionCanBeEnabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("user-action-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_USER_ACTION});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasOnlyDisableReason(
+      id, extensions::disable_reason::DISABLE_USER_ACTION));
+
+  // Even if dialogs are set to CANCEL, no dialog should be shown for only
+  // user action disable reason.
+  extensions::ScopedTestDialogAutoConfirm auto_cancel(
+      extensions::ScopedTestDialogAutoConfirm::CANCEL);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::FINISHED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->GetDisableReasons(id).empty());
+}
+
+// Test that an extension disabled by both user action and greylist cannot be
+// enabled and remains disabled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       CombinedUserActionAndGreylistExtensionRemainsDisabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("user-action-greylist-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_USER_ACTION,
+           extensions::disable_reason::DISABLE_GREYLIST});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_GREYLIST));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_USER_ACTION));
+
+  extensions::ScopedTestDialogAutoConfirm auto_confirm(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::ABORTED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_GREYLIST));
+}
+
+// Test that an extension disabled for permissions increase shows a prompt and
+// can be enabled when the prompt is accepted.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       PermissionsIncreaseExtensionAcceptEnables) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("permissions-increase-extension").Build();
+  extension_registrar()->AddExtension(extension);
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/extensions/extension_enable_flow_browsertest.cc b/chrome/browser/ui/extensions/extension_enable_flow_browsertest.cc
index 54c1621..fe2075df 100644
--- a/chrome/browser/ui/extensions/extension_enable_flow_browsertest.cc
+++ b/chrome/browser/ui/extensions/extension_enable_flow_browsertest.cc
@@ -13,6 +13,7 @@
 #include "content/public/test/browser_test.h"
 #include "extensions/browser/disable_reason.h"
 #include "extensions/browser/extension_dialog_auto_confirm.h"
+#include "extensions/browser/extension_prefs.h"
 #include "extensions/browser/extension_registrar.h"
 #include "extensions/browser/extension_registry.h"
 #include "extensions/browser/extension_system.h"
@@ -90,3 +91,335 @@
     management_policy->UnregisterProvider(&test_provider);
   }
 }
+
+// Test that trying to enable an extension that is disabled due to greylist
+// fails and leaves the extension disabled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       GreylistedExtensionRemainsDisabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("greylisted-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_GREYLIST});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_GREYLIST));
+
+  extensions::ScopedTestDialogAutoConfirm auto_confirm(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::ABORTED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_GREYLIST));
+}
+
+// Test that trying to enable an extension that is disabled due to allowlist
+// enforcement fails and leaves the extension disabled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       NotAllowlistedExtensionRemainsDisabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("not-allowlisted-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_NOT_ALLOWLISTED});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_NOT_ALLOWLISTED));
+
+  extensions::ScopedTestDialogAutoConfirm auto_confirm(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::ABORTED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_NOT_ALLOWLISTED));
+}
+
+// Test that trying to enable a corrupted extension fails and leaves the
+// extension disabled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       CorruptedExtensionRemainsDisabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("corrupted-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_CORRUPTED});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_CORRUPTED));
+
+  extensions::ScopedTestDialogAutoConfirm auto_confirm(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::ABORTED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_CORRUPTED));
+}
+
+// Test that trying to enable an extension with unsupported requirements fails
+// and leaves the extension disabled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       UnsupportedRequirementExtensionRemainsDisabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("unsupported-req-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_UNSUPPORTED_REQUIREMENT});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_UNSUPPORTED_REQUIREMENT));
+
+  extensions::ScopedTestDialogAutoConfirm auto_confirm(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::ABORTED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_UNSUPPORTED_REQUIREMENT));
+}
+
+// Test that an extension disabled only by user action can be enabled directly
+// without prompting.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       UserActionDisabledExtensionCanBeEnabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("user-action-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_USER_ACTION});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasOnlyDisableReason(
+      id, extensions::disable_reason::DISABLE_USER_ACTION));
+
+  // Even if dialogs are set to CANCEL, no dialog should be shown for only
+  // user action disable reason.
+  extensions::ScopedTestDialogAutoConfirm auto_cancel(
+      extensions::ScopedTestDialogAutoConfirm::CANCEL);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::FINISHED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->GetDisableReasons(id).empty());
+}
+
+// Test that an extension disabled by both user action and greylist cannot be
+// enabled and remains disabled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       CombinedUserActionAndGreylistExtensionRemainsDisabled) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("user-action-greylist-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_USER_ACTION,
+           extensions::disable_reason::DISABLE_GREYLIST});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_GREYLIST));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_USER_ACTION));
+
+  extensions::ScopedTestDialogAutoConfirm auto_confirm(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::ABORTED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_GREYLIST));
+}
+
+// Test that an extension disabled for permissions increase shows a prompt and
+// can be enabled when the prompt is accepted.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       PermissionsIncreaseExtensionAcceptEnables) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("permissions-increase-extension").Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_PERMISSIONS_INCREASE});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_PERMISSIONS_INCREASE));
+
+  extensions::ScopedTestDialogAutoConfirm auto_accept(
+      extensions::ScopedTestDialogAutoConfirm::ACCEPT);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
+  EXPECT_TRUE(delegate.result().has_value());
+  EXPECT_EQ(ExtensionEnableFlowTestDelegate::FINISHED, *delegate.result());
+
+  EXPECT_TRUE(extension_registry()->enabled_extensions().Contains(id));
+  EXPECT_FALSE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->GetDisableReasons(id).empty());
+}
+
+// Test that an extension disabled for permissions increase aborts and remains
+// disabled when the prompt is canceled.
+IN_PROC_BROWSER_TEST_F(ExtensionEnableFlowTest,
+                       PermissionsIncreaseExtensionCancelAborts) {
+  scoped_refptr<const extensions::Extension> extension =
+      extensions::ExtensionBuilder("permissions-increase-cancel-extension")
+          .Build();
+  extension_registrar()->AddExtension(extension);
+  const extensions::ExtensionId id = extension->id();
+
+  extension_registrar()->DisableExtension(
+      id, {extensions::disable_reason::DISABLE_PERMISSIONS_INCREASE});
+
+  extensions::ExtensionPrefs* prefs =
+      extensions::ExtensionPrefs::Get(profile());
+  EXPECT_TRUE(extension_registry()->disabled_extensions().Contains(id));
+  EXPECT_TRUE(prefs->HasDisableReason(
+      id, extensions::disable_reason::DISABLE_PERMISSIONS_INCREASE));
+
+  extensions::ScopedTestDialogAutoConfirm auto_cancel(
+      extensions::ScopedTestDialogAutoConfirm::CANCEL);
+
+  ExtensionEnableFlowTestDelegate delegate;
+  ExtensionEnableFlow enable_flow(profile(), id, &delegate);
+  enable_flow.StartForWebContents(
+      browser()->GetTabStripModel()->GetActiveWebContents());
+  delegate.Wait();
+
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

ExtensionEnableFlow silently re-enables extensions disabled by Safe-Browsing and allowlists

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic flaw in ExtensionEnableFlow allows disabled extensions to be silently re-enabled without user prompt or warning when launched via specific entry points. If an extension’s permissions have not escalated, the enable flow bypasses prompts and unconditionally clears all disable reasons, permanently neutralizing Safe-Browsing and Enhanced Safe-Browsing protections.

Affected files:

  • chrome/browser/ui/extensions/extension_enable_flow.cc

Estimated timestamp from git blame: 2013-01-08

Root Cause Analysis

In chrome/browser/ui/extensions/extension_enable_flow.cc, the function ExtensionEnableFlow::CheckPermissionAndMaybePromptUser() decides if an extension can be re-enabled without showing a dialog to the user. It performs this decision by checking DidExtensionEscalatePermissions():

  extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
  if (!prefs->DidExtensionEscalatePermissions(extension_id_)) {
    // Enable the extension immediately if its privileges weren't escalated.
    registrar->EnableExtension(extension_id_);
    delegate_->ExtensionEnableFlowFinished();
    return;
  }

However, DidExtensionEscalatePermissions() (defined in extensions/browser/extension_prefs.cc:1025) only checks for DISABLE_PERMISSIONS_INCREASE or DISABLE_REMOTE_INSTALL. It does not account for critical security-related disable reasons such as Safe-Browsing greylisting (DISABLE_GREYLIST) or Enhanced Safe-Browsing CRX-allowlist enforcement (DISABLE_NOT_ALLOWLISTED).

When registrar->EnableExtension(extension_id_) is called, it unconditionally calls extension_prefs_->ClearDisableReasons(extension_id) (in extensions/browser/extension_registrar.cc:354), wiping out all security-related disable reasons.

Furthermore, when the extension is enabled, ExtensionAllowlist::OnExtensionStateChanged() (in extensions/browser/extension_allowlist.cc:405) observes the change and permanently transitions the acknowledge state to ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER, which prevents Enhanced Safe Browsing from ever disabling the extension in the future. For DISABLE_GREYLIST, the greylist state was already marked as acknowledged when the reason was added, meaning background scanning will not re-disable it.

(Note: Although the flow also clears DISABLE_CORRUPTED, any actual file corruption on disk is quickly detected during the load/activation phase by ChromeContentVerifierDelegate::OnExtensionCorruptionDetected(), which will immediately re-disable the extension with DISABLE_CORRUPTED. Thus, the silent bypass is persistent for DISABLE_GREYLIST and DISABLE_NOT_ALLOWLISTED, but temporary/blocked for actual corruptions).

Potential Trigger Steps

  1. An attacker convinces a user to install a Chrome App/Extension.
  2. Safe Browsing or Enhanced Safe Browsing subsequently flags the extension as malicious or unallowlisted, and the browser disables it (setting DISABLE_GREYLIST or DISABLE_NOT_ALLOWLISTED).
  3. When the user attempts to launch the disabled app via normal desktop/OS entry points—such as clicking its icon in the ChromeOS Launcher/Shelf, double-clicking its App-Shim on macOS, or executing via command-line flags—the launch request instantiates ExtensionEnableFlow.
  4. Due to the lack of permission escalation, the app is silently and immediately re-enabled. All disable reasons are cleared, and Safe Browsing/ESB protections are persistently bypassed.

Suggested Fix

Instead of unconditionally clearing all disable reasons with registrar->EnableExtension(), ExtensionEnableFlow should use a targeted mechanism (such as ExtensionRegistrar::RemoveDisableReasonAndMaybeEnable(extension_id, disable_reason::DISABLE_USER_ACTION)) to ensure that security-related disable reasons are preserved unless explicitly overridden by a specialized prompt.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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