Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactRace condition in Payments
DescriptionRace condition in Payments
ComponentPayments
Bug ClassRace
Tracker505991181
Fix commit01866da89405 (chromium/src) +80/-1240
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
EmbeddedWorkerInstanceClient
content/browser/payments/payment_app_content_unittest_base.cc
modified
PaymentAppForWorkerTestHelper
content/browser/payments/payment_app_content_unittest_base.h
modified

Files Changed

  • content/browser/BUILD.gn
  • content/browser/payments/payment_app_content_unittest_base.cc
  • content/browser/payments/payment_app_content_unittest_base.h
  • content/browser/payments/payment_app_database.cc
From 01866da8940558cfa254d7f460cf9f559292cd39 Mon Sep 17 00:00:00 2001
From: Stephen McGruer <[email protected]>
Date: Thu, 23 Jul 2026 17:28:09 -0700
Subject: [PATCH] Remove SetPaymentInstrument Mojo endpoint and dead code

Remove the deprecated SetPaymentInstrument Mojo method from
payment_app.mojom and clean up corresponding implementation code in
PaymentManager and PaymentAppDatabase. Delete PaymentAppInfoFetcher and
PaymentInstrumentIconFetcher which are now dead code.

Bug: 505991181, 537253616
Change-Id: Ia6e660eb2b96867c283570d75f773caf01fdca52
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8132017
Auto-Submit: Stephen McGruer <[email protected]>
Commit-Queue: Joe Mason <[email protected]>
Reviewed-by: Joe Mason <[email protected]>
Reviewed-by: Slobodan Pejic <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1667535}
---

diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index b811d6a..115130d 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -1423,8 +1423,6 @@
     "payments/payment_app_context_impl.h",
     "payments/payment_app_database.cc",
     "payments/payment_app_database.h",
-    "payments/payment_app_info_fetcher.cc",
-    "payments/payment_app_info_fetcher.h",
     "payments/payment_app_installer.cc",
     "payments/payment_app_installer.h",
     "payments/payment_app_provider_impl.cc",
@@ -1434,8 +1432,6 @@
     "payments/payment_event_dispatcher.h",
     "payments/payment_handler_web_contents_observer.cc",
     "payments/payment_handler_web_contents_observer.h",
-    "payments/payment_instrument_icon_fetcher.cc",
-    "payments/payment_instrument_icon_fetcher.h",
     "payments/payment_manager.cc",
     "payments/payment_manager.h",
     "payments/respond_with_callback.cc",
