Low chrome UAF 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebAppInstalls
DescriptionUse after free in WebAppInstalls
ComponentWebAppInstalls
Sub-componentCustom handlers
Bug ClassUAF
Tracker553146562
Fix commita5b5230c1e25 (chromium/src) +244/-67
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/content_settings/content_setting_bubble_model.cc
modified

Files Changed

  • chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
  • components/custom_handlers/protocol_handler_registry_unittest.cc
  • chrome/browser/ui/content_settings/content_setting_bubble_model.h
  • chrome/browser/content_settings/page_specific_content_settings_delegate.h
  • components/custom_handlers/protocol_handler_registry.cc
  • chrome/browser/custom_handlers/protocol_handler_registry_browsertest.cc
  • chrome/browser/custom_handlers/BUILD.gn
  • components/custom_handlers/protocol_handler_registry.h
  • chrome/browser/ui/views/location_bar/content_setting_bubble_dialog_browsertest.cc
  • chrome/browser/ui/content_settings/content_setting_bubble_model.cc
From a5b5230c1e25b752ccc25943b4f4c681bc6f4531 Mon Sep 17 00:00:00 2001
From: Javier Fernández García-Boente <[email protected]>
Date: Tue, 22 Sep 2026 04:18:56 -0700
Subject: [PATCH] [custom handlers] Only Allow or Deny options in the settings bubble

The UnregisterProtocolHandler method has a misleading name. In fact, the
handler was not unregistered, but registered without being assigned as
default. It's removed in this CL to avoid further confusion.

The Ignore option is removed from the content settings bubble and its
action, the ProtocolHandlerRegistry::OnIgnoreRegisterProtocolHandler
method, is associated now to the Deny option.

With this change we lose one use case that I believe could be useful for
the user; queuing legit protocol handlers so that we have some available
when the default is removed. However, we can accept it for now in order
to solve a security bug.

Bug: 553146562
Change-Id: I99bf2d5c016d9870589f91eac35e2da861bc6bf7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8379027
Reviewed-by: Antonio Sartori <[email protected]>
Commit-Queue: Javier Fernandez <[email protected]>
Reviewed-by: Ted Choc <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1702483}
---
diff --git a/chrome/browser/content_settings/page_specific_content_settings_delegate.h b/chrome/browser/content_settings/page_specific_content_settings_delegate.h
index 269116b..5e96fc4 100644
--- a/chrome/browser/content_settings/page_specific_content_settings_delegate.h
+++ b/chrome/browser/content_settings/page_specific_content_settings_delegate.h
@@ -114,6 +114,11 @@
   // The previous protocol handler to be replaced by
   // the pending_protocol_handler_, if there is one. Empty if
   // there is no handler which would be replaced.
+  // TODO(crbug.com/553146562): No longer read. ContentSettingRPHBubbleModel
+  // takes the handler being replaced from the registry when it opens, since
+  // this snapshot went stale whenever the registry changed while the request
+  // was pending. Remove it, its accessors, and the write in
+  // BrowserWebContentsDelegate::RegisterProtocolHandler().
   custom_handlers::ProtocolHandler previous_protocol_handler_ =
       custom_handlers::ProtocolHandler::EmptyProtocolHandler();
 
diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model.cc
index 046bc71..1ddac21 100644
--- a/chrome/browser/ui/content_settings/content_setting_bubble_model.cc
+++ b/chrome/browser/ui/content_settings/content_setting_bubble_model.cc
@@ -523,12 +523,20 @@
 
 namespace {
 
-// These states must match the order of appearance of the radio buttons
-// in the XIB file for the Mac port.
+// These states must match the order of the radio buttons.
+//
+// TODO(crbug.com/553146562): The bubble offers only "Allow" (register as the
+// default) and "Deny" (refuse). The registry also supports registering a
+// handler as a non-default option the user can pick later in settings
+// (OnDenyRegisterProtocolHandler(), despite its name); the bubble used to
+// reach it through a button labelled "Deny", a grant under a refusal's name.
+// Offering it again needs an honestly labelled third option, i.e. new
+// strings. Until then IDS_REGISTER_PROTOCOL_HANDLER_IGNORE has no user, and
+// the names below predate the change: RPH_BLOCK is the "Deny" radio, and
+// IgnoreProtocolHandler() performs its refusal.
 enum RPHState {
   RPH_ALLOW = 0,
   RPH_BLOCK,
-  RPH_IGNORE,
 };
 
 }  // namespace
