CVE-2026-78894
Overview
Background
- `PaymentManager` / `payment_app.mojom`
- the renderer-reachable Mojo interface that a service-worker-backed payment handler page uses to manage its payment app. Per the changed-file list, this patch edits
payment_app.mojom,payment_manager.ccandpayment_manager.hto remove theSetPaymentInstrumentendpoint; its callback typePaymentManager::SetPaymentInstrumentCallbackdisappears from the tests. - `PaymentAppDatabase`
- the browser-process (UI thread) store that saves payment app and instrument records as service-worker registration user data, using
StoreRegistrationUserDataandGetRegistrationUserDataByKeyPrefix. - Multi-hop async chains
- the removed
WritePaymentInstrumentandFetchAndUpdatePaymentAppInfoflows each made several asynchronous hops (icon/manifest fetch →FindReadyRegistrationForScope→ user-data read/write), bound withweak_ptr_factory_.GetWeakPtr(). - `PaymentAppInfoFetcher::SelfDeleteFetcher`
- a heap object allocated with
newthat owns its own lifetime while it collects window-client frames fromGetWindowClientFrameRoutingIdsand downloads manifest and icon data.
Root Cause Analysis
SetPaymentInstrument Mojo method, its backing PaymentAppDatabase::WritePaymentInstrument chain, the FetchAndUpdatePaymentAppInfo chain, and the PaymentAppInfoFetcher (and, per the removed include, PaymentInstrumentIconFetcher) helpers. The removed code shows why these paths were race-prone. Each flow captured a scope when the request arrived, then after one or two asynchronous fetches re-resolved the registration with FindReadyRegistrationForScope(scope, ...) and only then wrote user data. The registration that was written to could therefore differ from, or outlive, the state at request time. In addition, DidGetPaymentAppInfoToUpdatePaymentAppInfo performed a non-atomic read-modify-write: it parsed the stored StoredPaymentAppProto, merged fetched fields and called StoreRegistrationUserData, so concurrent invocations could interleave. The diff does not show which interleaving the bug report used. The SelfDeleteFetcher lifetime and the PaymentManager side are truncated or absent, so the exact racing objects are (inferred). Removing the renderer-callable entry point and all its async continuations eliminates every such interleaving, and the tests now install apps through the synchronous-from-the-caller SetPaymentAppInfoForRegisteredServiceWorker path instead.Attack Path
- Reach the endpoint
a page controlling a payment-handler service worker calls
PaymentManager::SetPaymentInstrumentover Mojo. The diff confirms this method existed, because the tests calledmanager->SetPaymentInstrument(...). - Start an async chain
the call reaches
PaymentAppDatabase::WritePaymentInstrument, which either startsPaymentInstrumentIconFetcher::Startusing the scope’s window-client frames or goes straight toFindReadyRegistrationForScope. The relatedFetchAndUpdatePaymentAppInfopath similarly startsPaymentAppInfoFetcher::Start. - Change state during the window
while the fetch is pending, the renderer issues more calls or changes the service worker registration, frames or manager state. Which concrete state change triggered the bug is not shown in the diff
(inferred). - Stale completion
the continuation (
DidFetchedPaymentInstrumentIcon,DidFindRegistrationToWritePaymentInstrument,DidGetPaymentAppInfoToUpdatePaymentAppInfo) runs against whatever registration or state exists at that moment and writes or merges user data. The resulting effect, whether inconsistent payment-app records or a lifetime bug in the fetcher, is(inferred).
Impact Assessment
(inferred). The diff contains nothing about in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/payments/payment_app_database.cc |
modified | |
forcontent/browser/payments/payment_app_database.cc |
modified | |
ServiceWorkerRegistrationcontent/browser/payments/payment_app_database.h |
modified | |
PaymentAppDatabasecontent/browser/payments/payment_app_database.h |
modified | |
CONTENT_EXPORTcontent/browser/payments/payment_app_database.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.cccontent/browser/payments/payment_app_database.hcontent/browser/payments/payment_app_provider_impl_unittest.cccontent/browser/payments/payment_manager.cccontent/browser/payments/payment_manager.hcontent/browser/payments/payment_manager_unittest.ccthird_party/blink/public/mojom/payments/payment_app.mojom
Audit Directions
- Late re-resolution by scopein the remaining
PaymentAppDatabaseflows (SetPaymentAppInfoForRegisteredServiceWorker,ClearPaymentInstruments,SetPaymentAppUserHint, and the read paths), look for chains that capture ascopeorregistration_id, cross an asynchronous fetch, and then act on whatever registration exists at completion without re-checking origin or identity. - Read-modify-write on registration user dataany
GetRegistrationUserData*→ mutate proto →StoreRegistrationUserDatasequence without serialization can lose updates or combine state from different requests. Check the other payment and background-* databases (background_fetch,content_index,periodic_background_sync) for the same pattern. - Self-deleting fetchers bound to framesother
SelfDeleteFetcher-style helpers that captureGetWindowClientFrameRoutingIdsand outlive the frames or thePaymentManagerthat started them should be checked for lifetime races (frame deletion, profile shutdown, service worker unregistration during the fetch).
Patch
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;
- }
-
- // TODO(crbug.com/40177656): Update this when PaymentManager
- // implements StorageKey.
- 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),
- icon, std::move(callback)));
-}
-
-void PaymentAppDatabase::FetchAndUpdatePaymentAppInfo(
- const GURL& context,
- const GURL& scope,
- FetchAndUpdatePaymentAppInfoCallback callback) {
- DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
- PaymentAppInfoFetcher::Start(
- context, service_worker_context_,
- base::BindOnce(&PaymentAppDatabase::FetchPaymentAppInfoCallback,
- weak_ptr_factory_.GetWeakPtr(), scope,
- std::move(callback)));
-}
-
-void PaymentAppDatabase::FetchPaymentAppInfoCallback(
- const GURL& scope,
- FetchAndUpdatePaymentAppInfoCallback callback,
- std::unique_ptr<PaymentAppInfoFetcher::PaymentAppInfo> app_info) {
- DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
- // TODO(crbug.com/40177656): Update this when PaymentManager
- // implements StorageKey.
- service_worker_context_->FindReadyRegistrationForScope(
- scope, blink::StorageKey::CreateFirstParty(url::Origin::Create(scope)),
- base::BindOnce(
- &PaymentAppDatabase::DidFindRegistrationToUpdatePaymentAppInfo,
- weak_ptr_factory_.GetWeakPtr(), std::move(callback),
- std::move(app_info)));
-}
-
-void PaymentAppDatabase::DidFindRegistrationToUpdatePaymentAppInfo(
- FetchAndUpdatePaymentAppInfoCallback callback,
- std::unique_ptr<PaymentAppInfoFetcher::PaymentAppInfo> app_info,
- blink::ServiceWorkerStatusCode status,
- scoped_refptr<ServiceWorkerRegistration> registration) {
- DCHECK_CURRENTLY_ON(BrowserThread::UI);
- if (status != blink::ServiceWorkerStatusCode::kOk) {
- std::move(callback).Run(PaymentHandlerStatus::NO_ACTIVE_WORKER);
- return;
- }
-
- service_worker_context_->GetRegistrationUserDataByKeyPrefix(
- registration->id(), CreatePaymentAppKey(registration->scope().spec()),
- base::BindOnce(
- &PaymentAppDatabase::DidGetPaymentAppInfoToUpdatePaymentAppInfo,
- weak_ptr_factory_.GetWeakPtr(), std::move(callback),
- std::move(app_info), registration));
-}
-
-void PaymentAppDatabase::DidGetPaymentAppInfoToUpdatePaymentAppInfo(
- FetchAndUpdatePaymentAppInfoCallback callback,
- std::unique_ptr<PaymentAppInfoFetcher::PaymentAppInfo> app_info,
- scoped_refptr<ServiceWorkerRegistration> registration,
- const std::vector<std::string>& data,
- blink::ServiceWorkerStatusCode status) {
- DCHECK_CURRENTLY_ON(BrowserThread::UI);
- if (status != blink::ServiceWorkerStatusCode::kOk) {
- std::move(callback).Run(PaymentHandlerStatus::NO_ACTIVE_WORKER);
- return;
- }
-
- DCHECK_LE(data.size(), 1U);
- StoredPaymentAppProto payment_app_proto;
- if (data.size() == 1U) {
- payment_app_proto.ParseFromString(data[0]);
- }
- payment_app_proto.set_registration_id(registration->id());
- payment_app_proto.set_scope(registration->scope().spec());
- // Do not override name and icon if they are invalid.
- if (!app_info->name.empty()) {
- payment_app_proto.set_name(app_info->name);
- }
- if (!app_info->icon.empty()) {
- payment_app_proto.set_icon(app_info->icon);
- }
- payment_app_proto.set_prefer_related_applications(
- app_info->prefer_related_applications);
- for (const auto& related_app : app_info->related_applications) {
- StoredRelatedApplicationProto* related_app_proto =
- payment_app_proto.add_related_applications();
- related_app_proto->set_platform(related_app.platform);
- related_app_proto->set_id(related_app.id);
- }
-
- std::string serialized_payment_app;
- bool success = payment_app_proto.SerializeToString(&serialized_payment_app);
- DCHECK(success);
-
- service_worker_context_->StoreRegistrationUserData(
- registration->id(), registration->key(),
- {{CreatePaymentAppKey(registration->scope().spec()),
- serialized_payment_app}},
- base::BindOnce(&PaymentAppDatabase::DidUpdatePaymentApp,
- weak_ptr_factory_.GetWeakPtr(), std::move(callback),
- app_info->name.empty() || app_info->icon.empty()));
-}
-
-void PaymentAppDatabase::DidUpdatePaymentApp(
- FetchAndUpdatePaymentAppInfoCallback callback,
- bool fetch_app_info_failed,
- blink::ServiceWorkerStatusCode status) {
- DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
- PaymentHandlerStatus handler_status =
- fetch_app_info_failed
- ? PaymentHandlerStatus::FETCH_PAYMENT_APP_INFO_FAILED
- : PaymentHandlerStatus::SUCCESS;
- handler_status = status == blink::ServiceWorkerStatusCode::kOk
- ? handler_status
- : PaymentHandlerStatus::STORAGE_OPERATION_FAILED;
- return std::move(callback).Run(handler_status);
-}
-
void PaymentAppDatabase::ClearPaymentInstruments(
const GURL& scope,
ClearPaymentInstrumentsCallback callback) {
@@ -893,68 +729,6 @@
std::move(callback).Run(PaymentHandlerStatus::SUCCESS);
}
-void PaymentAppDatabase::DidFindRegistrationToWritePaymentInstrument(
- const std::string& instrument_key,
- PaymentInstrumentPtr instrument,
- const std::string& decoded_instrument_icon,
- WritePaymentInstrumentCallback callback,
- blink::ServiceWorkerStatusCode status,
- scoped_refptr<ServiceWorkerRegistration> registration) {
- DCHECK_CURRENTLY_ON(BrowserThread::UI);
- if (status != blink::ServiceWorkerStatusCode::kOk) {
- std::move(callback).Run(PaymentHandlerStatus::NO_ACTIVE_WORKER);
- return;
- }
-
- StoredPaymentInstrumentProto instrument_proto;
- instrument_proto.set_registration_id(registration->id());
- instrument_proto.set_decoded_instrument_icon(decoded_instrument_icon);
- instrument_proto.set_instrument_key(instrument_key);
- instrument_proto.set_name(instrument->name);
- instrument_proto.set_method(instrument->method);
- for (const auto& icon : instrument->icons) {
- StoredPaymentInstrumentImageObject* image_object_proto =
- instrument_proto.add_icons();
- image_object_proto->set_src(icon.src.spec());
- image_object_proto->set_type(base::UTF16ToUTF8(icon.type));
- for (const auto& size : icon.sizes) {
- ImageSizeProto* size_proto = image_object_proto->add_sizes();
- size_proto->set_width(size.width());
- size_proto->set_height(size.height());
- }
- }
-
- std::string serialized_instrument;
- bool success = instrument_proto.SerializeToString(&serialized_instrument);
- DCHECK(success);
-
- StoredPaymentInstrumentKeyInfoProto key_info_proto;
- key_info_proto.set_key(instrument_key);
- key_info_proto.set_insertion_order(base::Time::Now().ToInternalValue());
-
- std::string serialized_key_info;
- success = key_info_proto.SerializeToString(&serialized_key_info);
- DCHECK(success);
-
- service_worker_context_->StoreRegistrationUserData(
- registration->id(), registration->key(),
- {{CreatePaymentInstrumentKey(instrument_key), serialized_instrument},
- {CreatePaymentInstrumentKeyInfoKey(instrument_key),
- serialized_key_info}},
- base::BindOnce(&PaymentAppDatabase::DidWritePaymentInstrument,
- weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
-}
-
-void PaymentAppDatabase::DidWritePaymentInstrument(
- WritePaymentInstrumentCallback callback,
- blink::ServiceWorkerStatusCode status) {
- DCHECK_CURRENTLY_ON(BrowserThread::UI);
- return std::move(callback).Run(
- status == blink::ServiceWorkerStatusCode::kOk
- ? PaymentHandlerStatus::SUCCESS
- : PaymentHandlerStatus::STORAGE_OPERATION_FAILED);
-}
-
void PaymentAppDatabase::DidFindRegistrationToClearPaymentInstruments(
const GURL& scope,
ClearPaymentInstrumentsCallback callback,
diff --git a/content/browser/payments/payment_app_database.h b/content/browser/payments/payment_app_database.h
index d176f56..3989166 100644
--- a/content/browser/payments/payment_app_database.h
+++ b/content/browser/payments/payment_app_database.h
@@ -10,10 +10,9 @@
#include <vector>
#include "base/memory/weak_ptr.h"
-#include "content/browser/payments/payment_app_info_fetcher.h"
-#include "content/browser/payments/payment_instrument_icon_fetcher.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/browser/service_worker/service_worker_registration.h"
+#include "content/common/content_export.h"
#include "content/public/browser/stored_payment_app.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
#include "third_party/blink/public/mojom/payments/payment_app.mojom.h"
@@ -22,7 +21,7 @@
class ServiceWorkerRegistration;
-class PaymentAppDatabase {
+class CONTENT_EXPORT PaymentAppDatabase {
public:
using PaymentApps = std::map<int64_t, std::unique_ptr<StoredPaymentApp>>;
using ReadAllPaymentAppsCallback = base::OnceCallback<void(PaymentApps)>;
@@ -37,10 +36,6 @@
payments::mojom::PaymentHandlerStatus)>;
using HasPaymentInstrumentCallback =
base::OnceCallback<void(payments::mojom::PaymentHandlerStatus)>;
- using WritePaymentInstrumentCallback =
- base::OnceCallback<void(payments::mojom::PaymentHandlerStatus)>;
- using FetchAndUpdatePaymentAppInfoCallback =
- base::OnceCallback<void(payments::mojom::PaymentHandlerStatus)>;
using ClearPaymentInstrumentsCallback =
base::OnceCallback<void(payments::mojom::PaymentHandlerStatus)>;
using SetPaymentAppInfoCallback =
@@ -69,14 +64,6 @@
void HasPaymentInstrument(const GURL& scope,
const std::string& instrument_key,
HasPaymentInstrumentCallback callback);
- void WritePaymentInstrument(const GURL& scope,
- const std::string& instrument_key,
- payments::mojom::PaymentInstrumentPtr instrument,
- WritePaymentInstrumentCallback callback);
- void FetchAndUpdatePaymentAppInfo(
- const GURL& context,
- const GURL& scope,
- FetchAndUpdatePaymentAppInfoCallback callback);
void ClearPaymentInstruments(const GURL& scope,
ClearPaymentInstrumentsCallback callback);
void SetPaymentAppUserHint(const GURL& scope, const std::string& user_hint);
@@ -148,45 +135,6 @@
const std::vector<std::string>& data,
blink::ServiceWorkerStatusCode status);
- // WritePaymentInstrument callbacks
- void DidFindRegistrationToWritePaymentInstrument(
- const std::string& instrument_key,
- payments::mojom::PaymentInstrumentPtr instrument,
- const std::string& decoded_instrument_icon,
- WritePaymentInstrumentCallback callback,
- blink::ServiceWorkerStatusCode status,
- scoped_refptr<ServiceWorkerRegistration> registration);
- void DidWritePaymentInstrument(WritePaymentInstrumentCallback callback,
- blink::ServiceWorkerStatusCode status);
-
- // FetchAndUpdatePaymentAppInfo callbacks.
- void FetchPaymentAppInfoCallback(
- const GURL& scope,
- FetchAndUpdatePaymentAppInfoCallback callback,
- std::unique_ptr<PaymentAppInfoFetcher::PaymentAppInfo> app_info);
- void DidFindRegistrationToUpdatePaymentAppInfo(
- FetchAndUpdatePaymentAppInfoCallback callback,
- std::unique_ptr<PaymentAppInfoFetcher::PaymentAppInfo> app_info,
- blink::ServiceWorkerStatusCode status,
- scoped_refptr<ServiceWorkerRegistration> registration);
- void DidGetPaymentAppInfoToUpdatePaymentAppInfo(
- FetchAndUpdatePaymentAppInfoCallback callback,
- std::unique_ptr<PaymentAppInfoFetcher::PaymentAppInfo> app_info,
- scoped_refptr<ServiceWorkerRegistration> registration,
- const std::vector<std::string>& data,
- blink::ServiceWorkerStatusCode status);
- void DidUpdatePaymentApp(FetchAndUpdatePaymentAppInfoCallback callback,
- bool fetch_app_info_failed,
- blink::ServiceWorkerStatusCode status);
-
- // PaymentInstrumentIconFetcherCallback.
- void DidFetchedPaymentInstrumentIcon(
- const GURL& scope,
- const std::string& instrument_key,
- payments::mojom::PaymentInstrumentPtr instrument,
- WritePaymentInstrumentCallback callback,
- const std::string& icon);
-
// ClearPaymentInstruments callbacks
void DidFindRegistrationToClearPaymentInstruments(
const GURL& scope,
diff --git a/content/browser/payments/payment_app_info_fetcher.cc b/content/browser/payments/payment_app_info_fetcher.cc
deleted file mode 100644
index e9136dbb..0000000
--- a/content/browser/payments/payment_app_info_fetcher.cc
+++ /dev/null
@@ -1,316 +0,0 @@
-// Copyright 2017 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "content/browser/payments/payment_app_info_fetcher.h"
-
-#include <limits>
-#include <string_view>
-#include <utility>
-
-#include "base/base64.h"
-#include "base/functional/bind.h"
-#include "base/functional/callback_helpers.h"
-#include "base/task/sequenced_task_runner.h"
-#include "components/payments/content/icon/icon_size.h"
-#include "content/browser/renderer_host/render_frame_host_impl.h"
-#include "content/browser/service_worker/service_worker_context_wrapper.h"
-#include "content/browser/web_contents/web_contents_impl.h"
-#include "content/public/browser/browser_task_traits.h"
-#include "content/public/browser/browser_thread.h"
-#include "content/public/browser/global_routing_id.h"
-#include "content/public/browser/manifest_icon_downloader.h"
-#include "content/public/browser/page.h"
-#include "third_party/blink/public/common/manifest/manifest_icon_selector.h"
-#include "third_party/blink/public/common/manifest/manifest_util.h"
-#include "third_party/blink/public/common/storage_key/storage_key.h"
-#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
-#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
-#include "ui/gfx/codec/png_codec.h"
-#include "url/origin.h"
-
-namespace content {
-
-PaymentAppInfoFetcher::PaymentAppInfo::PaymentAppInfo() {}
-
-PaymentAppInfoFetcher::PaymentAppInfo::~PaymentAppInfo() {}
-
-void PaymentAppInfoFetcher::Start(
- const GURL& context_url,
- scoped_refptr<ServiceWorkerContextWrapper> service_worker_context,
- PaymentAppInfoFetchCallback callback) {
- DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
- std::unique_ptr<std::vector<GlobalRenderFrameHostId>> frame_routing_ids =
- service_worker_context->GetWindowClientFrameRoutingIds(
- blink::StorageKey::CreateFirstParty(
- url::Origin::Create(context_url)));
-
- SelfDeleteFetcher* fetcher = new SelfDeleteFetcher(std::move(callback));
- fetcher->Start(context_url, std::move(frame_routing_ids));
-}
-
-PaymentAppInfoFetcher::SelfDeleteFetcher::SelfDeleteFetcher(
... (file diff truncated)
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..51cb04c 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.