Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Payments
DescriptionMissing authorization in Payments
ComponentPayments
Bug ClassLogic Error
Tracker513608317
Fix commit5ae225cd4481 (chromium/src) +117/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
GURL
components/facilitated_payments/content/browser/content_facilitated_payments_driver.h
modified
RenderFrameHost
components/facilitated_payments/content/browser/content_facilitated_payments_driver.h
modified
MockPixManager
components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc
modified
MockPaymentLinkManager
components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc
modified

Files Changed

  • components/facilitated_payments/content/browser/content_facilitated_payments_driver.cc
  • components/facilitated_payments/content/browser/content_facilitated_payments_driver.h
  • components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc
  • components/facilitated_payments/core/browser/facilitated_payments_driver.cc
  • components/facilitated_payments/core/browser/facilitated_payments_driver.h
From 5ae225cd448101685abdb096bebb24822ef3f1f2 Mon Sep 17 00:00:00 2001
From: Luis Antunes <luisantunes@google.com>
Date: Tue, 07 Jul 2026 06:54:09 -0700
Subject: [PATCH] [Payments] Block Pix copy flow on insecure contexts

This CL refactors FacilitatedPaymentsDriver to check page security
before forwarding copy events to PixManager. It introduces the pure
virtual IsSecureForPaymentHandling helper, which is overridden by
ContentFacilitatedPaymentsDriver to verify the frame using
SecurityChecker::IsSecureForPaymentLinkHandling. This prevents
insecure contexts (e.g., HTTP pages) from triggering the Pix flow.

Fixed: 513608317
Change-Id: Ib9bf73eb97713a9ce015e3b0312d1b58bb3f932a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8012370
Reviewed-by: Stephen McGruer <smcgruer@chromium.org>
Reviewed-by: Slobodan Pejic <slobodan@chromium.org>
Commit-Queue: Luis Antunes <luisantunes@google.com>
Cr-Commit-Position: refs/heads/main@{#1657914}
---

diff --git a/components/facilitated_payments/content/browser/content_facilitated_payments_driver.cc b/components/facilitated_payments/content/browser/content_facilitated_payments_driver.cc
index 3e96283..4968015e 100644
--- a/components/facilitated_payments/content/browser/content_facilitated_payments_driver.cc
+++ b/components/facilitated_payments/content/browser/content_facilitated_payments_driver.cc
@@ -6,14 +6,10 @@
 
 #include <memory>
 
-#include "base/functional/callback.h"
 #include "components/facilitated_payments/content/browser/facilitated_payments_api_client_factory.h"
 #include "components/facilitated_payments/content/browser/security_checker.h"
-#include "components/facilitated_payments/core/browser/facilitated_payments_api_client.h"
-#include "components/facilitated_payments/core/browser/payment_link_manager.h"
-#include "components/facilitated_payments/core/browser/pix_manager.h"
 #include "content/public/browser/render_frame_host.h"
-#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
+#include "url/gurl.h"
 
 namespace payments::facilitated {
 
@@ -48,6 +44,12 @@
       /*ukm_source_id=*/render_frame_host->GetPageUkmSourceId());
 }
 