diff --git a/content/browser/payments/payment_app_content_unittest_base.cc b/content/browser/payments/payment_app_content_unittest_base.cc
index ea6497a7..51cb04ce 100644
--- a/content/browser/payments/payment_app_content_unittest_base.cc
+++ b/content/browser/payments/payment_app_content_unittest_base.cc
@@ -62,9 +62,7 @@
     : public EmbeddedWorkerTestHelper {
  public:
   PaymentAppForWorkerTestHelper()
-      : EmbeddedWorkerTestHelper(base::FilePath()),
-        last_sw_registration_id_(
-            blink::mojom::kInvalidServiceWorkerRegistrationId) {}
+      : EmbeddedWorkerTestHelper(base::FilePath()) {}
 
   PaymentAppForWorkerTestHelper(const PaymentAppForWorkerTestHelper&) = delete;
   PaymentAppForWorkerTestHelper& operator=(
@@ -72,6 +70,12 @@
 
   ~PaymentAppForWorkerTestHelper() override {}
 
+  void set_last_sw_registration_id(int64_t id) {
+    last_sw_registration_id_ = id;
+  }
+
+  void set_last_sw_scope(const GURL& scope) { last_sw_scope_ = scope; }
+
   class EmbeddedWorkerInstanceClient : public FakeEmbeddedWorkerInstanceClient {
    public:
     explicit EmbeddedWorkerInstanceClient(
@@ -164,7 +168,10 @@
     return std::make_unique<ServiceWorker>(this);
   }
 
-  int64_t last_sw_registration_id_;
+  // The registration ID and scope of the most recent service worker to be
+  // installed or started.
+  int64_t last_sw_registration_id_ =
+      blink::mojom::kInvalidServiceWorkerRegistrationId;
   GURL last_sw_scope_;
 
   // Variables to delay payment request response.
@@ -195,13 +202,12 @@
   return worker_helper_->browser_context();
 }
 
-PaymentManager*
-PaymentAppContentUnitTestBase::CreateUninitializedPaymentManager(
+int64_t PaymentAppContentUnitTestBase::RegisterAndActivateServiceWorker(
     const GURL& scope_url,
     const GURL& sw_script_url) {
   // Register service worker for payment manager.
   bool called = false;
-  int64_t registration_id;
+  int64_t registration_id = blink::mojom::kInvalidServiceWorkerRegistrationId;
   blink::mojom::ServiceWorkerRegistrationOptions registration_opt;
   registration_opt.scope = scope_url;
   const blink::StorageKey key =
@@ -219,6 +225,8 @@
 
   base::RunLoop().RunUntilIdle();
   EXPECT_TRUE(called);
+  worker_helper_->set_last_sw_registration_id(registration_id);
+  worker_helper_->set_last_sw_scope(scope_url);
 
   // Ensure the worker used for installation has stopped.
   called = false;
@@ -233,6 +241,15 @@
   base::RunLoop().RunUntilIdle();
   EXPECT_TRUE(called);
 
+  return registration_id;
+}
+
+PaymentManager*
+PaymentAppContentUnitTestBase::CreateUninitializedPaymentManager(
+    const GURL& scope_url,
+    const GURL& sw_script_url) {
+  RegisterAndActivateServiceWorker(scope_url, sw_script_url);
+
   // This function should eventually return created payment manager
   // but there is no way to get last created payment manager from
   // payment_app_context()->payment_managers_ because its type is std::map
diff --git a/content/browser/payments/payment_app_content_unittest_base.h b/content/browser/payments/payment_app_content_unittest_base.h
index 9b831a6a..79117b97 100644
--- a/content/browser/payments/payment_app_content_unittest_base.h
+++ b/content/browser/payments/payment_app_content_unittest_base.h
@@ -36,6 +36,8 @@
   ~PaymentAppContentUnitTestBase() override;
 
   BrowserContext* browser_context();
+  int64_t RegisterAndActivateServiceWorker(const GURL& scope_url,
+                                           const GURL& sw_script_url);
   PaymentManager* CreateUninitializedPaymentManager(const GURL& scope_url,
                                                     const GURL& sw_script_url);
   PaymentManager* CreatePaymentManager(const GURL& scope_url,
@@ -51,11 +53,12 @@
   void RespondPendingPaymentRequest(
       payments::mojom::PaymentHandlerResponsePtr response);
 
+  PaymentAppContextImpl* payment_app_context();
+
  private:
   class PaymentAppForWorkerTestHelper;
 
   StoragePartitionImpl* storage_partition();
-  PaymentAppContextImpl* payment_app_context();
 
   std::unique_ptr<BrowserTaskEnvironment> task_environment_;
   std::unique_ptr<PaymentAppForWorkerTestHelper> worker_helper_;
diff --git a/content/browser/payments/payment_app_database.cc b/content/browser/payments/payment_app_database.cc
index f6104b74..db0921d9 100644
--- a/content/browser/payments/payment_app_database.cc
+++ b/content/browser/payments/payment_app_database.cc
@@ -219,170 +219,6 @@
           weak_ptr_factory_.GetWeakPtr(), instrument_key, std::move(callback)));
 }
 
-void PaymentAppDatabase::WritePaymentInstrument(
-    const GURL& scope,
-    const std::string& instrument_key,
-    PaymentInstrumentPtr instrument,
-    WritePaymentInstrumentCallback callback) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
-  // TODO(crbug.com/40177656): Update this when PaymentManager
-  // implements StorageKey.
-  if (instrument->icons.size() > 0) {
-    std::vector<blink::Manifest::ImageResource> icons(instrument->icons);
-    PaymentInstrumentIconFetcher::Start(
-        scope,
-        service_worker_context_->GetWindowClientFrameRoutingIds(
-            blink::StorageKey::CreateFirstParty(url::Origin::Create(scope))),
-        icons,
-        base::BindOnce(&PaymentAppDatabase::DidFetchedPaymentInstrumentIcon,
-                       weak_ptr_factory_.GetWeakPtr(), scope, instrument_key,
-                       std::move(instrument), std::move(callback)));
-  } else {
-    service_worker_context_->FindReadyRegistrationForScope(
-        scope, blink::StorageKey::CreateFirstParty(url::Origin::Create(scope)),
-        base::BindOnce(
-            &PaymentAppDatabase::DidFindRegistrationToWritePaymentInstrument,
-            weak_ptr_factory_.GetWeakPtr(), instrument_key,
-            std::move(instrument), std::string(), std::move(callback)));
-  }
-}
-
-void PaymentAppDatabase::DidFetchedPaymentInstrumentIcon(
-    const GURL& scope,
-    const std::string& instrument_key,
-    payments::mojom::PaymentInstrumentPtr instrument,
-    WritePaymentInstrumentCallback callback,
-    const std::string& icon) {
-  DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
-  if (icon.empty()) {
-    std::move(callback).Run(PaymentHandlerStatus::FETCH_INSTRUMENT_ICON_FAILED);
-    return;
-  }
-
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/payments/payment_app_content_unittest_base.cc b/content/browser/payments/payment_app_content_unittest_base.cc
index ea6497a7..51cb04ce 100644
--- a/content/browser/payments/payment_app_content_unittest_base.cc
+++ b/content/browser/payments/payment_app_content_unittest_base.cc
@@ -62,9 +62,7 @@
     : public EmbeddedWorkerTestHelper {
  public:
   PaymentAppForWorkerTestHelper()
-      : EmbeddedWorkerTestHelper(base::FilePath()),
-        last_sw_registration_id_(
-            blink::mojom::kInvalidServiceWorkerRegistrationId) {}
+      : EmbeddedWorkerTestHelper(base::FilePath()) {}
 
   PaymentAppForWorkerTestHelper(const PaymentAppForWorkerTestHelper&) = delete;
   PaymentAppForWorkerTestHelper& operator=(
@@ -72,6 +70,12 @@
 
   ~PaymentAppForWorkerTestHelper() override {}
 
+  void set_last_sw_registration_id(int64_t id) {
+    last_sw_registration_id_ = id;
+  }
+
+  void set_last_sw_scope(const GURL& scope) { last_sw_scope_ = scope; }
+
   class EmbeddedWorkerInstanceClient : public FakeEmbeddedWorkerInstanceClient {
    public:
     explicit EmbeddedWorkerInstanceClient(
@@ -164,7 +168,10 @@
     return std::make_unique<ServiceWorker>(this);
   }
 
-  int64_t last_sw_registration_id_;
+  // The registration ID and scope of the most recent service worker to be
+  // installed or started.
+  int64_t last_sw_registration_id_ =
+      blink::mojom::kInvalidServiceWorkerRegistrationId;
   GURL last_sw_scope_;
 
   // Variables to delay payment request response.
@@ -195,13 +202,12 @@
   return worker_helper_->browser_context();
 }
 
-PaymentManager*
-PaymentAppContentUnitTestBase::CreateUninitializedPaymentManager(
+int64_t PaymentAppContentUnitTestBase::RegisterAndActivateServiceWorker(
     const GURL& scope_url,
     const GURL& sw_script_url) {
   // Register service worker for payment manager.
   bool called = false;
-  int64_t registration_id;
+  int64_t registration_id = blink::mojom::kInvalidServiceWorkerRegistrationId;
   blink::mojom::ServiceWorkerRegistrationOptions registration_opt;
   registration_opt.scope = scope_url;
   const blink::StorageKey key =
@@ -219,6 +225,8 @@
 
   base::RunLoop().RunUntilIdle();
   EXPECT_TRUE(called);
+  worker_helper_->set_last_sw_registration_id(registration_id);
+  worker_helper_->set_last_sw_scope(scope_url);
 
   // Ensure the worker used for installation has stopped.
   called = false;
@@ -233,6 +241,15 @@
   base::RunLoop().RunUntilIdle();
   EXPECT_TRUE(called);
 
+  return registration_id;
+}
+
+PaymentManager*
+PaymentAppContentUnitTestBase::CreateUninitializedPaymentManager(
+    const GURL& scope_url,
+    const GURL& sw_script_url) {
+  RegisterAndActivateServiceWorker(scope_url, sw_script_url);
+
   // This function should eventually return created payment manager
   // but there is no way to get last created payment manager from
   // payment_app_context()->payment_managers_ because its type is std::map
diff --git a/content/browser/payments/payment_app_content_unittest_base.h b/content/browser/payments/payment_app_content_unittest_base.h
index 9b831a6a..79117b97 100644
--- a/content/browser/payments/payment_app_content_unittest_base.h
+++ b/content/browser/payments/payment_app_content_unittest_base.h
@@ -36,6 +36,8 @@
   ~PaymentAppContentUnitTestBase() override;
 
   BrowserContext* browser_context();
+  int64_t RegisterAndActivateServiceWorker(const GURL& scope_url,
+                                           const GURL& sw_script_url);
   PaymentManager* CreateUninitializedPaymentManager(const GURL& scope_url,
                                                     const GURL& sw_script_url);
   PaymentManager* CreatePaymentManager(const GURL& scope_url,
@@ -51,11 +53,12 @@
   void RespondPendingPaymentRequest(
       payments::mojom::PaymentHandlerResponsePtr response);
 
+  PaymentAppContextImpl* payment_app_context();
+
  private:
   class PaymentAppForWorkerTestHelper;
 
   StoragePartitionImpl* storage_partition();
-  PaymentAppContextImpl* payment_app_context();
 
   std::unique_ptr<BrowserTaskEnvironment> task_environment_;
   std::unique_ptr<PaymentAppForWorkerTestHelper> worker_helper_;
diff --git a/content/browser/payments/payment_app_provider_impl_unittest.cc b/content/browser/payments/payment_app_provider_impl_unittest.cc
index 02c3bbb..e6b7b5bb 100644
--- a/content/browser/payments/payment_app_provider_impl_unittest.cc
+++ b/content/browser/payments/payment_app_provider_impl_unittest.cc
@@ -14,6 +14,7 @@
 #include "base/test/run_until.h"
 #include "content/browser/payments/installed_payment_apps_finder_impl.h"
 #include "content/browser/payments/payment_app_content_unittest_base.h"
+#include "content/browser/payments/payment_app_installer.h"
 #include "content/browser/payments/payment_handler_web_contents_observer.h"
 #include "content/public/browser/payment_app_provider.h"
 #include "content/public/browser/web_contents.h"
@@ -42,11 +43,6 @@
 using ::payments::mojom::PaymentInstrument;
 using ::payments::mojom::PaymentInstrumentPtr;
 
-void SetPaymentInstrumentCallback(PaymentHandlerStatus* out_status,
-                                  PaymentHandlerStatus status) {
-  *out_status = status;
-}
-
 void GetAllPaymentAppsCallback(
     InstalledPaymentAppsFinder::PaymentApps* out_apps,
     InstalledPaymentAppsFinder::PaymentApps apps) {
@@ -108,15 +104,25 @@
 
   ~PaymentAppProviderTest() override {}
 
-  void SetPaymentInstrument(
-      PaymentManager* manager,
-      const std::string& instrument_key,
-      PaymentInstrumentPtr instrument,
-      PaymentManager::SetPaymentInstrumentCallback callback) {
-    ASSERT_NE(nullptr, manager);
-    manager->SetPaymentInstrument(instrument_key, std::move(instrument),
-                                  std::move(callback));
-    base::RunLoop().RunUntilIdle();
+  void InstallPaymentApp(const GURL& scope,
+                         const GURL& sw_url,
+                         const std::string& method = "fake-method") {
+    int64_t registration_id = RegisterAndActivateServiceWorker(scope, sw_url);
+    base::RunLoop run_loop;
+    payment_app_context()
+        ->payment_app_database()
+        ->SetPaymentAppInfoForRegisteredServiceWorker(
+            registration_id, scope.spec(), "Test App", /*icon=*/"", method,
+            SupportedDelegations(),
+            base::BindOnce(
+                [](base::OnceClosure quit,
+                   payments::mojom::PaymentHandlerStatus status) {
+                  EXPECT_EQ(payments::mojom::PaymentHandlerStatus::SUCCESS,
+                            status);
+                  std::move(quit).Run();
+                },
+                run_loop.QuitClosure()));
+    run_loop.Run();
   }
 
   void GetAllPaymentApps(
@@ -195,13 +201,8 @@
 };
 
 TEST_F(PaymentAppProviderTest, AbortPaymentTest) {
-  PaymentManager* manager = CreatePaymentManager(
-      GURL("https://example.test"), GURL("https://example.test/script.js"));
-
-  PaymentHandlerStatus status;
-  SetPaymentInstrument(manager, "payment_instrument_key",
-                       payments::mojom::PaymentInstrument::New(),
-                       base::BindOnce(&SetPaymentInstrumentCallback, &status));
+  InstallPaymentApp(GURL("https://example.test"),
+                    GURL("https://example.test/script.js"));
 
   InstalledPaymentAppsFinder::PaymentApps apps;
   GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));
@@ -209,7 +210,8 @@
 
   bool payment_aborted = false;
   base::RunLoop loop;
-  AbortPayment(last_sw_registration_id(), url::Origin::Create(apps[0]->scope),
+  AbortPayment(last_sw_registration_id(),
+               url::Origin::Create(apps[last_sw_registration_id()]->scope),
                "id",
                base::BindOnce(&CaptureAbortResult, loop.QuitClosure(),
                               &payment_aborted));
@@ -218,13 +220,8 @@
 }
 
 TEST_F(PaymentAppProviderTest, CanMakePaymentTest) {
-  PaymentManager* manager = CreatePaymentManager(
-      GURL("https://example.test"), GURL("https://example.test/script.js"));
-
-  PaymentHandlerStatus status;
-  SetPaymentInstrument(manager, "payment_instrument_key",
-                       payments::mojom::PaymentInstrument::New(),
-                       base::BindOnce(&SetPaymentInstrumentCallback, &status));
+  InstallPaymentApp(GURL("https://example.test"),
+                    GURL("https://example.test/script.js"));
 
   InstalledPaymentAppsFinder::PaymentApps apps;
   GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));
@@ -249,28 +246,16 @@
 }
 
 TEST_F(PaymentAppProviderTest, InvokePaymentAppTest) {
-  PaymentManager* manager1 =
-      CreatePaymentManager(GURL("https://hellopay.test/a/"),
-                           GURL("https://hellopay.test/a/script.js"));
-  PaymentManager* manager2 = CreatePaymentManager(
-      GURL("https://bobpay.test/b/"), GURL("https://bobpay.test/b/script.js"));
-
-  PaymentHandlerStatus status;
-  SetPaymentInstrument(manager1, "test_key1",
-                       payments::mojom::PaymentInstrument::New(),
-                       base::BindOnce(&SetPaymentInstrumentCallback, &status));
-  SetPaymentInstrument(manager2, "test_key2",
-                       payments::mojom::PaymentInstrument::New(),
-                       base::BindOnce(&SetPaymentInstrumentCallback, &status));
-  SetPaymentInstrument(manager2, "test_key3",
-                       payments::mojom::PaymentInstrument::New(),
-                       base::BindOnce(&SetPaymentInstrumentCallback, &status));
+  InstallPaymentApp(GURL("https://hellopay.test/a/"),
+                    GURL("https://hellopay.test/a/script.js"));
+  InstallPaymentApp(GURL("https://bobpay.test/b/"),
+                    GURL("https://bobpay.test/b/script.js"));
+  int64_t bobpay_registration_id = last_sw_registration_id();
 
   InstalledPaymentAppsFinder::PaymentApps apps;
   GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));
   ASSERT_EQ(2U, apps.size());
 
-  int64_t bobpay_registration_id = last_sw_registration_id();
   EXPECT_EQ(apps[bobpay_registration_id]->scope.spec(),
             "https://bobpay.test/b/");
 
@@ -288,95 +273,48 @@
 }
 
 TEST_F(PaymentAppProviderTest, GetAllPaymentAppsTest) {
-  PaymentManager* manager1 =
-      CreatePaymentManager(GURL("https://hellopay.test/a/"),
-                           GURL("https://hellopay.test/a/script.js"));
+  InstallPaymentApp(GURL("https://hellopay.test/a/"),
+                    GURL("https://hellopay.test/a/script.js"), "hellopay");
   int64_t hellopay_registration_id = last_sw_registration_id();
-
-  PaymentManager* manager2 = CreatePaymentManager(
-      GURL("https://bobpay.test/b/"), GURL("https://bobpay.test/b/script.js"));
+  InstallPaymentApp(GURL("https://bobpay.test/b/"),
+                    GURL("https://bobpay.test/b/script.js"), "bobpay");
   int64_t bobpay_registration_id = last_sw_registration_id();
 
-  PaymentHandlerStatus status;
-  PaymentInstrumentPtr instrument_1 = PaymentInstrument::New();
-  instrument_1->method = "hellopay";
-  SetPaymentInstrument(manager1, "test_key1", std::move(instrument_1),
-                       base::BindOnce(&SetPaymentInstrumentCallback, &status));
-
-  PaymentInstrumentPtr instrument_2 = PaymentInstrument::New();
-  instrument_2->method = "hellopay";
-  SetPaymentInstrument(manager2, "test_key2", std::move(instrument_2),
-                       base::BindOnce(&SetPaymentInstrumentCallback, &status));
-
-  PaymentInstrumentPtr instrument_3 = PaymentInstrument::New();
-  instrument_3->method = "bobpay";
-  SetPaymentInstrument(manager2, "test_key3", std::move(instrument_3),
-                       base::BindOnce(&SetPaymentInstrumentCallback, &status));
-
   InstalledPaymentAppsFinder::PaymentApps apps;
   GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));
 
   ASSERT_EQ(2U, apps.size());
   ASSERT_EQ(1U, apps[hellopay_registration_id]->enabled_methods.size());
-  ASSERT_EQ(2U, apps[bobpay_registration_id]->enabled_methods.size());
+  ASSERT_EQ(1U, apps[bobpay_registration_id]->enabled_methods.size());
 }
 
 TEST_F(PaymentAppProviderTest, GetAllPaymentAppsFromTheSameOriginTest) {
-  PaymentManager* manager1 = CreatePaymentManager(
-      GURL("https://bobpay.test/a/"), GURL("https://bobpay.test/a/script.js"));
+  InstallPaymentApp(GURL("https://bobpay.test/a/"),
+                    GURL("https://bobpay.test/a/script.js"), "hellopay");
   int64_t bobpay_a_registration_id = last_sw_registration_id();
-
-  PaymentManager* manager2 = CreatePaymentManager(
-      GURL("https://bobpay.test/b/"), GURL("https://bobpay.test/b/script.js"));
+  InstallPaymentApp(GURL("https://bobpay.test/b/"),
+                    GURL("https://bobpay.test/b/script.js"), "bobpay");
   int64_t bobpay_b_registration_id = last_sw_registration_id();
 
-  PaymentHandlerStatus status;
-  PaymentInstrumentPtr instrument_1 = PaymentInstrument::New();
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

TOCTOU in PaymentAppInfoFetcher allows 1-bit cross-origin oracle

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Time-Of-Check to Time-Of-Use vulnerability exists in PaymentAppInfoFetcher. A compromised renderer can delay a web app manifest response and navigate the frame, causing a subsequent icon download to execute in the context of the newly committed cross-origin frame. This allows an attacker to leak cross-origin state via a 1-bit oracle.

Affected files:

  • content/browser/payments/payment_app_info_fetcher.cc
  • content/browser/web_contents/web_contents_impl.cc
  • content/browser/manifest/manifest_icon_downloader.cc
  • content/public/browser/manifest_icon_downloader.h

Estimated timestamp from git blame: 2024-07-19

Summary

A potential Time-Of-Check to Time-Of-Use (TOCTOU) vulnerability exists in the PaymentAppInfoFetcher component of the Payment Request API. A compromised renderer can exploit this to trigger a credentialed icon fetch using the context of a cross-origin site (e.g., a site the user has navigated to in the same tab). This results in a 1-bit cross-origin oracle that can leak information like login status or resource existence on the victim site.

Root Cause

The vulnerability resides in PaymentAppInfoFetcher::SelfDeleteFetcher, which handles fetching payment app information, including the manifest and icon.

  1. In SelfDeleteFetcher::Start(), the code verifies that the provided context_url is same-origin with the WebContents’s current primary main frame (web_contents_->GetLastCommittedURL()). After this check passes, it stores a WeakPtr to the WebContents and initiates an asynchronous IPC to the renderer to fetch the web app manifest via GetManifest().
  2. When the manifest response eventually arrives, execution resumes in SelfDeleteFetcher::FetchPaymentAppManifestCallback. The code verifies that the WebContents is still alive via the WeakPtr, but it fails to re-verify that the frame’s current origin is still the same as the original context_url.
  3. The code then extracts an icon URL from the manifest and initiates a download via ManifestIconDownloader::Download(). Crucially, this function call does not specify an initiator_frame_routing_id.
  4. Inside WebContentsImpl::DownloadImageInFrame(), because the initiator_frame_routing_id is empty, the browser falls back to calling GetPrimaryMainFrame() to resolve the initiator frame.
  5. If the tab has navigated cross-origin between the time the manifest was requested and the response was received, GetPrimaryMainFrame() will return the newly committed, cross-origin frame. Consequently, the DownloadImage Mojo request is sent to the new trusted renderer, executing a first-party, credentialed fetch.

Potential Attack Vector

Note: These are suggested steps; our tooling has not executed this as a live exploit.

  1. An attacker controls a compromised renderer process hosting https://attacker.com/.
  2. The attacker uses the bound payments::mojom::PaymentManager Mojo interface to call Init(), ensuring the browser will attempt to fetch payment app info, followed by SetPaymentInstrument().
  3. The browser validates the origin in PaymentAppInfoFetcher, passes the check, and sends a RequestManifest IPC to the attacker’s renderer.
  4. The attacker’s compromised renderer intentionally stalls the RequestManifest response and initiates a top-level navigation of the frame to https://victim.com/.
  5. After the browser commits the navigation (updating GetPrimaryMainFrame() to point to victim.com), the old attacker.com frame enters the pending_delete state but remains alive to run unload handlers. During this window, the attacker’s renderer finally sends the delayed RequestManifestResponse, containing an icon URL pointing to a private resource on victim.com (e.g., https://victim.com/private_avatar.png).
  6. The browser processes the response. Because it skips re-validating the origin, it attempts to download the icon.
  7. Due to the lack of an initiator_frame_routing_id, the download request falls back to GetPrimaryMainFrame(), routing the DownloadImage request to the trusted victim.com renderer.
  8. The victim.com renderer executes a same-origin fetch for the image, attaching all relevant credentials (including SameSite=Strict cookies).
  9. The success or failure of the image decode operation is returned to the browser. This result dictates whether the original SetPaymentInstrument call succeeds or fails (FETCH_PAYMENT_APP_INFO_FAILED).
  10. The attacker receives this success/failure signal via their original Mojo callback, providing a 1-bit oracle confirming whether the credentialed fetch on victim.com succeeded.

Impact

While an attacker cannot read the image pixels directly, they gain a 1-bit oracle that can detect whether a specific credentialed request on a cross-origin site succeeded. This can be used to infer the user’s login state or the existence of private resources on the victim domain.

Suggested Fix

  1. In PaymentAppInfoFetcher::SelfDeleteFetcher::FetchPaymentAppManifestCallback, re-validate that url::IsSameOriginWith(context_url_, web_contents_->GetLastCommittedURL()) before proceeding with the icon download.
  2. In PaymentAppInfoFetcher::SelfDeleteFetcher::Start, capture the GlobalRenderFrameHostId of the validated frame and pass it explicitly to ManifestIconDownloader::Download() rather than relying on the GetPrimaryMainFrame() fallback. This ensures the download request is routed to the correct, originally validated frame, failing safely if that frame is no longer active.

Evaluated with Chrome root at commit: 3acbde3302da0cb19488c22c0eb007c791207b4b


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. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker