CVE-2026-17955
Overview
Files Changed
components/payments/content/secure_payment_confirmation_app_factory.cccomponents/payments/content/secure_payment_confirmation_app_factory_unittest.cc
Patch
From c5bda04d9412eb19f4bf134561eb42615bda7f00 Mon Sep 17 00:00:00 2001
From: Luis Antunes <luisantunes@google.com>
Date: Tue, 09 Jun 2026 12:03:54 -0700
Subject: [PATCH] [SPC] Correctly truncate payment_entities_logos to 2 elements
SecurePaymentConfirmationAppFactory::Create attempts to restrict the
renderer-supplied payment_entities_logos vector to a maximum of two
entries. However, it incorrectly uses the single-iterator overload of
std::vector::erase, which only deletes the element at index 2 instead
of truncating all elements from that point onward.
This CL fixes this by calling the range-based erase overload to remove
all elements from index 2 to the end of the vector.
Fixed: 517385072
Change-Id: I5aa838a980a736aa011e16688be63d88b6d3a579
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7903500
Reviewed-by: Stephen McGruer <smcgruer@chromium.org>
Reviewed-by: Slobodan Pejic <slobodan@chromium.org>
Commit-Queue: Luis Antunes <luisantunes@google.com>
Cr-Commit-Position: refs/heads/main@{#1644149}
---
diff --git a/components/payments/content/secure_payment_confirmation_app_factory.cc b/components/payments/content/secure_payment_confirmation_app_factory.cc
index 8691f20b..58aa01ba 100644
--- a/components/payments/content/secure_payment_confirmation_app_factory.cc
+++ b/components/payments/content/secure_payment_confirmation_app_factory.cc
@@ -183,7 +183,8 @@
// than 2 logos are provided.
if (spc_request->payment_entities_logos.size() > 2) {
spc_request->payment_entities_logos.erase(
- spc_request->payment_entities_logos.begin() + 2);
+ spc_request->payment_entities_logos.begin() + 2,
+ spc_request->payment_entities_logos.end());
}
// Record if the user will be offered an opt-out experience. Technically
diff --git a/components/payments/content/secure_payment_confirmation_app_factory_unittest.cc b/components/payments/content/secure_payment_confirmation_app_factory_unittest.cc
index 0180ce20..46e0a24 100644
--- a/components/payments/content/secure_payment_confirmation_app_factory_unittest.cc
+++ b/components/payments/content/secure_payment_confirmation_app_factory_unittest.cc
@@ -357,6 +357,8 @@
GURL("https://payment-entity-2.example/icon.png");
const GURL kPaymentEntity3LogoUrl =
GURL("https://payment-entity-3.example/icon.png");
+ const GURL kPaymentEntity4LogoUrl =
+ GURL("https://payment-entity-4.example/icon.png");
SecurePaymentConfirmationAppFactoryPaymentEntitiesLogosTest() = default;
@@ -410,6 +412,8 @@
kPaymentEntity2LogoUrl, "Payment Entity 2"));
spc_request->payment_entities_logos.push_back(mojom::PaymentEntityLogo::New(
kPaymentEntity3LogoUrl, "Payment Entity 3"));
+ spc_request->payment_entities_logos.push_back(mojom::PaymentEntityLogo::New(
+ kPaymentEntity4LogoUrl, "Payment Entity 4"));
method_data->secure_payment_confirmation = std::move(spc_request);
std::unique_ptr<MockPaymentAppFactoryDelegate> mock_delegate =
@@ -432,9 +436,9 @@
FakeImageDownloaded(kPaymentEntity2LogoUrl, /*succeeded=*/true,
/*height=*/60);
- // Even though the third entity logo was not downloaded (and was not attempted
- // to be downloaded), the first two should be sufficient and the payment app
- // should be created.
+ // Even though the third and fourth entity logos were not downloaded (and were
+ // not attempted to be downloaded), the first two should be sufficient and the
+ // payment app should be created.
ASSERT_TRUE(created_payment_app);
EXPECT_THAT(
created_payment_app->GetPaymentEntitiesLogos(),
Regression Test / PoC
diff --git a/components/payments/content/secure_payment_confirmation_app_factory_unittest.cc b/components/payments/content/secure_payment_confirmation_app_factory_unittest.cc
index 0180ce20..46e0a24 100644
--- a/components/payments/content/secure_payment_confirmation_app_factory_unittest.cc
+++ b/components/payments/content/secure_payment_confirmation_app_factory_unittest.cc
@@ -357,6 +357,8 @@
GURL("https://payment-entity-2.example/icon.png");
const GURL kPaymentEntity3LogoUrl =
GURL("https://payment-entity-3.example/icon.png");
+ const GURL kPaymentEntity4LogoUrl =
+ GURL("https://payment-entity-4.example/icon.png");
SecurePaymentConfirmationAppFactoryPaymentEntitiesLogosTest() = default;
@@ -410,6 +412,8 @@
kPaymentEntity2LogoUrl, "Payment Entity 2"));
spc_request->payment_entities_logos.push_back(mojom::PaymentEntityLogo::New(
kPaymentEntity3LogoUrl, "Payment Entity 3"));
+ spc_request->payment_entities_logos.push_back(mojom::PaymentEntityLogo::New(
+ kPaymentEntity4LogoUrl, "Payment Entity 4"));
method_data->secure_payment_confirmation = std::move(spc_request);
std::unique_ptr<MockPaymentAppFactoryDelegate> mock_delegate =
@@ -432,9 +436,9 @@
FakeImageDownloaded(kPaymentEntity2LogoUrl, /*succeeded=*/true,
/*height=*/60);
- // Even though the third entity logo was not downloaded (and was not attempted
- // to be downloaded), the first two should be sufficient and the payment app
- // should be created.
+ // Even though the third and fourth entity logos were not downloaded (and were
+ // not attempted to be downloaded), the first two should be sufficient and the
+ // payment app should be created.
ASSERT_TRUE(created_payment_app);
EXPECT_THAT(
created_payment_app->GetPaymentEntitiesLogos(),
Original Bug Report
Potential SPC WYSIWYS break: unshown paymentEntitiesLogos signed into clientDataJSON
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logical flaw in Secure Payment Confirmation (SPC) allows a potential attacker to include unshown payment logos in the cryptographically signed clientDataJSON payload. The browser attempts to cap the logo array size to two elements but uses a single-iterator std::vector::erase, which only removes one element. These extra elements are subsequently signed by the WebAuthn credential without ever being displayed to the user.
Affected files:
components/payments/content/secure_payment_confirmation_app_factory.cccomponents/payments/content/secure_payment_confirmation_app.cccontent/browser/webauth/client_data_json.ccchrome/browser/ui/views/payments/secure_payment_confirmation_dialog_view.cccomponents/payments/content/android/spc/java/src/org/chromium/components/payments/secure_payment_confirmation/SecurePaymentConfirmationViewBinder.java
Estimated timestamp from git blame: 2025-06-06
Root Cause
In SecurePaymentConfirmationAppFactory::Create, the browser attempts to restrict the renderer-supplied payment_entities_logos vector to a maximum of two entries. However, on lines 184–187, it incorrectly uses the single-iterator overload of std::vector::erase, which only deletes exactly one element instead of truncating all elements from that point onward:
// components/payments/content/secure_payment_confirmation_app_factory.cc:184-187
if (spc_request->payment_entities_logos.size() > 2) {
spc_request->payment_entities_logos.erase(
spc_request->payment_entities_logos.begin() + 2);
}
For an input vector of length $N \ge 4$ (e.g. [L0, L1, L2, L3]), this operation results in [L0, L1, L3], which retains $N - 1 = 3$ elements and completely preserves elements at index 3 and above. No other size validation boundaries or IDL array limits are enforced in the pipeline to restrict the array length.
Potential Vulnerability Mechanism
- Triggering the Request: A page initiates an SPC payment flow via the
PaymentRequestAPI containing four or more logos inpaymentEntitiesLogos(e.g.,[L0, L1, L2, L3]). - Defective Truncation: The request passes browser-side validation. The defective
eraseis executed, which erases onlyL2. This leaves a vector of size 3:[L0, L1, L3]. - UI Rendering (Omission of Indices $\ge 2$):
- Desktop UI (
secure_payment_confirmation_dialog_view.cc): Explicitly renders only the logos at index 0 and index 1. Any logo at index 2 or higher is silently dropped from the display. - Android UI (
SecurePaymentConfirmationViewBinder.java): Only displays the primary (index 0) and secondary (index 1) logos. Although there is an assertionassert (logos.size() == 2);, Java assertions compile out in release builds, enabling the execution to proceed silently.
- Desktop UI (
- Signing Path: After the user approves and completes authentication, all elements stored in
payment_entities_logos_(including the hidden logos at indices $\ge 2$) are mapped intoShownPaymentEntityLogoMojom structures, serialized into thepaymentEntitiesLogosarray within theclientDataJSONblock insidecontent/browser/webauth/client_data_json.cc, and cryptographically signed.
Potential Impact
This constitutes a potential direct violation of Secure Payment Confirmation’s What-You-See-Is-What-You-Sign (WYSIWYS) integrity guarantee. A malicious merchant could present the user with two benign logos while smuggling additional, arbitrary {url, label} pairs into the signed payment evidence. Since the verifying Relying Party relies on the signed clientDataJSON as browser-attested proof of what transaction details the user actually saw, they will be misled into believing the user consented to/saw logos and labels that were completely hidden from view.
Potential Reproduction Steps
- On a web page with an enrolled SPC credential, execute the following script:
const req = new PaymentRequest([{ supportedMethods: 'secure-payment-confirmation', data: { rpId: 'rp.example', credentialIds: [knownId], challenge: new Uint8Array([1]), instrument: {displayName: 'Card', icon: 'https://rp.example/card.png'}, payeeOrigin: 'https://merchant.example', paymentEntitiesLogos: [ {url: 'https://a/benign1.png', label: 'Visa'}, {url: 'https://a/benign2.png', label: 'Bank'}, {url: 'https://a/x.png', label: 'erased-by-bug'}, {url: 'https://a/hidden.png', label: 'HIDDEN-NOT-SHOWN'} ] } }], {total: {label: 'Total', amount: {currency: 'USD', value: '1.00'}}}); const resp = await req.show(); - Observe that the SPC confirmation dialog displays only ‘Visa’ and ‘Bank’.
- Authenticate and complete the payment flow.
- Decode the returned
resp.details.response.clientDataJSON. Note that thepaymentEntitiesLogosarray contains three entries, including theHIDDEN-NOT-SHOWNlogo (https://a/hidden.png), cryptographically signed as if it were displayed to the user.
(Note: Our tooling agent doesn’t yet have the ability to run code, and these reproduction steps are suggested/potential steps.)
Suggested Fix
Modify SecurePaymentConfirmationAppFactory::Create in components/payments/content/secure_payment_confirmation_app_factory.cc to use the range-based erase overload to remove all elements from index 2 to the end of the vector:
if (spc_request->payment_entities_logos.size() > 2) {
spc_request->payment_entities_logos.erase(
spc_request->payment_entities_logos.begin() + 2,
spc_request->payment_entities_logos.end());
}
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.