+bool ContentFacilitatedPaymentsDriver::IsSecureForPaymentHandling() const {
+  content::RenderFrameHost* render_frame_host =
+      content::RenderFrameHost::FromID(render_frame_host_id_);
+  return security_checker_->IsSecureForPaymentLinkHandling(*render_frame_host);
+}
+
 void ContentFacilitatedPaymentsDriver::SetPaymentLinkHandlerReceiver(
     mojo::PendingReceiver<mojom::PaymentLinkHandler> pending_receiver) {
   if (receiver_.is_bound()) {
diff --git a/components/facilitated_payments/content/browser/content_facilitated_payments_driver.h b/components/facilitated_payments/content/browser/content_facilitated_payments_driver.h
index 92242459..362b910 100644
--- a/components/facilitated_payments/content/browser/content_facilitated_payments_driver.h
+++ b/components/facilitated_payments/content/browser/content_facilitated_payments_driver.h
@@ -10,6 +10,8 @@
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "third_party/blink/public/mojom/facilitated_payments/payment_link_handler.mojom.h"
 
+class GURL;
+
 namespace content {
 class RenderFrameHost;
 }  // namespace content
@@ -43,6 +45,9 @@
       mojo::PendingReceiver<mojom::PaymentLinkHandler> pending_receiver);
 
  private:
+  // FacilitatedPaymentsDriver:
+  bool IsSecureForPaymentHandling() const override;
+
   // The ID of the frame to which this driver is associated.
   const content::GlobalRenderFrameHostId render_frame_host_id_;
 
diff --git a/components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc b/components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc
index c1fd153..9c83b44 100644
--- a/components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc
+++ b/components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc
@@ -22,6 +22,32 @@
 namespace payments::facilitated {
 namespace {
 
+constexpr char16_t kFakePixCode[] =
+    u"00020126370014br.gov.bcb.pix2515www.example.com6304EA3F";
+
+class MockPixManager : public PixManager {
+ public:
+  MockPixManager(
+      FacilitatedPaymentsClient* client,
+      FacilitatedPaymentsApiClientCreator api_client_creator,
+      optimization_guide::OptimizationGuideDecider* optimization_guide_decider)
+      : PixManager(client,
+                   std::move(api_client_creator),
+                   optimization_guide_decider) {}
+  ~MockPixManager() override = default;
+
+  MOCK_METHOD(void,
+              OnPixCodeCopiedToClipboard,
+              (const GURL&,
+               const std::optional<GURL>&,
+               const url::Origin&,
+               bool,
+               std::optional<PixCodeRustValidationResult>,
+               std::string,
+               ukm::SourceId),
+              (override));
+};
+
 class MockPaymentLinkManager : public PaymentLinkManager {
  public:
   MockPaymentLinkManager(
@@ -75,6 +101,15 @@
             decider_.get());
     payment_link_manager_ = em.get();
     driver_->SetPaymentLinkManagerForTesting(std::move(em));
+
+    std::unique_ptr<MockPixManager> pm =
+        std::make_unique<testing::NiceMock<MockPixManager>>(
+            client_.get(),
+            GetFacilitatedPaymentsApiClientCreator(
+                render_frame_host->GetGlobalId()),
+            decider_.get());
+    pix_manager_ = pm.get();
+    driver_->SetPixManagerForTesting(std::move(pm));
   }
 
   void TearDown() override {
@@ -82,6 +117,7 @@
     driver_.reset();
     security_checker_ = nullptr;
     payment_link_manager_ = nullptr;
+    pix_manager_ = nullptr;
     content::RenderViewHostTestHarness::TearDown();
   }
 
@@ -90,6 +126,7 @@
   std::unique_ptr<FacilitatedPaymentsClient> client_;
   std::unique_ptr<ContentFacilitatedPaymentsDriver> driver_;
   raw_ptr<MockPaymentLinkManager> payment_link_manager_;
+  raw_ptr<MockPixManager> pix_manager_;
   raw_ptr<MockSecurityChecker> security_checker_;
 };
 
@@ -131,5 +168,31 @@
   driver_->HandlePaymentLink(kFakePaymentLinkUrl);
 }
 
+TEST_F(ContentFacilitatedPaymentsDriverTest,
+       PixCodeCopied_ForwardedToPixManager) {
+  EXPECT_CALL(*security_checker_,
+              IsSecureForPaymentLinkHandling(testing::Ref(*main_rfh())))
+      .WillOnce(testing::Return(true));
+  EXPECT_CALL(*pix_manager_, OnPixCodeCopiedToClipboard).Times(1);
+
+  driver_->OnTextCopiedToClipboard(
+      GURL("https://example.com"), std::nullopt,
+      url::Origin::Create(GURL("https://example.com")), kFakePixCode,
+      ukm::kInvalidSourceId, /*is_same_origin=*/false);
+}
+
+TEST_F(ContentFacilitatedPaymentsDriverTest,
+       SecurityCheckFailed_PixCodeBlocked) {
+  EXPECT_CALL(*security_checker_,
+              IsSecureForPaymentLinkHandling(testing::Ref(*main_rfh())))
+      .WillOnce(testing::Return(false));
+  EXPECT_CALL(*pix_manager_, OnPixCodeCopiedToClipboard).Times(0);
+
+  driver_->OnTextCopiedToClipboard(
+      GURL("http://example.com"), std::nullopt,
+      url::Origin::Create(GURL("http://example.com")), kFakePixCode,
+      ukm::kInvalidSourceId, /*is_same_origin=*/false);
+}
+
 }  // namespace
 }  // namespace payments::facilitated
