CVE-2026-79042
Overview
Background
- Facilitated Payments / Pix
- A browser-side feature in
components/facilitated_paymentsthat spots a copied Brazilian Pix payment code (the test’sbr.gov.bcb.pixidentifier) and hands it toPixManager, 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
PixManagerorPaymentLinkManager.ContentFacilitatedPaymentsDriveris itscontent/subclass bound to aRenderFrameHost. - `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.
PixManager.Attack Path
- 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.
- 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). - Missing gate
Pre-patch, nothing checked whether the frame was a secure context, so the copied code was validated and forwarded to
PixManager::OnPixCodeCopiedToClipboard. - 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
(inferred). The advisory describes this only as “Missing authorization in Payments” and does not report in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
GURLcomponents/facilitated_payments/content/browser/content_facilitated_payments_driver.h |
modified | |
RenderFrameHostcomponents/facilitated_payments/content/browser/content_facilitated_payments_driver.h |
modified | |
MockPixManagercomponents/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc |
modified | |
MockPaymentLinkManagercomponents/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cc |
modified | |
MockFacilitatedPaymentsDrivercomponents/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc |
modified | |
FacilitatedPaymentsDriverTestBasecomponents/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc |
modified | |
FacilitatedPaymentsDriverTestBasecomponents/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc |
modified | |
TEST_Pcomponents/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cc |
modified |
Files Changed
components/facilitated_payments/core/browser/facilitated_payments_driver.cccomponents/facilitated_payments/core/browser/facilitated_payments_driver_unittest.cccomponents/facilitated_payments/content/browser/content_facilitated_payments_driver.hcomponents/facilitated_payments/core/browser/facilitated_payments_driver.hcomponents/facilitated_payments/content/browser/content_facilitated_payments_driver_unittest.cccomponents/facilitated_payments/content/browser/content_facilitated_payments_driver.cc
Audit Directions
- Sibling entry points with uneven gatingWhen 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 layersContent-agnostic
core/classes cannot see frame state themselves. Look for core handlers that act on renderer-reported data without a virtual hook back into thecontent/layer for origin and secure-context checks. - Frame lookup robustnessThe new override dereferences the result of
RenderFrameHost::FromIDwithout a null check. Review similar drivers for lifetime assumptions between the stored frame ID and asynchronous renderer events.
Patch
From 5ae225cd448101685abdb096bebb24822ef3f1f2 Mon Sep 17 00:00:00 2001 From: Luis Antunes <[email protected]> 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 <[email protected]> Reviewed-by: Slobodan Pejic <[email protected]> Commit-Queue: Luis Antunes <[email protected]> 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());
Regression Test / PoC
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());
Original Bug Report
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.cccomponents/facilitated_payments/core/browser/pix_manager.cccomponents/facilitated_payments/core/browser/facilitated_payments_driver.cccomponents/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
paymentfeature 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:
- Interception: The attacker intercepts traffic to a host on the
PIX_MERCHANT_ORIGINS_ALLOWLIST(e.g., a major Brazilian retailer). - Insecure Serving: The attacker serves a malicious page over plain HTTP or via a connection with an invalid SSL certificate.
- 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. - 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.
- 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.