CVE-2026-78894
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
EmbeddedWorkerInstanceClientcontent/browser/payments/payment_app_content_unittest_base.cc |
modified | |
PaymentAppForWorkerTestHelpercontent/browser/payments/payment_app_content_unittest_base.h |
modified |
Files Changed
content/browser/BUILD.gncontent/browser/payments/payment_app_content_unittest_base.cccontent/browser/payments/payment_app_content_unittest_base.hcontent/browser/payments/payment_app_database.cc
Patch
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; - } -
Regression Test / PoC
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)
Original Bug Report
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.cccontent/browser/web_contents/web_contents_impl.cccontent/browser/manifest/manifest_icon_downloader.cccontent/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.
- In
SelfDeleteFetcher::Start(), the code verifies that the providedcontext_urlis same-origin with theWebContents’s current primary main frame (web_contents_->GetLastCommittedURL()). After this check passes, it stores aWeakPtrto theWebContentsand initiates an asynchronous IPC to the renderer to fetch the web app manifest viaGetManifest(). - When the manifest response eventually arrives, execution resumes in
SelfDeleteFetcher::FetchPaymentAppManifestCallback. The code verifies that theWebContentsis still alive via theWeakPtr, but it fails to re-verify that the frame’s current origin is still the same as the originalcontext_url. - The code then extracts an icon URL from the manifest and initiates a download via
ManifestIconDownloader::Download(). Crucially, this function call does not specify aninitiator_frame_routing_id. - Inside
WebContentsImpl::DownloadImageInFrame(), because theinitiator_frame_routing_idis empty, the browser falls back to callingGetPrimaryMainFrame()to resolve the initiator frame. - 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, theDownloadImageMojo 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.
- An attacker controls a compromised renderer process hosting
https://attacker.com/. - The attacker uses the bound
payments::mojom::PaymentManagerMojo interface to callInit(), ensuring the browser will attempt to fetch payment app info, followed bySetPaymentInstrument(). - The browser validates the origin in
PaymentAppInfoFetcher, passes the check, and sends aRequestManifestIPC to the attacker’s renderer. - The attacker’s compromised renderer intentionally stalls the
RequestManifestresponse and initiates a top-level navigation of the frame tohttps://victim.com/. - After the browser commits the navigation (updating
GetPrimaryMainFrame()to point tovictim.com), the oldattacker.comframe enters thepending_deletestate but remains alive to rununloadhandlers. During this window, the attacker’s renderer finally sends the delayedRequestManifestResponse, containing an icon URL pointing to a private resource onvictim.com(e.g.,https://victim.com/private_avatar.png). - The browser processes the response. Because it skips re-validating the origin, it attempts to download the icon.
- Due to the lack of an
initiator_frame_routing_id, the download request falls back toGetPrimaryMainFrame(), routing theDownloadImagerequest to the trustedvictim.comrenderer. - The
victim.comrenderer executes a same-origin fetch for the image, attaching all relevant credentials (includingSameSite=Strictcookies). - The success or failure of the image decode operation is returned to the browser. This result dictates whether the original
SetPaymentInstrumentcall succeeds or fails (FETCH_PAYMENT_APP_INFO_FAILED). - The attacker receives this success/failure signal via their original Mojo callback, providing a 1-bit oracle confirming whether the credentialed fetch on
victim.comsucceeded.
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
- In
PaymentAppInfoFetcher::SelfDeleteFetcher::FetchPaymentAppManifestCallback, re-validate thaturl::IsSameOriginWith(context_url_, web_contents_->GetLastCommittedURL())before proceeding with the icon download. - In
PaymentAppInfoFetcher::SelfDeleteFetcher::Start, capture theGlobalRenderFrameHostIdof the validated frame and pass it explicitly toManifestIconDownloader::Download()rather than relying on theGetPrimaryMainFrame()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.