@@ -542,18 +550,21 @@
                                       ContentSettingsType::PROTOCOL_HANDLERS),
       registry_(registry),
       pending_handler_(
-          custom_handlers::ProtocolHandler::EmptyProtocolHandler()),
-      previous_handler_(
           custom_handlers::ProtocolHandler::EmptyProtocolHandler()) {
   auto* content_settings =
       PageSpecificContentSettingsDelegate::FromWebContents(web_contents());
   pending_handler_ = content_settings->pending_protocol_handler();
-  previous_handler_ = content_settings->previous_protocol_handler();
 
   std::u16string protocol = pending_handler_.GetProtocolDisplayName();
 
-  // Note that we ignore the |title| parameter.
-  if (previous_handler_.IsEmpty()) {
+  // The handler being replaced is read from the registry rather than
+  // remembered from when the request arrived: the registry may have changed
+  // since, for instance through chrome://settings/handlers. Note that we
+  // ignore the |title| parameter.
+  const custom_handlers::ProtocolHandler& current_default =
+      registry_ ? registry_->GetHandlerFor(pending_handler_.protocol())
+                : custom_handlers::ProtocolHandler::EmptyProtocolHandler();
+  if (current_default.IsEmpty()) {
     set_title(l10n_util::GetStringFUTF16(
         IDS_REGISTER_PROTOCOL_HANDLER_CONFIRM,
         base::UTF8ToUTF16(pending_handler_.url().GetHost()), protocol));
@@ -561,29 +572,24 @@
     set_title(l10n_util::GetStringFUTF16(
         IDS_REGISTER_PROTOCOL_HANDLER_CONFIRM_REPLACE,
         base::UTF8ToUTF16(pending_handler_.url().GetHost()), protocol,
-        base::UTF8ToUTF16(previous_handler_.url().GetHost())));
+        base::UTF8ToUTF16(current_default.url().GetHost())));
   }
 
   std::u16string radio_allow_label =
       l10n_util::GetStringUTF16(IDS_REGISTER_PROTOCOL_HANDLER_ACCEPT);
   std::u16string radio_deny_label =
       l10n_util::GetStringUTF16(IDS_REGISTER_PROTOCOL_HANDLER_DENY);
-  std::u16string radio_ignore_label =
-      l10n_util::GetStringUTF16(IDS_REGISTER_PROTOCOL_HANDLER_IGNORE);
 
   const GURL& url = GetPage().GetMainDocument().GetLastCommittedURL();
   RadioGroup radio_group;
   radio_group.url = url;
 
-  radio_group.radio_items = {radio_allow_label, radio_deny_label,
-                             radio_ignore_label};
+  radio_group.radio_items = {radio_allow_label, radio_deny_label};
   ContentSetting setting = content_settings->pending_protocol_handler_setting();
   if (setting == CONTENT_SETTING_ALLOW) {
     radio_group.default_item = RPH_ALLOW;
-  } else if (setting == CONTENT_SETTING_BLOCK) {
-    radio_group.default_item = RPH_BLOCK;
   } else {
-    radio_group.default_item = RPH_IGNORE;
+    radio_group.default_item = RPH_BLOCK;
   }
 
   set_radio_group(radio_group);
@@ -604,6 +610,10 @@
 void ContentSettingRPHBubbleModel::RegisterProtocolHandler() {
   // A no-op if the handler hasn't been ignored, but needed in case the user
   // selects sequences like register/ignore/register.
+  // TODO(crbug.com/553146562): Move this into
+  // OnAcceptRegisterProtocolHandler(), so that accepting revokes an earlier
+  // refusal from any surface, e.g. "Set as default" in
+  // chrome://settings/handlers, and not only from this bubble.
   registry_->RemoveIgnoredHandler(pending_handler_);
 
   registry_->OnAcceptRegisterProtocolHandler(pending_handler_);
@@ -611,34 +621,19 @@
       ->set_pending_protocol_handler_setting(CONTENT_SETTING_ALLOW);
 }
 