diff --git a/components/facilitated_payments/core/browser/facilitated_payments_driver.cc b/components/facilitated_payments/core/browser/facilitated_payments_driver.cc
index 4a0ef7d0..88d8d43 100644
--- a/components/facilitated_payments/core/browser/facilitated_payments_driver.cc
+++ b/components/facilitated_payments/core/browser/facilitated_payments_driver.cc
@@ -80,6 +80,9 @@
     const std::u16string& copied_text,
     ukm::SourceId ukm_source_id,
     bool is_same_origin) {
+  if (!IsSecureForPaymentHandling()) {
+    return;
+  }
   std::string copied_text_utf8 = base::UTF16ToUTF8(copied_text);
   // Even if the feature is not enabled, always run the Rust validator to log
   // metrics about whether or not the results agree.
diff --git a/components/facilitated_payments/core/browser/facilitated_payments_driver.h b/components/facilitated_payments/core/browser/facilitated_payments_driver.h
index 2782e42..eebbc97c 100644
--- a/components/facilitated_payments/core/browser/facilitated_payments_driver.h
+++ b/components/facilitated_payments/core/browser/facilitated_payments_driver.h
@@ -69,6 +69,10 @@
   virtual void SetPaymentLinkManagerForTesting(
       std::unique_ptr<PaymentLinkManager> payment_link_manager);
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc b/components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc
index c1fd153..9c83b44 100644
--- a/components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc
+++ b/components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc
@@ -22,6 +22,32 @@
 namespace payments::facilitated {
 namespace {
 
+constexpr char16_t kFakePixCode[] =
+    u"00020126370014br.gov.bcb.pix2515www.example.com6304EA3F";
+
+class MockPixManager : public PixManager {
+ public:
+  MockPixManager(
+      FacilitatedPaymentsClient* client,
+      FacilitatedPaymentsApiClientCreator api_client_creator,
+      optimization_guide::OptimizationGuideDecider* optimization_guide_decider)
+      : PixManager(client,
+                   std::move(api_client_creator),
+                   optimization_guide_decider) {}
+  ~MockPixManager() override = default;
+
+  MOCK_METHOD(void,
+              OnPixCodeCopiedToClipboard,
+              (const GURL&,
+               const std::optional<GURL>&,
+               const url::Origin&,
+               bool,
+               std::optional<PixCodeRustValidationResult>,
+               std::string,
+               ukm::SourceId),
+              (override));
+};
+
 class MockPaymentLinkManager : public PaymentLinkManager {
  public:
   MockPaymentLinkManager(
@@ -75,6 +101,15 @@
             decider_.get());
     payment_link_manager_ = em.get();
     driver_->SetPaymentLinkManagerForTesting(std::move(em));
+
+    std::unique_ptr<MockPixManager> pm =
+        std::make_unique<testing::NiceMock<MockPixManager>>(
+            client_.get(),
+            GetFacilitatedPaymentsApiClientCreator(
+                render_frame_host->GetGlobalId()),
+            decider_.get());
+    pix_manager_ = pm.get();
+    driver_->SetPixManagerForTesting(std::move(pm));
   }
 
   void TearDown() override {
@@ -82,6 +117,7 @@
     driver_.reset();
     security_checker_ = nullptr;
     payment_link_manager_ = nullptr;
+    pix_manager_ = nullptr;
     content::RenderViewHostTestHarness::TearDown();
   }
 
@@ -90,6 +126,7 @@
   std::unique_ptr<FacilitatedPaymentsClient> client_;
   std::unique_ptr<ContentFacilitatedPaymentsDriver> driver_;
   raw_ptr<MockPaymentLinkManager> payment_link_manager_;
+  raw_ptr<MockPixManager> pix_manager_;
   raw_ptr<MockSecurityChecker> security_checker_;
 };
 
@@ -131,5 +168,31 @@
   driver_->HandlePaymentLink(kFakePaymentLinkUrl);
 }
 
+TEST_F(ContentFacilitatedPaymentsDriverTest,
+       PixCodeCopied_ForwardedToPixManager) {
+  EXPECT_CALL(*security_checker_,
+              IsSecureForPaymentLinkHandling(testing::Ref(*main_rfh())))
+      .WillOnce(testing::Return(true));
+  EXPECT_CALL(*pix_manager_, OnPixCodeCopiedToClipboard).Times(1);
+
+  driver_->OnTextCopiedToClipboard(
+      GURL("https://example.com"), std::nullopt,
+      url::Origin::Create(GURL("https://example.com")), kFakePixCode,
+      ukm::kInvalidSourceId, /*is_same_origin=*/false);
+}
+
+TEST_F(ContentFacilitatedPaymentsDriverTest,
+       SecurityCheckFailed_PixCodeBlocked) {
+  EXPECT_CALL(*security_checker_,
+              IsSecureForPaymentLinkHandling(testing::Ref(*main_rfh())))
+      .WillOnce(testing::Return(false));
+  EXPECT_CALL(*pix_manager_, OnPixCodeCopiedToClipboard).Times(0);
+
+  driver_->OnTextCopiedToClipboard(
+      GURL("http://example.com"), std::nullopt,
+      url::Origin::Create(GURL("http://example.com")), kFakePixCode,
+      ukm::kInvalidSourceId, /*is_same_origin=*/false);
+}
+
 }  // namespace
 }  // namespace payments::facilitated
