CVE-2026-11245
Overview
Files Changed
chrome/browser/payments/service_worker_payment_app_finder_browsertest.cccomponents/payments/content/installable_payment_app_crawler.cccomponents/test/data/payments/harry.example.test/payment-manifest.json
Patch
From aa2bdb25afbea26bf1bd404d30710ade11bc4882 Mon Sep 17 00:00:00 2001 From: Xuehui Chen <[email protected]> Date: Mon, 13 Apr 2026 13:10:11 -0700 Subject: [PATCH] Use post-redirect URL to resolve relative default applications. The Payment Method Manifest specification requires that relative URLs in the 'default_applications' field be resolved against the manifest's own URL. Previously, InstallablePaymentAppCrawler incorrectly used the original merchant-supplied URL as the base for resolution, even if redirects had occurred. This logic error could allow a malicious merchant to perform a path confusion attack. By providing an initial URL that redirects to a legitimate manifest, the merchant could cause relative application paths to be resolved against their own controlled path (on the same origin), potentially leading to the installation of an unintended or vulnerable payment handler. This CL ensures that the post-redirect URL is passed as the base URL when parsing the manifest. Follow https://g-issues.chromium.org/issues/497610654#comment2, test pass with local build: https://paste.googleplex.com/4868129831387136#l=13 Bug: 497610654 Change-Id: Ic92336a3d5729d6aa075305f32ac44407aec7970 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7738137 Reviewed-by: Darwin Yang <[email protected]> Reviewed-by: Slobodan Pejic <[email protected]> Commit-Queue: Xuehui Chen <[email protected]> Cr-Commit-Position: refs/heads/main@{#1613923} --- diff --git a/chrome/browser/payments/service_worker_payment_app_finder_browsertest.cc b/chrome/browser/payments/service_worker_payment_app_finder_browsertest.cc index 8a41289..c0fbf6a 100644 --- a/chrome/browser/payments/service_worker_payment_app_finder_browsertest.cc +++ b/chrome/browser/payments/service_worker_payment_app_finder_browsertest.cc @@ -836,6 +836,24 @@ } } +// The payment method https://george.example.test/webpay redirects to +// https://harry.example.test/webpay, which serves a manifest with a relative +// URL. This test verifies that the relative URL is resolved against the +// post-redirect URL (harry) and not the pre-redirect URL (george). +IN_PROC_BROWSER_TEST_F( + ServiceWorkerPaymentAppFinderBrowserTest, + RedirectResolvesRelativeManifestUrlAgainstPostRedirectUrl) { + { + GetAllPaymentAppsForMethods({"https://george.example.test/webpay"}); + + EXPECT_TRUE(apps().empty()); + ASSERT_EQ(1U, installable_apps().size()); + ExpectInstallablePaymentAppInScope("https://harry.example.test/webpay"); + EXPECT_TRUE(error_message().empty()) + << "Expected no error, but error message was: " << error_message(); + } +} + // The payment method https://larry.example.test/webpay is not valid, because of // its cross-origin service worker scope https://harry.example.test/webpay/". IN_PROC_BROWSER_TEST_F(ServiceWorkerPaymentAppFinderBrowserTest, diff --git a/components/payments/content/installable_payment_app_crawler.cc b/components/payments/content/installable_payment_app_crawler.cc index 5c68f25a..12ece9b 100644 --- a/components/payments/content/installable_payment_app_crawler.cc +++ b/components/payments/content/installable_payment_app_crawler.cc @@ -142,7 +142,7 @@ number_of_payment_method_manifest_to_parse_++; parser_->ParsePaymentMethodManifest( - method_manifest_url, content, + method_manifest_url_after_redirects, content, base::BindOnce( &InstallablePaymentAppCrawler::OnPaymentMethodManifestParsed, weak_ptr_factory_.GetWeakPtr(), method_manifest_url, diff --git a/components/test/data/payments/harry.example.test/payment-manifest.json b/components/test/data/payments/harry.example.test/payment-manifest.json index ad9a26b..7d6560a0 100644 --- a/components/test/data/payments/harry.example.test/payment-manifest.json +++ b/components/test/data/payments/harry.example.test/payment-manifest.json @@ -1,3 +1,3 @@ { - "default_applications": ["https://harry.example.test/app.json"] -} + "default_applications": ["./app.json"] +} \ No newline at end of file
Regression Test / PoC
diff --git a/chrome/browser/payments/service_worker_payment_app_finder_browsertest.cc b/chrome/browser/payments/service_worker_payment_app_finder_browsertest.cc
index 8a41289..c0fbf6a 100644
--- a/chrome/browser/payments/service_worker_payment_app_finder_browsertest.cc
+++ b/chrome/browser/payments/service_worker_payment_app_finder_browsertest.cc
@@ -836,6 +836,24 @@
}
}
+// The payment method https://george.example.test/webpay redirects to
+// https://harry.example.test/webpay, which serves a manifest with a relative
+// URL. This test verifies that the relative URL is resolved against the
+// post-redirect URL (harry) and not the pre-redirect URL (george).
+IN_PROC_BROWSER_TEST_F(
+ ServiceWorkerPaymentAppFinderBrowserTest,
+ RedirectResolvesRelativeManifestUrlAgainstPostRedirectUrl) {
+ {
+ GetAllPaymentAppsForMethods({"https://george.example.test/webpay"});
+
+ EXPECT_TRUE(apps().empty());
+ ASSERT_EQ(1U, installable_apps().size());
+ ExpectInstallablePaymentAppInScope("https://harry.example.test/webpay");
+ EXPECT_TRUE(error_message().empty())
+ << "Expected no error, but error message was: " << error_message();
+ }
+}
+
// The payment method https://larry.example.test/webpay is not valid, because of
// its cross-origin service worker scope https://harry.example.test/webpay/".
IN_PROC_BROWSER_TEST_F(ServiceWorkerPaymentAppFinderBrowserTest,
diff --git a/components/test/data/payments/harry.example.test/payment-manifest.json b/components/test/data/payments/harry.example.test/payment-manifest.json
index ad9a26b..7d6560a0 100644
--- a/components/test/data/payments/harry.example.test/payment-manifest.json
+++ b/components/test/data/payments/harry.example.test/payment-manifest.json
@@ -1,3 +1,3 @@
{
- "default_applications": ["https://harry.example.test/app.json"]
-}
+ "default_applications": ["./app.json"]
+}
\ No newline at end of file
Original Bug Report
Payment Method Manifest Default Applications Resolved Against Original Pre-Redirect URL
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: In InstallablePaymentAppCrawler, relative URLs in the default_applications field of a payment method manifest are incorrectly resolved against the original merchant-supplied URL instead of the post-redirect URL where the manifest was actually fetched. This violates W3C specifications and allows a malicious merchant to cause the browser to install an unintended, same-origin payment handler, potentially enabling version downgrade attacks.
Affected files:
components/payments/content/installable_payment_app_crawler.cccomponents/payments/content/utility/payment_manifest_parser.cc
Estimated timestamp from git blame: 2020-04-15
Summary
A logic error in InstallablePaymentAppCrawler causes relative URLs in the default_applications field of a payment method manifest to be resolved against the original, pre-redirect URL provided by the merchant instead of the manifest’s actual location. This violates the W3C Payment Method Manifest spec and enables a same-origin path confusion attack.
Technical Details
When InstallablePaymentAppCrawler crawls a merchant-supplied payment method URL, it uses PaymentManifestDownloader to follow up to three same-site HTTP redirects and locate the manifest via the Link: rel=payment-method-manifest header.
In components/payments/content/installable_payment_app_crawler.cc:145-146, the code calls parser_->ParsePaymentMethodManifest using method_manifest_url (the original merchant-supplied URL) as the base URL for resolution:
145: parser_->ParsePaymentMethodManifest(
146: method_manifest_url, content,
Inside PaymentManifestParser::ParsePaymentMethodManifestIntoVectors -> ParseDefaultApplications (in components/payments/content/utility/payment_manifest_parser.cc:109), relative entries in the default_applications list are then resolved against this incorrect base URL:
109: GURL url = manifest_url.Resolve(*item);
While a subsequent check at installable_payment_app_crawler.cc:190-200 ensures the resolved URL is same-origin with method_manifest_url_after_redirects, it does not prevent path confusion within that origin. This allows a malicious merchant to control the path used for resolving the web app manifest, provided the target is on the same origin as the legitimate payment manifest.
Impact
This flaw allows for version confusion or denial of service (DoS) for just-in-time (JIT) payment handler installation. For instance, if a provider has an old, vulnerable payment handler hosted at a relative path on the same origin, a malicious merchant can cause the browser to install it instead of the secure, updated version.
Potential Attack Steps
Note: These steps are theoretical as our tooling agent does not run code to provide a working PoC.
- Provider Setup: A legitimate payment provider has an old endpoint
https://bank.test/v1/paythat redirects to a new endpointhttps://bank.test/v2/pay. - Provider Manifests: The provider hosts a secure payment handler web app manifest at
https://bank.test/v2/app.jsonand has an older, vulnerable version athttps://bank.test/v1/app.json. - Link Header Emission: A request to the new endpoint
https://bank.test/v2/payreturns an HTTP response containing aLinkheader that points to the manifest:Link: <manifest.json>; rel="payment-method-manifest". - Manifest Content: The manifest located at
https://bank.test/v2/manifest.jsoncontains a relative URL in itsdefault_applicationslist:{"default_applications": ["app.json"]}. - Malicious Merchant Execution: An attacker sets up a malicious merchant website that explicitly calls
new PaymentRequest([{supportedMethods: "https://bank.test/v1/pay"}], ...).show(). - Incorrect Resolution: The browser follows the redirect to
v2and downloadsmanifest.json. However, it incorrectly resolves the relative URLapp.jsonagainst the original URLhttps://bank.test/v1/pay. - Exploit Realized: The browser fetches and installs the vulnerable payment handler from
https://bank.test/v1/app.jsoninstead of the secure one athttps://bank.test/v2/app.json.
Suggested Fix
In components/payments/content/installable_payment_app_crawler.cc:145-146, pass method_manifest_url_after_redirects as the base URL to ParsePaymentMethodManifest instead of method_manifest_url.
parser_->ParsePaymentMethodManifest(
method_manifest_url_after_redirects, content,
base::BindOnce(
&InstallablePaymentAppCrawler::OnPaymentMethodManifestParsed,
weak_ptr_factory_.GetWeakPtr(), method_manifest_url,
method_manifest_url_after_redirects, content));
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.