-void ContentSettingRPHBubbleModel::UnregisterProtocolHandler() {
-  registry_->OnDenyRegisterProtocolHandler(pending_handler_);
-  PageSpecificContentSettingsDelegate::FromWebContents(web_contents())
-      ->set_pending_protocol_handler_setting(CONTENT_SETTING_BLOCK);
-  ClearOrSetPreviousHandler();
-}
-
 void ContentSettingRPHBubbleModel::IgnoreProtocolHandler() {
+  // Refusing also undoes an "Allow" chosen earlier in the same bubble: the
+  // registry removes the handler and hands the default back to the previous
+  // one, so nothing has to be remembered or restored here.
   registry_->OnIgnoreRegisterProtocolHandler(pending_handler_);
   PageSpecificContentSettingsDelegate::FromWebContents(web_contents())
-      ->set_pending_protocol_handler_setting(CONTENT_SETTING_DEFAULT);
-  ClearOrSetPreviousHandler();
-}
-
-void ContentSettingRPHBubbleModel::ClearOrSetPreviousHandler() {
-  if (previous_handler_.IsEmpty()) {
-    registry_->ClearDefault(pending_handler_.protocol());
-  } else {
-    registry_->OnAcceptRegisterProtocolHandler(previous_handler_);
-  }
+      ->set_pending_protocol_handler_setting(CONTENT_SETTING_BLOCK);
 }
 
 void ContentSettingRPHBubbleModel::PerformActionForSelectedItem() {
   if (selected_item() == RPH_ALLOW) {
     RegisterProtocolHandler();
   } else if (selected_item() == RPH_BLOCK) {
-    UnregisterProtocolHandler();
-  } else if (selected_item() == RPH_IGNORE) {
     IgnoreProtocolHandler();
   } else {
     NOTREACHED();
diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model.h b/chrome/browser/ui/content_settings/content_setting_bubble_model.h
index 690931b..8fa96b9e 100644
--- a/chrome/browser/ui/content_settings/content_setting_bubble_model.h
+++ b/chrome/browser/ui/content_settings/content_setting_bubble_model.h
@@ -373,14 +373,11 @@
 
  private:
   void RegisterProtocolHandler();
-  void UnregisterProtocolHandler();
   void IgnoreProtocolHandler();
-  void ClearOrSetPreviousHandler();
   void PerformActionForSelectedItem();
 
   raw_ptr<custom_handlers::ProtocolHandlerRegistry> registry_;
   custom_handlers::ProtocolHandler pending_handler_;
-  custom_handlers::ProtocolHandler previous_handler_;
 };
 
 // The model of the content settings bubble for media settings.
diff --git a/components/custom_handlers/protocol_handler_registry.cc b/components/custom_handlers/protocol_handler_registry.cc
index 0c4dbc5..1b943b3b 100644
--- a/components/custom_handlers/protocol_handler_registry.cc
+++ b/components/custom_handlers/protocol_handler_registry.cc
@@ -137,6 +137,18 @@
 void ProtocolHandlerRegistry::OnIgnoreRegisterProtocolHandler(
     const ProtocolHandler& handler) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
+  // The registered handlers are the ones allowed to handle the protocol, so a
+  // handler the user just refused must not stay among them: it would be
+  // offered in chrome://settings/handlers and be eligible for the promotion
+  // RemoveHandler() performs when the default handler goes away. The user may
+  // have accepted it first in the same bubble; removing it then hands the
+  // default back to the previous one.
+  //
+  // Remove before ignoring: RemoveHandler() short-circuits to
+  // RemoveIgnoredHandler() for handlers that are already ignored.
+  if (IsRegistered(handler)) {
+    RemoveHandler(handler, /*save=*/false);
+  }
   IgnoreProtocolHandler(handler, USER);
   Save();
   NotifyChanged();
diff --git a/components/custom_handlers/protocol_handler_registry.h b/components/custom_handlers/protocol_handler_registry.h
index 55272ebb..976f6da2 100644
--- a/components/custom_handlers/protocol_handler_registry.h
+++ b/components/custom_handlers/protocol_handler_registry.h
@@ -110,14 +110,27 @@
   // function returns false the user needs to be prompted for confirmation.
   bool SilentlyHandleRegisterHandlerRequest(const ProtocolHandler& handler);
 
-  // Called when the user accepts the registration of a given protocol handler.
+  // Called when the user accepts the registration of a given protocol handler,
+  // which becomes the default one for its protocol.
+  // TODO(crbug.com/553146562): Revoke an earlier refusal here
+  // (RemoveIgnoredHandler()) once the registration succeeded, instead of
+  // leaving that to the content setting bubble; "Set as default" in
+  // chrome://settings/handlers reaches this too.
   void OnAcceptRegisterProtocolHandler(const ProtocolHandler& handler);
 
-  // Called when the user denies the registration of a given protocol handler.
+  // Registers the given protocol handler without making it the default: the
+  // user allowed it as an option to pick later in settings. Despite its name
+  // this is a grant, not a refusal; the refusal is
+  // OnIgnoreRegisterProtocolHandler().
+  // TODO(crbug.com/553146562): Rename it after what it does, e.g.
+  // OnAcceptRegisterProtocolHandlerAsCandidate(), and stop content shell from
+  // calling it for requests made without a user gesture, which nobody granted;
+  // those should be refused.
   void OnDenyRegisterProtocolHandler(const ProtocolHandler& handler);
 
-  // Called when the user indicates that they don't want to be asked about the
-  // given protocol handler again.
+  // Called when the user refuses the registration of a given protocol handler
+  // and doesn't want to be asked about it again. A registration the handler
+  // already had, e.g. from an earlier choice in the same bubble, is removed.
   void OnIgnoreRegisterProtocolHandler(const ProtocolHandler& handler);
 
   // Removes all handlers that have the same origin and protocol as the given
diff --git a/chrome/browser/custom_handlers/protocol_handler_registry_browsertest.cc b/chrome/browser/custom_handlers/protocol_handler_registry_browsertest.cc
index 1b7019d..c8649b19 100644
--- a/chrome/browser/custom_handlers/protocol_handler_registry_browsertest.cc
+++ b/chrome/browser/custom_handlers/protocol_handler_registry_browsertest.cc
@@ -22,6 +22,8 @@
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
+#include "chrome/browser/ui/content_settings/content_setting_bubble_model.h"
+#include "chrome/browser/ui/content_settings/fake_owner.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
 #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
@@ -292,6 +294,87 @@
   ASSERT_TRUE(content_settings->pending_protocol_handler().IsValid());
 }
 
