Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Payments
DescriptionInappropriate implementation in Payments
ComponentPayments
Bug ClassLogic Error
Tracker497610654
Fix commitaa2bdb25afbe (chromium/src) +21/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • chrome/browser/payments/service_worker_payment_app_finder_browsertest.cc
  • components/payments/content/installable_payment_app_crawler.cc
  • components/test/data/payments/harry.example.test/payment-manifest.json
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • components/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.

  1. Provider Setup: A legitimate payment provider has an old endpoint https://bank.test/v1/pay that redirects to a new endpoint https://bank.test/v2/pay.
  2. Provider Manifests: The provider hosts a secure payment handler web app manifest at https://bank.test/v2/app.json and has an older, vulnerable version at https://bank.test/v1/app.json.
  3. Link Header Emission: A request to the new endpoint https://bank.test/v2/pay returns an HTTP response containing a Link header that points to the manifest: Link: <manifest.json>; rel="payment-method-manifest".
  4. Manifest Content: The manifest located at https://bank.test/v2/manifest.json contains a relative URL in its default_applications list: {"default_applications": ["app.json"]}.
  5. Malicious Merchant Execution: An attacker sets up a malicious merchant website that explicitly calls new PaymentRequest([{supportedMethods: "https://bank.test/v1/pay"}], ...).show().
  6. Incorrect Resolution: The browser follows the redirect to v2 and downloads manifest.json. However, it incorrectly resolves the relative URL app.json against the original URL https://bank.test/v1/pay.
  7. Exploit Realized: The browser fetches and installs the vulnerable payment handler from https://bank.test/v1/app.json instead of the secure one at https://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.

View on issue tracker