diff --git a/components/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc b/components/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc
index e5ff4c0..4d8dad36 100644
--- a/components/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc
+++ b/components/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc
@@ -61,13 +61,25 @@
               (override));
 };
 
+class MockFacilitatedPaymentsDriver : public FacilitatedPaymentsDriver {
+ public:
+  using FacilitatedPaymentsDriver::FacilitatedPaymentsDriver;
+  ~MockFacilitatedPaymentsDriver() override = default;
+
+  MOCK_METHOD(bool, IsSecureForPaymentHandling, (), (const, override));
+};
+
 class FacilitatedPaymentsDriverTestBase : public testing::Test {
  public:
   FacilitatedPaymentsDriverTestBase() {
     FacilitatedPaymentsApiClientCreator api_client_creator =
         base::BindRepeating(&MockFacilitatedPaymentsApiClient::CreateApiClient);
-    driver_ = std::make_unique<FacilitatedPaymentsDriver>(&client_,
-                                                          api_client_creator);
+    driver_ =
+        std::make_unique<testing::NiceMock<MockFacilitatedPaymentsDriver>>(
+            &client_, api_client_creator);
+    ON_CALL(*driver_, IsSecureForPaymentHandling)
+        .WillByDefault(testing::Return(true));
+
     std::unique_ptr<MockPixManager> pix_manager =
         std::make_unique<testing::NiceMock<MockPixManager>>(
             &client_, api_client_creator, &decider_);
@@ -87,7 +99,7 @@
   base::test::ScopedFeatureList scoped_feature_list_;
   optimization_guide::TestOptimizationGuideDecider decider_;
   MockFacilitatedPaymentsClient client_;
-  std::unique_ptr<FacilitatedPaymentsDriver> driver_;
+  std::unique_ptr<MockFacilitatedPaymentsDriver> driver_;
   raw_ptr<MockPixManager> pix_manager_;
 };
 
@@ -147,6 +159,26 @@
       /*is_same_origin=*/false);
 }
 