+// A registration made without a user gesture is parked on the page as a
+// pending handler. Removing the scheme's default handler while that request is
+// still pending must survive the user disposing of the bubble: neither denying
+// the request nor dismissing the bubble may bring the removed handler back.
+IN_PROC_BROWSER_TEST_F(ChromeRegisterProtocolHandlerBrowserTest,
+                       DenyingPendingRegistrationDoesNotRestoreRemovedHandler) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), embedded_test_server()->GetURL("/title1.html")));
+
+  // The accepted handler is registered from a different origin than the page
+  // below, so the page's registration is not silently replaced.
+  const std::string protocol = "web+search";
+  AddProtocolHandler(protocol, GURL("https://accepted.example/?q=%s"));
+
+  WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  auto* content_settings =
+      PageSpecificContentSettingsDelegate::FromWebContents(web_contents);
+
+  ASSERT_TRUE(content::ExecJs(web_contents,
+                              "navigator.registerProtocolHandler('web+"
+                              "search', 'test.html?%s', 'test');",
+                              content::EXECUTE_SCRIPT_NO_USER_GESTURE));
+  ASSERT_TRUE(content_settings->pending_protocol_handler().IsValid());
+
+  // The user removes the accepted handler while the request is pending.
+  ProtocolHandlerRegistry* registry = GetRegistry();
+  registry->RemoveDefaultHandler(protocol);
+  ASSERT_FALSE(registry->IsHandledProtocol(protocol));
+
+  // The user denies the pending registration in the bubble.
+  ContentSettingRPHBubbleModel bubble_model(
+      nullptr, web_contents->GetPrimaryPage(), registry);
+  std::unique_ptr<FakeOwner> owner = FakeOwner::Create(bubble_model, 0);
+  // "1" is the "Deny" radio button.
+  owner->SetSelectedRadioOptionAndCommit(1);
+
+  const ProtocolHandler& handler = registry->GetHandlerFor(protocol);
+  EXPECT_TRUE(handler.IsEmpty()) << "handler left behind: " << handler.url();
+  EXPECT_FALSE(registry->IsHandledProtocol(protocol));
+}
+
+IN_PROC_BROWSER_TEST_F(
+    ChromeRegisterProtocolHandlerBrowserTest,
+    DismissingPendingRegistrationDoesNotRestoreRemovedHandler) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), embedded_test_server()->GetURL("/title1.html")));
+
+  const std::string protocol = "web+search";
+  AddProtocolHandler(protocol, GURL("https://accepted.example/?q=%s"));
+
+  WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  auto* content_settings =
+      PageSpecificContentSettingsDelegate::FromWebContents(web_contents);
+
+  ASSERT_TRUE(content::ExecJs(web_contents,
+                              "navigator.registerProtocolHandler('web+"
+                              "search', 'test.html?%s', 'test');",
+                              content::EXECUTE_SCRIPT_NO_USER_GESTURE));
+  ASSERT_TRUE(content_settings->pending_protocol_handler().IsValid());
+
+  ProtocolHandlerRegistry* registry = GetRegistry();
+  registry->RemoveDefaultHandler(protocol);
+  ASSERT_FALSE(registry->IsHandledProtocol(protocol));
+
+  // Dismissing the bubble commits its default radio button, as
+  // ContentSettingBubbleContents::WindowClosing() does.
+  ContentSettingRPHBubbleModel bubble_model(
+      nullptr, web_contents->GetPrimaryPage(), registry);
+  std::unique_ptr<FakeOwner> owner = FakeOwner::Create(
+      bubble_model, bubble_model.bubble_content().radio_group.default_item);
+  bubble_model.CommitChanges();
+
+  const ProtocolHandler& handler = registry->GetHandlerFor(protocol);
+  EXPECT_TRUE(handler.IsEmpty()) << "handler left behind: " << handler.url();
+  EXPECT_FALSE(registry->IsHandledProtocol(protocol));
+}
+
 IN_PROC_BROWSER_TEST_F(ChromeRegisterProtocolHandlerBrowserTest,
                        RegisterProtocolHandlerFrameDetachOnFullscreenExit) {
   ASSERT_TRUE(embedded_test_server()->Start());
diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
index 35b3429..461cded2 100644
--- a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
+++ b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
@@ -986,8 +986,7 @@
       FakeOwner::Create(content_setting_bubble_model, 0);
 
   {
-    ProtocolHandler handler = registry.GetHandlerFor("mailto");
-    EXPECT_TRUE(handler.IsEmpty());
+    EXPECT_TRUE(registry.GetHandlerFor("mailto").IsEmpty());
     EXPECT_EQ(CONTENT_SETTING_DEFAULT,
               content_settings->pending_protocol_handler_setting());
   }
@@ -995,39 +994,29 @@
   // "0" is the "Allow" radio button.
   owner->SetSelectedRadioOptionAndCommit(0);
   {
-    ProtocolHandler handler = registry.GetHandlerFor("mailto");
-    ASSERT_FALSE(handler.IsEmpty());
+    EXPECT_TRUE(registry.IsDefault(test_handler));
     EXPECT_EQ(CONTENT_SETTING_ALLOW,
               content_settings->pending_protocol_handler_setting());
   }
 
-  // "1" is the "Deny" radio button.
+  // "1" is the "Deny" radio button. Refusing after allowing must leave no
+  // trace of the handler among the registered ones, only among the ignored.
   owner->SetSelectedRadioOptionAndCommit(1);
   {
-    ProtocolHandler handler = registry.GetHandlerFor("mailto");
-    EXPECT_TRUE(handler.IsEmpty());
+    EXPECT_TRUE(registry.GetHandlerFor("mailto").IsEmpty());
+    EXPECT_FALSE(registry.IsRegistered(test_handler));
+    EXPECT_TRUE(registry.IsIgnored(test_handler));
     EXPECT_EQ(CONTENT_SETTING_BLOCK,
               content_settings->pending_protocol_handler_setting());
   }
 
-  // "2" is the "Ignore button.
-  owner->SetSelectedRadioOptionAndCommit(2);
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/custom_handlers/protocol_handler_registry_browsertest.cc b/chrome/browser/custom_handlers/protocol_handler_registry_browsertest.cc
index 1b7019d..c8649b19 100644
--- a/chrome/browser/custom_handlers/protocol_handler_registry_browsertest.cc
+++ b/chrome/browser/custom_handlers/protocol_handler_registry_browsertest.cc
@@ -22,6 +22,8 @@
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
+#include "chrome/browser/ui/content_settings/content_setting_bubble_model.h"
+#include "chrome/browser/ui/content_settings/fake_owner.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
 #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
@@ -292,6 +294,87 @@
   ASSERT_TRUE(content_settings->pending_protocol_handler().IsValid());
 }
 
