CVE-2026-17742
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
validateSecurePaymentConfirmationRequestcomponents/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java |
modified | |
ifcomponents/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java |
modified | |
validateSecurePaymentConfirmationRequestcomponents/payments/content/android/java/src/org/chromium/components/payments/PaymentValidator.java |
modified | |
ifcomponents/payments/content/android/java/src/org/chromium/components/payments/PaymentValidator.java |
modified | |
validateSecurePaymentConfirmationRequestcomponents/payments/content/android/junit/src/org/chromium/components/payments/test_support/PaymentRequestServiceBuilder.java |
modified |
Files Changed
components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.javacomponents/payments/content/android/java/src/org/chromium/components/payments/PaymentValidator.javacomponents/payments/content/android/junit/src/org/chromium/components/payments/test_support/PaymentRequestServiceBuilder.javacomponents/payments/content/android/payment_validator_android.cccomponents/payments/content/payment_request.cc
Patch
From 67e652e50b3a07ba4a2b2c7d13a1a09ac80e654e Mon Sep 17 00:00:00 2001
From: Stephen McGruer <smcgruer@chromium.org>
Date: Wed, 10 Jun 2026 06:04:19 -0700
Subject: [PATCH] SPC: Disallow WebAuthn extensions in third-party cases
Disallow WebAuthn extensions for Secure Payment Confirmation calls that
are 'third-party' - that is, the caller is not the Relying Party. This
is done to avoid third-party usage of largeBlob and similar data-storing
extensions during the payment ceremony.
This is checked on both the renderer side
(secure_payment_confirmation_helper.cc) and the browser side
(secure_payment_confirmation_validation.cc). The change is put behind a
default-enabled feature flag:
kSecurePaymentConfirmationExtensionsAllowlist.
Bug: 499003233
Change-Id: I02db505f50535d344f734061566199039fa327f9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7869360
Commit-Queue: Stephen McGruer <smcgruer@chromium.org>
Reviewed-by: Slobodan Pejic <slobodan@chromium.org>
Auto-Submit: Stephen McGruer <smcgruer@chromium.org>
Reviewed-by: Rick Byers <rbyers@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1644585}
---
diff --git a/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java b/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java
index 11fde37..73bb3c8 100644
--- a/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java
+++ b/components/payments/content/android/java/src/org/chromium/components/payments/PaymentRequestService.java
@@ -298,8 +298,10 @@
* @return The validation error result.
*/
default @SecurePaymentConfirmationRequestValidationError int
- validateSecurePaymentConfirmationRequest(SecurePaymentConfirmationRequest request) {
- return PaymentValidator.validateSecurePaymentConfirmationRequest(request);
+ validateSecurePaymentConfirmationRequest(
+ SecurePaymentConfirmationRequest request, Origin initiatorOrigin) {
+ return PaymentValidator.validateSecurePaymentConfirmationRequest(
+ request, initiatorOrigin);
}
/**
@@ -541,14 +543,26 @@
PaymentErrorReason.INVALID_DATA_FROM_RENDERER);
return false;
}
- if (methodData.containsKey(MethodStrings.SECURE_PAYMENT_CONFIRMATION)
- && validateSecurePaymentConfirmationRequest(methodData, options)
- != SecurePaymentConfirmationRequestValidationError.OK) {
- mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
- disconnectFromClientWithDebugMessage(
- ErrorStrings.INVALID_PAYMENT_METHODS_OR_DATA,
- PaymentErrorReason.INVALID_DATA_FROM_RENDERER);
- return false;
+ if (methodData.containsKey(MethodStrings.SECURE_PAYMENT_CONFIRMATION)) {
+ @SecurePaymentConfirmationRequestValidationError
+ int validationResult = validateSecurePaymentConfirmationRequest(methodData, options);
+ if (validationResult != SecurePaymentConfirmationRequestValidationError.OK) {
+ mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
+
+ if (validationResult
+ == SecurePaymentConfirmationRequestValidationError
+ .WEB_AUTHN_EXTENSIONS_NOT_SUPPORTED) {
+ disconnectFromClientWithDebugMessage(
+ ErrorStrings.INVALID_PAYMENT_METHODS_OR_DATA,
+ PaymentErrorReason.NOT_SUPPORTED);
+ } else {
+ disconnectFromClientWithDebugMessage(
+ ErrorStrings.INVALID_PAYMENT_METHODS_OR_DATA,
+ PaymentErrorReason.INVALID_DATA_FROM_RENDERER);
+ }
+
+ return false;
+ }
}
methodData = Collections.unmodifiableMap(methodData);
@@ -627,7 +641,7 @@
// Delegate to the native implementation for final validation.
return mDelegate.validateSecurePaymentConfirmationRequest(
- spcMethodData.securePaymentConfirmation);
+ spcMethodData.securePaymentConfirmation, mPaymentRequestSecurityOrigin);
}
private void startPaymentAppService() {
diff --git a/components/payments/content/android/java/src/org/chromium/components/payments/PaymentValidator.java b/components/payments/content/android/java/src/org/chromium/components/payments/PaymentValidator.java
index 74ce867..bbceaeca 100644
--- a/components/payments/content/android/java/src/org/chromium/components/payments/PaymentValidator.java
+++ b/components/payments/content/android/java/src/org/chromium/components/payments/PaymentValidator.java
@@ -5,12 +5,14 @@
package org.chromium.components.payments;
import org.jni_zero.JNINamespace;
+import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;
import org.chromium.build.annotations.NullMarked;
import org.chromium.payments.mojom.PaymentDetails;
import org.chromium.payments.mojom.PaymentValidationErrors;
import org.chromium.payments.mojom.SecurePaymentConfirmationRequest;
+import org.chromium.url.Origin;
import java.nio.ByteBuffer;
@@ -33,12 +35,14 @@
}
public static @SecurePaymentConfirmationRequestValidationError int
- validateSecurePaymentConfirmationRequest(SecurePaymentConfirmationRequest request) {
- if (request == null) {
+ validateSecurePaymentConfirmationRequest(
+ SecurePaymentConfirmationRequest request, Origin initiatorOrigin) {
+ if (request == null || initiatorOrigin == null) {
return SecurePaymentConfirmationRequestValidationError.INTERNAL_ERROR;
}
return PaymentValidatorJni.get()
- .validateSecurePaymentConfirmationRequestAndroid(request.serialize());
+ .validateSecurePaymentConfirmationRequestAndroid(
+ request.serialize(), initiatorOrigin);
}
@NativeMethods
@@ -48,6 +52,7 @@
boolean validatePaymentValidationErrorsAndroid(ByteBuffer buffer);
@SecurePaymentConfirmationRequestValidationError
- int validateSecurePaymentConfirmationRequestAndroid(ByteBuffer buffer);
+ int validateSecurePaymentConfirmationRequestAndroid(
+ ByteBuffer buffer, @JniType("url::Origin") Origin initiatorOrigin);
}
}
diff --git a/components/payments/content/android/junit/src/org/chromium/components/payments/test_support/PaymentRequestServiceBuilder.java b/components/payments/content/android/junit/src/org/chromium/components/payments/test_support/PaymentRequestServiceBuilder.java
index 1115410a..13593a37 100644
--- a/components/payments/content/android/junit/src/org/chromium/components/payments/test_support/PaymentRequestServiceBuilder.java
+++ b/components/payments/content/android/junit/src/org/chromium/components/payments/test_support/PaymentRequestServiceBuilder.java
@@ -170,7 +170,8 @@
@Override
public @SecurePaymentConfirmationRequestValidationError int
- validateSecurePaymentConfirmationRequest(SecurePaymentConfirmationRequest request) {
+ validateSecurePaymentConfirmationRequest(
+ SecurePaymentConfirmationRequest request, Origin initiatorOrigin) {
return mIsSecurePaymentConfirmationRequestValid
? SecurePaymentConfirmationRequestValidationError.OK
: SecurePaymentConfirmationRequestValidationError.CREDENTIAL_IDS_REQUIRED;
diff --git a/components/payments/content/android/payment_validator_android.cc b/components/payments/content/android/payment_validator_android.cc
index 45aa98f..603dc59 100644
--- a/components/payments/content/android/payment_validator_android.cc
+++ b/components/payments/content/android/payment_validator_android.cc
@@ -17,6 +17,7 @@
#include "components/payments/core/payment_details_validation.h"
#include "components/payments/core/payments_validators.h"
#include "third_party/blink/public/mojom/payments/payment_request.mojom.h"
+#include "url/origin.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "components/payments/content/android/jni_headers/PaymentValidator_jni.h"
@@ -53,7 +54,8 @@
static jint
JNI_PaymentValidator_ValidateSecurePaymentConfirmationRequestAndroid(
JNIEnv* env,
- const base::android::JavaRef<jobject>& buffer) {
+ const base::android::JavaRef<jobject>& buffer,
+ const url::Origin& initiator_origin) {
mojom::SecurePaymentConfirmationRequestPtr request;
auto span = base::android::JavaByteBufferToSpan(env, buffer);
if (!mojom::SecurePaymentConfirmationRequest::Deserialize(
@@ -61,7 +63,8 @@
return static_cast<jint>(
SecurePaymentConfirmationRequestValidationError::kInternalError);
}
- return static_cast<jint>(IsValidSecurePaymentConfirmationRequest(request));
+ return static_cast<jint>(
+ IsValidSecurePaymentConfirmationRequest(request, initiator_origin));
}
} // namespace payments
diff --git a/components/payments/content/payment_request.cc b/components/payments/content/payment_request.cc
index 05c75a8..847c687 100644
--- a/components/payments/content/payment_request.cc
+++ b/components/payments/content/payment_request.cc
@@ -77,7 +77,8 @@
SecurePaymentConfirmationRequestValidationError
ValidateSecurePaymentConfirmationRequest(
const std::vector<mojom::PaymentMethodDataPtr>& method_data,
- const mojom::PaymentOptionsPtr& options) {
+ const mojom::PaymentOptionsPtr& options,
+ const url::Origin& initiator_origin) {
CHECK_GT(method_data.size(), 0u);
if (!base::FeatureList::IsEnabled(::features::kSecurePaymentConfirmation)) {
@@ -116,7 +117,7 @@
}
return IsValidSecurePaymentConfirmationRequest(
- method_data_entry->secure_payment_confirmation);
+ method_data_entry->secure_payment_confirmation, initiator_origin);
}
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 46e0a24..318667c 100644
--- a/components/payments/content/secure_payment_confirmation_app_factory_unittest.cc
+++ b/components/payments/content/secure_payment_confirmation_app_factory_unittest.cc
@@ -204,6 +204,9 @@
CreateSecurePaymentConfirmationRequest();
auto mock_delegate = std::make_unique<MockPaymentAppFactoryDelegate>(
web_contents_, std::move(method_data));
+ url::Origin caller_origin = url::Origin::Create(GURL("https://site.example"));
+ EXPECT_CALL(*mock_delegate, GetFrameSecurityOrigin())
+ .WillRepeatedly(ReturnRef(caller_origin));
EXPECT_CALL(*mock_delegate, OnPaymentAppCreationError(_, _)).Times(0);
secure_payment_confirmation_app_factory_->Create(mock_delegate->GetWeakPtr());
@@ -222,7 +225,7 @@
CreateMockDelegate(std::move(method_data));
url::Origin caller_origin = url::Origin::Create(GURL("https://rp.example"));
EXPECT_CALL(*mock_delegate, GetFrameSecurityOrigin())
- .WillOnce(ReturnRef(caller_origin));
+ .WillRepeatedly(ReturnRef(caller_origin));
EXPECT_CALL(*mock_credential_finder_, GetMatchingCredentials)
.WillOnce(RunOnceCallback<5>(GetMatchingCredentialsIsUnsupported()));
@@ -255,7 +258,7 @@
std::unique_ptr<MockPaymentAppFactoryDelegate> mock_delegate =
CreateMockDelegate(std::move(method_data));
EXPECT_CALL(*mock_delegate, GetFrameSecurityOrigin())
- .WillOnce(ReturnRef(caller_origin));
+ .WillRepeatedly(ReturnRef(caller_origin));
std::unique_ptr<PaymentApp> secure_payment_confirmation_app;
EXPECT_CALL(*mock_delegate, OnPaymentAppCreated(_))
@@ -296,7 +299,7 @@
std::unique_ptr<MockPaymentAppFactoryDelegate> mock_delegate =
CreateMockDelegate(std::move(method_data));
EXPECT_CALL(*mock_delegate, GetFrameSecurityOrigin())
- .WillOnce(ReturnRef(caller_origin));
+ .WillRepeatedly(ReturnRef(caller_origin));
EXPECT_CALL(*mock_delegate, OnPaymentAppCreated(_)).Times(0);
EXPECT_CALL(*mock_delegate, OnDoneCreatingPaymentApps());
@@ -332,7 +335,7 @@
std::unique_ptr<MockPaymentAppFactoryDelegate> mock_delegate =
CreateMockDelegate(std::move(method_data));
EXPECT_CALL(*mock_delegate, GetFrameSecurityOrigin())
- .WillOnce(ReturnRef(caller_origin));
+ .WillRepeatedly(ReturnRef(caller_origin));
// Ensure that the SecurePaymentConfirmationAppFactory extracts and passes in
// the correct set of credentials, relying party id, and caller origin. The
@@ -420,7 +423,7 @@
CreateMockDelegate(std::move(method_data));
url::Origin caller_origin = url::Origin::Create(GURL("https://rp.example"));
EXPECT_CALL(*mock_delegate, GetFrameSecurityOrigin())
- .WillOnce(ReturnRef(caller_origin));
+ .WillRepeatedly(ReturnRef(caller_origin));
std::unique_ptr<PaymentApp> created_payment_app;
EXPECT_CALL(*mock_delegate, OnPaymentAppCreated(_))
@@ -472,7 +475,7 @@
std::unique_ptr<MockPaymentAppFactoryDelegate> mock_delegate =
CreateMockDelegate(std::move(method_data));
EXPECT_CALL(*mock_delegate, GetFrameSecurityOrigin())
- .WillOnce(ReturnRef(caller_origin));
+ .WillRepeatedly(ReturnRef(caller_origin));
std::unique_ptr<PaymentApp> secure_payment_confirmation_app;
EXPECT_CALL(*mock_delegate, OnPaymentAppCreated(_))
.WillOnce(MoveArg<0>(&secure_payment_confirmation_app));
@@ -517,6 +520,9 @@
auto mock_delegate = std::make_unique<MockPaymentAppFactoryDelegate>(
web_contents_, std::move(method_data));
+ url::Origin caller_origin = url::Origin::Create(GURL("https://site.example"));
+ EXPECT_CALL(*mock_delegate, GetFrameSecurityOrigin())
+ .WillRepeatedly(ReturnRef(caller_origin));
scoped_refptr<MockWebPaymentsWebDataService> mock_service =
base::MakeRefCounted<MockWebPaymentsWebDataService>();
@@ -555,7 +561,7 @@
CreateMockDelegate(std::move(method_data));
url::Origin caller_origin = url::Origin::Create(GURL("https://site.example"));
EXPECT_CALL(*mock_delegate, GetFrameSecurityOrigin())
- .WillOnce(ReturnRef(caller_origin));
+ .WillRepeatedly(ReturnRef(caller_origin));
std::unique_ptr<PaymentApp> secure_payment_confirmation_app;
EXPECT_CALL(*mock_delegate, OnPaymentAppCreated(_))
.WillOnce(MoveArg<0>(&secure_payment_confirmation_app));
@@ -597,7 +603,7 @@
std::unique_ptr<MockPaymentAppFactoryDelegate> mock_delegate =
CreateMockDelegate(std::move(method_data));
EXPECT_CALL(*mock_delegate, GetFrameSecurityOrigin())
- .WillOnce(ReturnRef(caller_origin));
+ .WillRepeatedly(ReturnRef(caller_origin));
EXPECT_CALL(*mock_delegate, OnPaymentAppCreated(_)).Times(0);
EXPECT_CALL(*mock_delegate, OnDoneCreatingPaymentApps());
diff --git a/components/payments/content/secure_payment_confirmation_validation_unittest.cc b/components/payments/content/secure_payment_confirmation_validation_unittest.cc
index c986f3d..d94d25d 100644
--- a/components/payments/content/secure_payment_confirmation_validation_unittest.cc
+++ b/components/payments/content/secure_payment_confirmation_validation_unittest.cc
@@ -10,6 +10,7 @@
#include "components/payments/core/native_error_strings.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/payments/payment_request.mojom.h"
+#include "third_party/blink/public/mojom/webauthn/authenticator.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
@@ -35,7 +36,8 @@
TEST(SecurePaymentConfirmationValidationTest, IsValidRequest) {
auto request = CreateValidRequest();
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::kOk);
}
@@ -43,7 +45,8 @@
auto request = CreateValidRequest();
request->credential_ids.clear();
EXPECT_EQ(
- IsValidSecurePaymentConfirmationRequest(request),
+ payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::kCredentialIdsRequired);
}
@@ -51,7 +54,8 @@
auto request = CreateValidRequest();
request->credential_ids.emplace_back();
EXPECT_EQ(
- IsValidSecurePaymentConfirmationRequest(request),
+ payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::kCredentialIdsRequired);
}
@@ -59,14 +63,16 @@
auto request = CreateValidRequest();
request->challenge.clear();
EXPECT_EQ(
- IsValidSecurePaymentConfirmationRequest(request),
+ payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::kChallengeRequired);
}
TEST(SecurePaymentConfirmationValidationTest, EmptyDisplayName) {
auto request = CreateValidRequest();
request->instrument->display_name.clear();
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::
kInstrumentDisplayNameRequired);
}
@@ -74,7 +80,8 @@
TEST(SecurePaymentConfirmationValidationTest, EmptyInstrumentIcon) {
auto request = CreateValidRequest();
request->instrument->icon = GURL();
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::
kValidInstrumentIconRequired);
}
@@ -82,7 +89,8 @@
TEST(SecurePaymentConfirmationValidationTest, InvalidInstrumentIcon) {
auto request = CreateValidRequest();
request->instrument->icon = GURL("not-a-url");
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::
kValidInstrumentIconRequired);
}
@@ -90,7 +98,8 @@
TEST(SecurePaymentConfirmationValidationTest, NonUtf8InstrumentDetails) {
auto request = CreateValidRequest();
request->instrument->details = {'\xEF', '\xB7', '\xAF'};
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::
kNonUtf8InstrumentDetailsString);
}
@@ -98,7 +107,8 @@
TEST(SecurePaymentConfirmationValidationTest, EmptyInstrumentDetails) {
auto request = CreateValidRequest();
request->instrument->details = "";
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::
kEmptyInstrumentDetailsString);
}
@@ -106,7 +116,8 @@
TEST(SecurePaymentConfirmationValidationTest, TooLongInstrumentDetails) {
auto request = CreateValidRequest();
request->instrument->details = std::string(4097, '.');
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::
kTooLongInstrumentDetailsString);
}
@@ -124,7 +135,8 @@
for (const std::string& rp_id : invalid_cases) {
auto request = CreateValidRequest();
request->rp_id = rp_id;
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::kRpIdRequired)
<< "rp_id: " << rp_id;
}
@@ -134,7 +146,8 @@
auto request = CreateValidRequest();
request->payee_name.reset();
request->payee_origin.reset();
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::
kPayeeOriginOrPayeeNameRequired);
}
@@ -142,7 +155,8 @@
TEST(SecurePaymentConfirmationValidationTest, EmptyPayeeName) {
auto request = CreateValidRequest();
request->payee_name = "";
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::
kPayeeOriginOrPayeeNameRequired);
}
@@ -151,14 +165,16 @@
auto request = CreateValidRequest();
request->payee_origin = url::Origin::Create(GURL("http://site.example"));
EXPECT_EQ(
- IsValidSecurePaymentConfirmationRequest(request),
+ payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::kPayeeOriginMustBeHttps);
}
TEST(SecurePaymentConfirmationValidationTest, NullPaymentEntityLogo) {
auto request = CreateValidRequest();
request->payment_entities_logos.push_back(nullptr);
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::
kNonNullPaymentEntityLogoRequired);
}
@@ -168,7 +184,8 @@
request->payment_entities_logos.push_back(
mojom::PaymentEntityLogo::New(GURL(), "Label"));
EXPECT_EQ(
- IsValidSecurePaymentConfirmationRequest(request),
+ payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::kValidLogoUrlRequired);
}
@@ -177,7 +194,8 @@
request->payment_entities_logos.push_back(
mojom::PaymentEntityLogo::New(GURL("thisisnotaurl"), "Label"));
EXPECT_EQ(
- IsValidSecurePaymentConfirmationRequest(request),
+ payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::kValidLogoUrlRequired);
}
@@ -186,7 +204,8 @@
auto request = CreateValidRequest();
request->payment_entities_logos.push_back(mojom::PaymentEntityLogo::New(
GURL("blob://blob.foo.com/logo.png"), "Label"));
- EXPECT_EQ(IsValidSecurePaymentConfirmationRequest(request),
+ EXPECT_EQ(payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::
kValidLogoUrlSchemeRequired);
}
@@ -196,9 +215,53 @@
request->payment_entities_logos.push_back(mojom::PaymentEntityLogo::New(
GURL("https://entity.example/icon.png"), ""));
EXPECT_EQ(
- IsValidSecurePaymentConfirmationRequest(request),
+ payments::IsValidSecurePaymentConfirmationRequest(
+ request, url::Origin::Create(GURL("https://rp.example"))),
SecurePaymentConfirmationRequestValidationError::kLogoLabelRequired);
}
+TEST(SecurePaymentConfirmationValidationTest,
+ WebAuthnExtensionsAllowedForFirstParty) {
+ auto request = CreateValidRequest();
+ request->extensions =
... (truncated)
Original Bug Report
Cross-origin leak of WebAuthn secrets (PRF, largeBlob) via Secure Payment Confirmation
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 security team.
Overview: Secure Payment Confirmation (SPC) intentionally bypasses the standard WebAuthn registrable-domain constraint to allow third-party assertions. However, it fails to restrict the use of sensitive WebAuthn extensions like PRF and largeBlob during this bypass. This potentially allows a malicious origin to extract cryptographic secrets bound to a victim Relying Party (RP) through a deceptive user prompt.
Affected files:
components/payments/content/secure_payment_confirmation_app.cccontent/browser/webauth/authenticator_common_impl.cccontent/browser/webauth/webauth_request_security_checker.cccomponents/payments/content/secure_payment_confirmation_service.cc
Estimated timestamp from git blame: 2025-09-19
Vulnerability Details
Secure Payment Confirmation (SPC) allows a third-party origin (such as a merchant or payment provider) to request an assertion for a credential owned by a different Relying Party (RP). To support this, WebAuthRequestSecurityChecker::ValidateDomainAndRelyingPartyID explicitly bypasses the standard WebAuthn RP ID validation when the request type is kGetPaymentCredentialAssertion.
However, the system fails to restrict the use of sensitive WebAuthn extensions during this cross-origin bypass. When an SPC request is made, SecurePaymentConfirmationApp::InvokePaymentApp blindly clones the attacker-supplied extensions dictionary and passes it to AuthenticatorCommonImpl. Because AuthenticatorCommonImpl lacks logic to filter extensions based on the kGetPaymentCredentialAssertion request type, it forwards requests for extensions like prf, large_blob_read, and get_cred_blob directly to the platform authenticator.
Once the authenticator evaluates the extensions using the victim’s credential, the results (e.g., the HMAC-secret derived from the PRF) are returned to the browser. SecurePaymentConfirmationApp::SetAppSpecificResponseFields then explicitly clones these unverified extension outputs back into the PaymentResponse Mojo struct, returning them to the attacker’s origin.
On Desktop, this issue is exacerbated by a missing origin check in the Mojo interface. SecurePaymentConfirmationService::StorePaymentCredential allows a compromised renderer to poison the local database by binding an arbitrary credential_id to an arbitrary rp_id because it fails to verify that the rp_id matches the caller’s origin. This permits an attacker to target any WebAuthn credential on the device, rather than just those opted into SPC.
Potential Attack Steps
(Note: These are suggested steps based on source code analysis; a functional exploit has not yet been executed in a live environment.)
- DB Poisoning (Desktop only): An attacker with a compromised renderer calls the
StorePaymentCredentialMojo method, providing an arbitrarycredential_idand setting therp_idto a victim domain (e.g.,victim.com). The browser stores this in the local DB without validating the origin. - Malicious Request: The attacker runs JavaScript using the
PaymentRequestAPI to initiate an SPC request. They specifyrpId: 'victim.com', request sensitive WebAuthn extensions (prf,largeBlob), and provide a deceptivepayeeName(e.g., “Security Verification”) while omittingshowOptOutto hide the victim RP ID from the UI. - Bypass and Forwarding: The browser honors the SPC request, bypasses the RP ID check, and forwards the extension requests to the platform authenticator.
- Deceptive UI: The native SPC prompt appears, displaying the deceptive payee name and hiding the victim’s RP ID. The user clicks “Verify”.
- Exfiltration: The authenticator evaluates the PRF or largeBlob data for
victim.com. The browser blindly returns these results in thePaymentResponse, allowing the attacker’s JavaScript to read the cross-origin secrets.
Suggested Fix
- Restrict Extensions: In
AuthenticatorCommonImpl::ContinueGetAssertionAfterRpIdCheck, explicitly deny or filter out sensitive extensions (prf,large_blob_read,large_blob_write,get_cred_blob) if the request is an SPC assertion (request_type == RequestType::kGetPaymentCredentialAssertion). Alternatively, filter them inSecurePaymentConfirmationApp::InvokePaymentApp. - Enforce Origin Validation: In
SecurePaymentConfirmationService::StorePaymentCredential, verify that therp_idprovided by the renderer is a valid registrable domain suffix of theRenderFrameHost’s last committed origin to prevent database poisoning by a compromised renderer.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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.