Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in DevTools
DescriptionInappropriate implementation in DevTools
ComponentDevTools
Bug ClassLogic Error
Tracker496426191
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 <[email protected]>
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 <[email protected]>
Auto-Submit: Danil Somsikov <[email protected]>
Reviewed-by: Andrey Kosyakov <[email protected]>
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 [email protected]

Bypass of Secure Payment Confirmation dialog via Page.setSPCTransactionMode

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

Overview: The experimental DevTools command Page.setSPCTransactionMode lacks trust restrictions, allowing extensions with the debugger permission to auto-accept Secure Payment Confirmation (SPC) dialogs. This bypasses the primary UI where users review transaction details (payee, amount), leaving only a generic OS-level biometric prompt that lacks context.

Affected files:

  • chrome/browser/devtools/protocol/page_handler.cc
  • chrome/browser/devtools/chrome_devtools_session.cc
  • third_party/blink/public/devtools_protocol/domains/Page.pdl
  • components/payments/content/secure_payment_confirmation_controller.cc
  • components/payments/content/secure_payment_confirmation_app.cc

Estimated timestamp from git blame: 2025-12-05

Disclaimer

Please note: As an AI agent (Fortify), I do not currently have the ability to run code or build a working Proof of Concept. The following analysis and steps are potential, based on a rigorous static analysis of the Chromium codebase.

Description

The Secure Payment Confirmation (SPC) API relies on a browser-level dialog to securely display the transaction amount and payee to the user before they authorize the payment. However, the DevTools command Page.setSPCTransactionMode (intended for automated testing) is improperly exposed to untrusted DevTools clients, such as Chrome extensions with the debugger permission.

By setting the transaction mode to autoAccept, an attacker can completely bypass the browser’s SPC dialog. The user will only see the subsequent OS-level biometric prompt (e.g., Windows Hello or macOS Touch ID). Because OS-level prompts are generic and do not display transaction details, the user can be tricked into authorizing a tampered payment (e.g., with a highly inflated amount or different payee) that they never had the opportunity to review.

Technical Details

  1. Lack of Command Restriction: In third_party/blink/public/devtools_protocol/domains/Page.pdl, the setSPCTransactionMode command is marked experimental but lacks the restricted attribute.
  2. Untrusted Client Access: In chrome/browser/devtools/chrome_devtools_session.cc, PageHandler is explicitly made available to untrusted clients. Therefore, extensions using the debugger API can invoke this command without an IsTrusted() block.
  3. UI Bypass: When invoked with autoAccept, PageHandler::SetSPCTransactionMode sets the mode to kAutoAccept in the PaymentRequestWebContentsManager. Later, when SecurePaymentConfirmationController::SetupModelAndShowDialogIfApplicable processes a PaymentRequest, it checks this mode. Seeing kAutoAccept, it immediately calls OnConfirm() synchronously, skipping the UI review phase entirely and directly triggering the WebAuthn authenticator.

Potential Attacker Steps

  1. An attacker convinces a user to install a malicious Chrome extension requiring the debugger permission.
  2. The extension attaches the debugger to a tab on a merchant site where the user has an enrolled SPC credential.
  3. The extension sends the command to auto-accept SPC prompts: chrome.debugger.sendCommand({tabId: targetTabId}, 'Page.setSPCTransactionMode', {mode: 'autoAccept'})
  4. The extension injects JavaScript (via Runtime.evaluate or a content script) to initiate a PaymentRequest, maliciously modifying the amount or payee_name.
  5. The browser instantly bypasses the SPC dialog. The user only sees a generic OS-level biometric prompt (e.g., “Verify your identity”).
  6. Unaware of the tampered details, the user completes the biometric challenge, returning a valid cryptographic assertion for the fraudulent transaction.

Suggested Fix

The command should be restricted to trusted DevTools clients. Modify third_party/blink/public/devtools_protocol/domains/Page.pdl to add the restricted attribute to setSPCTransactionMode:

  experimental restricted command setSPCTransactionMode

Alternatively, enforce a strict IsTrusted() or equivalent security check within the implementation of PageHandler::SetSPCTransactionMode.

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