+// A registration made without a user gesture is parked on the page as a
+// pending handler. Removing the scheme's default handler while that request is
+// still pending must survive the user disposing of the bubble: neither denying
+// the request nor dismissing the bubble may bring the removed handler back.
+IN_PROC_BROWSER_TEST_F(ChromeRegisterProtocolHandlerBrowserTest,
+                       DenyingPendingRegistrationDoesNotRestoreRemovedHandler) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), embedded_test_server()->GetURL("/title1.html")));
+
+  // The accepted handler is registered from a different origin than the page
+  // below, so the page's registration is not silently replaced.
+  const std::string protocol = "web+search";
+  AddProtocolHandler(protocol, GURL("https://accepted.example/?q=%s"));
+
+  WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  auto* content_settings =
+      PageSpecificContentSettingsDelegate::FromWebContents(web_contents);
+
+  ASSERT_TRUE(content::ExecJs(web_contents,
+                              "navigator.registerProtocolHandler('web+"
+                              "search', 'test.html?%s', 'test');",
+                              content::EXECUTE_SCRIPT_NO_USER_GESTURE));
+  ASSERT_TRUE(content_settings->pending_protocol_handler().IsValid());
+
+  // The user removes the accepted handler while the request is pending.
+  ProtocolHandlerRegistry* registry = GetRegistry();
+  registry->RemoveDefaultHandler(protocol);
+  ASSERT_FALSE(registry->IsHandledProtocol(protocol));
+
+  // The user denies the pending registration in the bubble.
+  ContentSettingRPHBubbleModel bubble_model(
+      nullptr, web_contents->GetPrimaryPage(), registry);
+  std::unique_ptr<FakeOwner> owner = FakeOwner::Create(bubble_model, 0);
+  // "1" is the "Deny" radio button.
+  owner->SetSelectedRadioOptionAndCommit(1);
+
+  const ProtocolHandler& handler = registry->GetHandlerFor(protocol);
+  EXPECT_TRUE(handler.IsEmpty()) << "handler left behind: " << handler.url();
+  EXPECT_FALSE(registry->IsHandledProtocol(protocol));
+}
+
+IN_PROC_BROWSER_TEST_F(
+    ChromeRegisterProtocolHandlerBrowserTest,
+    DismissingPendingRegistrationDoesNotRestoreRemovedHandler) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), embedded_test_server()->GetURL("/title1.html")));
+
+  const std::string protocol = "web+search";
+  AddProtocolHandler(protocol, GURL("https://accepted.example/?q=%s"));
+
+  WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  auto* content_settings =
+      PageSpecificContentSettingsDelegate::FromWebContents(web_contents);
+
+  ASSERT_TRUE(content::ExecJs(web_contents,
+                              "navigator.registerProtocolHandler('web+"
+                              "search', 'test.html?%s', 'test');",
+                              content::EXECUTE_SCRIPT_NO_USER_GESTURE));
+  ASSERT_TRUE(content_settings->pending_protocol_handler().IsValid());
+
+  ProtocolHandlerRegistry* registry = GetRegistry();
+  registry->RemoveDefaultHandler(protocol);
+  ASSERT_FALSE(registry->IsHandledProtocol(protocol));
+
+  // Dismissing the bubble commits its default radio button, as
+  // ContentSettingBubbleContents::WindowClosing() does.
+  ContentSettingRPHBubbleModel bubble_model(
+      nullptr, web_contents->GetPrimaryPage(), registry);
+  std::unique_ptr<FakeOwner> owner = FakeOwner::Create(
+      bubble_model, bubble_model.bubble_content().radio_group.default_item);
+  bubble_model.CommitChanges();
+
+  const ProtocolHandler& handler = registry->GetHandlerFor(protocol);
+  EXPECT_TRUE(handler.IsEmpty()) << "handler left behind: " << handler.url();
+  EXPECT_FALSE(registry->IsHandledProtocol(protocol));
+}
+
 IN_PROC_BROWSER_TEST_F(ChromeRegisterProtocolHandlerBrowserTest,
                        RegisterProtocolHandlerFrameDetachOnFullscreenExit) {
   ASSERT_TRUE(embedded_test_server()->Start());
diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
index 35b3429..461cded2 100644
--- a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
+++ b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
@@ -986,8 +986,7 @@
       FakeOwner::Create(content_setting_bubble_model, 0);
 
   {
-    ProtocolHandler handler = registry.GetHandlerFor("mailto");
-    EXPECT_TRUE(handler.IsEmpty());
+    EXPECT_TRUE(registry.GetHandlerFor("mailto").IsEmpty());
     EXPECT_EQ(CONTENT_SETTING_DEFAULT,
               content_settings->pending_protocol_handler_setting());
   }
@@ -995,39 +994,29 @@
   // "0" is the "Allow" radio button.
   owner->SetSelectedRadioOptionAndCommit(0);
   {
-    ProtocolHandler handler = registry.GetHandlerFor("mailto");
-    ASSERT_FALSE(handler.IsEmpty());
+    EXPECT_TRUE(registry.IsDefault(test_handler));
     EXPECT_EQ(CONTENT_SETTING_ALLOW,
               content_settings->pending_protocol_handler_setting());
   }
 
-  // "1" is the "Deny" radio button.
+  // "1" is the "Deny" radio button. Refusing after allowing must leave no
+  // trace of the handler among the registered ones, only among the ignored.
   owner->SetSelectedRadioOptionAndCommit(1);
   {
-    ProtocolHandler handler = registry.GetHandlerFor("mailto");
-    EXPECT_TRUE(handler.IsEmpty());
+    EXPECT_TRUE(registry.GetHandlerFor("mailto").IsEmpty());
+    EXPECT_FALSE(registry.IsRegistered(test_handler));
+    EXPECT_TRUE(registry.IsIgnored(test_handler));
     EXPECT_EQ(CONTENT_SETTING_BLOCK,
               content_settings->pending_protocol_handler_setting());
   }
 
-  // "2" is the "Ignore button.
-  owner->SetSelectedRadioOptionAndCommit(2);
-  {
-    ProtocolHandler handler = registry.GetHandlerFor("mailto");
-    EXPECT_TRUE(handler.IsEmpty());
-    EXPECT_EQ(CONTENT_SETTING_DEFAULT,
-              content_settings->pending_protocol_handler_setting());
-    EXPECT_TRUE(registry.IsIgnored(test_handler));
-  }
-
   // "0" is the "Allow" radio button.
   owner->SetSelectedRadioOptionAndCommit(0);
   {
-    ProtocolHandler handler = registry.GetHandlerFor("mailto");
-    ASSERT_FALSE(handler.IsEmpty());
+    EXPECT_TRUE(registry.IsDefault(test_handler));
+    EXPECT_FALSE(registry.IsIgnored(test_handler));
     EXPECT_EQ(CONTENT_SETTING_ALLOW,
               content_settings->pending_protocol_handler_setting());
-    EXPECT_FALSE(registry.IsIgnored(test_handler));
   }
 
   registry.Shutdown();
@@ -1053,14 +1042,58 @@
       content_setting_bubble_model,
       content_setting_bubble_model.bubble_content().radio_group.default_item);
 
