Medium watch CVSS 4.3 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Payments
DescriptionMissing authorization in Payments
ComponentPayments
Sub-componentPayments
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513608317
Fix commit5ae225cd4481 (chromium/src) +117/-8
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-15 (per vendor advisory)
Fix landed2026-07-07 (public git)
Disclosed2026-08-25
Patch gap49 days — fix public in source before the advisory

Background

Facilitated Payments / Pix
A browser-side feature in components/facilitated_payments that spots a copied Brazilian Pix payment code (the test’s br.gov.bcb.pix identifier) and hands it to PixManager, which can offer a browser-driven payment flow.
`FacilitatedPaymentsDriver`
The core, content-agnostic per-frame driver that receives renderer-reported events such as copy-to-clipboard and forwards them to PixManager or PaymentLinkManager. ContentFacilitatedPaymentsDriver is its content/ subclass bound to a RenderFrameHost.
`SecurityChecker::IsSecureForPaymentLinkHandling`
An existing browser-side check of a RenderFrameHost’s security state. The payment-link path already used it before this patch, and it rejects insecure contexts such as plain HTTP pages.

Root Cause Analysis

Before the patch, FacilitatedPaymentsDriver::OnTextCopiedToClipboard (the hunk shows only the tail of its signature, copied_text, ukm_source_id, is_same_origin; the tests confirm the name) checked nothing about the page’s security. It converted the copied text, ran the Rust Pix validator and forwarded the event towards PixManager::OnPixCodeCopiedToClipboard. The sibling payment-link flow was already gated on SecurityChecker::IsSecureForPaymentLinkHandling, but the Pix copy flow had no equivalent authorization, so an insecure (e.g. http://) page could trigger the Pix payment flow.

The fix adds a pure virtual IsSecureForPaymentHandling() to the core driver and checks it as the first statement of the copy handler, returning early before any Pix processing. ContentFacilitatedPaymentsDriver implements it by resolving its render_frame_host_id_ and calling security_checker_->IsSecureForPaymentLinkHandling. The new unit tests confirm that an HTTP origin with a valid Pix code no longer reaches OnPixCodeCopiedToClipboard, while HTTPS still does.

Key insight
The Pix copy-to-clipboard entry point was missing the page-security authorization check that the payment-link entry point already had. The fix adds the check inside the shared core driver and runs it before anything is forwarded to PixManager.

Attack Path

  1. Insecure page with a Pix code An attacker controls or can modify an insecure-context page, e.g. a plain HTTP site or traffic injected by a network attacker (inferred), and puts an attacker-chosen Pix code on it.
  2. User copies the code The user copies the text, and the renderer reports the event to the browser, ending up in FacilitatedPaymentsDriver::OnTextCopiedToClipboard. This needs a user copy action, and the feature only applies where Facilitated Payments/Pix is enabled (inferred: Android, Brazil-targeted).
  3. Missing gate Pre-patch, nothing checked whether the frame was a secure context, so the copied code was validated and forwarded to PixManager::OnPixCodeCopiedToClipboard.
  4. Trusted payment UI on untrusted content Chrome could then start its Pix payment flow for a code from a page whose integrity is not guaranteed (inferred: this could mislead the user into paying an attacker-chosen payee through browser UI).

Impact Assessment

An insecure-context page, or a network attacker who can tamper with one, can make the browser process start the Pix facilitated-payment flow, which the feature policy reserves for secure contexts. This needs a user copy action on that page and a configuration where the Pix feature is active. Any financial loss depends on the user then confirming a payment (inferred). The advisory describes this only as “Missing authorization in Payments” and does not report in-the-wild exploitation.

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
MockFacilitatedPaymentsDriver
components/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc
modified
FacilitatedPaymentsDriverTestBase
components/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc
modified
FacilitatedPaymentsDriverTestBase
components/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc
modified
TEST_P
components/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc
modified

Files Changed

  • components/facilitated_payments/core/browser/facilitated_payments_driver.cc
  • components/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc
  • components/facilitated_payments/content/browser/content_facilitated_payments_driver.h
  • components/facilitated_payments/core/browser/facilitated_payments_driver.h
  • components/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc
  • components/facilitated_payments/content/browser/content_facilitated_payments_driver.cc

Audit Directions

  • Sibling entry points with uneven gating
    When one entry point of a feature (here payment links) has a security check, check every other renderer-triggered entry point that reaches the same privileged UI (Pix copy, other facilitated-payment triggers, autofill-adjacent payment prompts) for the same check.
  • Authorization in shared core vs. platform layers
    Content-agnostic core/ classes cannot see frame state themselves. Look for core handlers that act on renderer-reported data without a virtual hook back into the content/ layer for origin and secure-context checks.
  • Frame lookup robustness
    The new override dereferences the result of RenderFrameHost::FromID without a null check. Review similar drivers for lifetime assumptions between the stored frame ID and asynchronous renderer events.
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/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);
 
+ protected:
+  // Returns true if the current page context is secure for payments.
+  virtual bool IsSecureForPaymentHandling() const = 0;
+
  private:
   const raw_ref<FacilitatedPaymentsClient> facilitated_payments_client_;
   FacilitatedPaymentsApiClientCreator api_client_creator_;
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…

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