+TEST_P(FacilitatedPaymentsDriverTest,
+       InsecureContext_OnPixCodeCopiedToClipboardNotTriggered) {
+  GURL url("http://example.com/");
+  url::Origin origin = url::Origin::Create(url);
+
+  EXPECT_CALL(*driver_, IsSecureForPaymentHandling)
+      .WillOnce(testing::Return(false));
+
+  EXPECT_CALL(*pix_manager_, OnPixCodeCopiedToClipboard).Times(0);
+
+  // "0014br.gov.bcb.pix" is the Pix identifier.
+  driver_->OnTextCopiedToClipboard(
+      /*main_frame_url=*/url,
+      /*iframe_url=*/std::nullopt,
+      /*main_frame_origin=*/origin, /*copied_text=*/
+      u"00020126370014br.gov.bcb.pix2515www.example.com6304EA3F",
+      /*ukm_source_id=*/123,
+      /*is_same_origin=*/false);
+}
+
 TEST_P(FacilitatedPaymentsDriverIframeUrlTypeTest, UrlTypeLogged) {
   const IframeUrlTypeTestCase& test_case = std::get<1>(GetParam());
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Security Context Bypass in Pix Clipboard-Based Payment Flow

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: The Pix payment flow triggered by clipboard events in Chrome for Android fails to verify secure context requirements (HTTPS and valid SSL). This allows an attacker to trigger trusted payment UI on insecure pages matching an allowlisted hostname. This bypasses the security checks enforced in other facilitated payment paths.

Affected files:

  • components/facilitated_payments/content/browser/content_facilitated_payments_driver_factory.cc
  • components/facilitated_payments/core/browser/pix_manager.cc
  • components/facilitated_payments/core/browser/facilitated_payments_driver.cc
  • components/optimization_guide/core/filters/optimization_filter.cc

Estimated timestamp from git blame: 2024-07-12

Description

A security parity gap exists in the Facilitated Payments (Pix) implementation on Chrome for Android. While the ‘Payment Link’ flow correctly enforces security requirements (HTTPS, valid SSL, and Permissions Policy) via the SecurityChecker, the flow triggered by copying text to the clipboard (OnTextCopiedToClipboard) potentially skips these essential checks. This allows a sensitive, browser-native payment UI to be displayed on insecure or MITM’d pages that match an allowlisted merchant hostname.

Technical Details

1. Missing Security Validation

The entry point for the clipboard-based Pix flow is ContentFacilitatedPaymentsDriverFactory::OnTextCopiedToClipboard in components/facilitated_payments/content/browser/content_facilitated_payments_driver_factory.cc. Unlike the payment link flow in ContentFacilitatedPaymentsDriver::HandlePaymentLink, this path does not invoke SecurityChecker::IsSecureForPaymentLinkHandling (or any equivalent security validation).

As a result, the following security properties are not verified before triggering the payment UI:

  • HTTPS Requirement: SchemeIsCryptographic() is not checked.
  • SSL Validity: IsSslValid() is not checked, meaning the UI can appear on pages with certificate errors (e.g., after a user bypasses an interstitial).
  • Permissions Policy: The payment feature policy is not checked, potentially allowing unauthorized frames to trigger the UI.

2. Insufficient Host-Only Allowlisting

The primary security gate for this flow is PixManager::IsMerchantAllowlisted, which queries the Optimization Guide for the PIX_MERCHANT_ORIGINS_ALLOWLIST. The underlying matching logic in OptimizationFilter::Matches (in components/optimization_guide/core/filters/optimization_filter.cc) performs a host-suffix match using a Bloom filter. This check only considers the hostname and ignores the URL scheme and the certificate state. Therefore, http://allowlisted-merchant.com is treated as valid despite the insecure connection.

Potential Attack Scenario

An on-path network attacker (e.g., via a malicious Wi-Fi hotspot) could potentially exploit this as follows:

  1. Interception: The attacker intercepts traffic to a host on the PIX_MERCHANT_ORIGINS_ALLOWLIST (e.g., a major Brazilian retailer).
  2. Insecure Serving: The attacker serves a malicious page over plain HTTP or via a connection with an invalid SSL certificate.
  3. Clipboard Trigger: The page induces the user to perform a copy action (e.g., via a ‘Copy Pix Code’ button). To avoid ‘Secure Context’ requirements of the modern Clipboard API, the attacker can use the legacy document.execCommand('copy') command, which functions in insecure contexts.
  4. UI Trigger: Chrome’s trusted browser-process Bottom Sheet appears, showing the user’s real bank accounts. The user, seeing a legitimate browser surface on a familiar merchant domain, is likely to trust the transaction.
  5. Unauthorized Transaction: If the user confirms, the attacker-controlled Pix code is used to initiate a payment via Google’s backend.

Impact

This issue allows an attacker to bypass secure-context requirements for a sensitive financial feature, enabling the display of a trusted browser surface over an untrusted page and the potential initiation of unauthorized payments via an attacker-controlled Pix code.

Suggested Fix

Update the clipboard-triggered flow (likely in ContentFacilitatedPaymentsDriver::OnTextCopiedToClipboard) to invoke SecurityChecker::IsSecureForPaymentLinkHandling (or a similar validation method) before proceeding with Pix detection or UI presentation. This ensures that the Pix payment UI is only displayed in contexts that meet the same security standards as the Payment Link flow.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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
Links in the report