-  // If nothing is selected, the default action "Ignore" should be performed.
+  // If nothing is selected, the default action "Deny" should be performed:
+  // dismissing the bubble grants nothing.
   content_setting_bubble_model.CommitChanges();
   {
-    ProtocolHandler handler = registry.GetHandlerFor("mailto");
-    EXPECT_TRUE(handler.IsEmpty());
-    EXPECT_EQ(CONTENT_SETTING_DEFAULT,
-              content_settings->pending_protocol_handler_setting());
+    EXPECT_TRUE(registry.GetHandlerFor("mailto").IsEmpty());
+    EXPECT_FALSE(registry.IsRegistered(test_handler));
     EXPECT_TRUE(registry.IsIgnored(test_handler));
+    EXPECT_EQ(CONTENT_SETTING_BLOCK,
+              content_settings->pending_protocol_handler_setting());
+  }
+
+  registry.Shutdown();
+}
+
+// The handler named as being replaced is the registry's current default, not
+// whatever was the default when the registration request arrived.
+TEST_F(ContentSettingBubbleModelTest, RPHTitleNamesCurrentDefaultHandler) {
+  custom_handlers::ProtocolHandlerRegistry registry(
+      profile()->GetPrefs(),
+      std::make_unique<custom_handlers::TestProtocolHandlerRegistryDelegate>());
+  registry.InitProtocolSettings();
+  registry.OnAcceptRegisterProtocolHandler(
+      ProtocolHandler::CreateProtocolHandler(
+          "mailto", GURL("https://installed.example/?u=%s")));
+
+  const GURL page_url("https://toplevel.example/");
+  NavigateAndCommit(page_url);
+  PageSpecificContentSettingsDelegate::FromWebContents(web_contents())
+      ->set_pending_protocol_handler(ProtocolHandler::CreateProtocolHandler(
+          "mailto", GURL("https://www.toplevel.example/?u=%s")));
+
+  {
+    ContentSettingRPHBubbleModel content_setting_bubble_model(nullptr, page(),
+                                                              &registry);
+    EXPECT_EQ(content_setting_bubble_model.bubble_content().title,
+              l10n_util::GetStringFUTF16(
+                  IDS_REGISTER_PROTOCOL_HANDLER_CONFIRM_REPLACE,
+                  u"www.toplevel.example",
+                  ProtocolHandler::GetProtocolDisplayName("mailto"),
+                  u"installed.example"));
+  }
+
+  // Once the installed handler is gone there is nothing left to replace.
+  registry.RemoveDefaultHandler("mailto");
+  {
+    ContentSettingRPHBubbleModel content_setting_bubble_model(nullptr, page(),
+                                                              &registry);
+    EXPECT_EQ(
+        content_setting_bubble_model.bubble_content().title,
+        l10n_util::GetStringFUTF16(
+            IDS_REGISTER_PROTOCOL_HANDLER_CONFIRM, u"www.toplevel.example",
+            ProtocolHandler::GetProtocolDisplayName("mailto")));
   }
 
   registry.Shutdown();
diff --git a/chrome/browser/ui/views/location_bar/content_setting_bubble_dialog_browsertest.cc b/chrome/browser/ui/views/location_bar/content_setting_bubble_dialog_browsertest.cc
index f0e28539..d31d1f1 100644
--- a/chrome/browser/ui/views/location_bar/content_setting_bubble_dialog_browsertest.cc
+++ b/chrome/browser/ui/views/location_bar/content_setting_bubble_dialog_browsertest.cc
@@ -372,6 +372,7 @@
 
 IN_PROC_BROWSER_TEST_P(ContentSettingBubbleDialogTest,
                        InvokeUi_protocol_handlers) {
+  set_baseline("553146562");
   ShowAndVerifyUi();
 }
diff --git a/components/custom_handlers/protocol_handler_registry_unittest.cc b/components/custom_handlers/protocol_handler_registry_unittest.cc
index 552783c..89aee217 100644
--- a/components/custom_handlers/protocol_handler_registry_unittest.cc
+++ b/components/custom_handlers/protocol_handler_registry_unittest.cc
@@ -277,6 +277,42 @@
   ASSERT_TRUE(registry()->IsHandledProtocol("news"));
 }
 
+// A refused handler must not be offered for the scheme, nor be promoted when
+// the accepted handler is removed.
+TEST_F(ProtocolHandlerRegistryTest,
+       IgnoredHandlerIsNotAllowedToHandleProtocol) {
+  ProtocolHandler accepted = CreateProtocolHandler("news", "accepted");
+  ProtocolHandler refused = CreateProtocolHandler("news", "refused");
+  registry()->OnAcceptRegisterProtocolHandler(accepted);
+  registry()->OnIgnoreRegisterProtocolHandler(refused);
+
+  EXPECT_FALSE(registry()->IsRegistered(refused));
+  EXPECT_TRUE(registry()->IsIgnored(refused));
+
+  registry()->RemoveDefaultHandler("news");
+
+  EXPECT_FALSE(registry()->IsDefault(refused));
+  EXPECT_TRUE(registry()->GetHandlerFor("news").IsEmpty());
+  EXPECT_FALSE(registry()->IsHandledProtocol("news"));
+}
+
+// Refusing a handler that is currently the default, e.g. after "Allow" then
+// "Deny" in the same bubble, removes it and hands the default back to the
+// handler that had it before.
+TEST_F(ProtocolHandlerRegistryTest, IgnoringTheDefaultFallsBackToOldDefault) {
+  ProtocolHandler ph1 = CreateProtocolHandler("news", "test1");
+  ProtocolHandler ph2 = CreateProtocolHandler("news", "test2");
+  registry()->OnAcceptRegisterProtocolHandler(ph1);
+  registry()->OnAcceptRegisterProtocolHandler(ph2);
+  ASSERT_TRUE(registry()->IsDefault(ph2));
+
+  registry()->OnIgnoreRegisterProtocolHandler(ph2);
+
+  EXPECT_TRUE(registry()->IsDefault(ph1));
+  EXPECT_FALSE(registry()->IsRegistered(ph2));
+  EXPECT_TRUE(registry()->IsIgnored(ph2));
+}
+
 TEST_F(ProtocolHandlerRegistryTest, ClearDefaultMakesProtocolNotHandled) {
   registry()->OnAcceptRegisterProtocolHandler(test_protocol_handler());
   registry()->ClearDefault("news");
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.