Chrome · WebAppInstalls › WebInstall
CVE-2026-106179
Spoofing in WebAppInstalls (WebInstall)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/web_applications/web_install_service_impl.cc |
modified | |
TEST_Fchrome/browser/web_applications/web_install_service_impl_unittest.cc |
modified |
Files Changed
chrome/browser/web_applications/web_install_service_impl.ccchrome/browser/web_applications/web_install_service_impl_unittest.cc
Patch
From 5dde31be86a2eeddcce0e063f8dad3e42b2aba53 Mon Sep 17 00:00:00 2001
From: Adriana Ixba <aixba@chromium.org>
Date: Fri, 11 Sep 2026 11:43:09 -0700
Subject: [PATCH] [WebInstall] Track initiating page lifecycle in ReserveInstallInProgress
Centralize the initiating page snapshot inside ReserveInstallInProgress
so all Web Install operations consistently track page liveness. This
ensures ongoing install and launch flows abort cleanly if the initiating
page navigates or enters BFCache, preventing cross-origin UI
misattribution.
Bug: 553250818
Change-Id: I0cbf49b01d4c74c523cb15eec84f307453e990f3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8394157
Reviewed-by: Marijn Kruisselbrink <mek@chromium.org>
Commit-Queue: Adriana Ixba <aixba@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1696475}
---
diff --git a/chrome/browser/web_applications/web_install_service_impl.cc b/chrome/browser/web_applications/web_install_service_impl.cc
index e134a62..d2a0365 100644
--- a/chrome/browser/web_applications/web_install_service_impl.cc
+++ b/chrome/browser/web_applications/web_install_service_impl.cc
@@ -409,11 +409,6 @@
}
base::ScopedClosureRunner install_guard = ReserveInstallInProgress();
- // Snapshot the Page for the install command, which can outlive the document
- // service, and needs to detect if the page navigated away at any point.
- initiating_page_ = render_frame_host().GetPage().GetWeakPtr();
- initiating_page_changed_during_install_ = false;
-
// Available regardless of fetch/parse, so it is recorded on every exit path.
ukm::SourceId requesting_page_source_id =
render_frame_host().GetPageUkmSourceId();
@@ -734,6 +729,15 @@
return;
}
+ // The initiating page changed during manifest check - this intentionally
+ // comes after the id checks above to preserve DataError behavior.
+ if (IsInitiatingPageGoneOrChanged()) {
+ std::move(callback_with_metrics)
+ .Run(web_app::WebInstallServiceResult::kUnexpectedFailure,
+ blink::mojom::WebInstallServiceResult::kAbortError);
+ return;
+ }
+
// Manifest was successfully parsed and meets all web install requirements.
// Check if web app installs are supported in this profile (fails for
// Incognito/Guest). This check intentionally comes after manifest parse so
@@ -818,6 +822,13 @@
auto* provider = WebAppProvider::GetForWebContents(web_contents);
CHECK(provider);
+ if (IsInitiatingPageGoneOrChanged()) {
+ std::move(callback_with_metrics)
+ .Run(web_app::WebInstallServiceResult::kUnexpectedFailure,
+ blink::mojom::WebInstallServiceResult::kAbortError);
+ return;
+ }
+
// Now that we know the app is already installed, show the intent picker
// scoped to the resolved app so nested-scope URLs don't reintroduce ambiguity
// between the parent and child apps that both control the current document.
@@ -833,9 +844,9 @@
InstallFromManifestCallbackWithMetrics callback_with_metrics,
webapps::AppId app_id,
bool user_chose_to_open) {
- // If the user chose to open the app in the intent picker, return success.
- // Otherwise, return an abort error.
- if (user_chose_to_open) {
+ // If the user chose to open the app in the intent picker and the page is
+ // still live, return success. Otherwise, return an abort error.
+ if (user_chose_to_open && !IsInitiatingPageGoneOrChanged()) {
OnAppInstalledFromManifest(
std::move(callback_with_metrics), app_id,
webapps::InstallResultCode::kSuccessAlreadyInstalled);
@@ -1261,6 +1272,10 @@
base::ScopedClosureRunner WebInstallServiceImpl::ReserveInstallInProgress() {
install_in_progress_ = true;
+ // Snapshot the Page for the install operation, which can outlive the document
+ // service or be BFCached, and needs to detect if the page navigated away.
+ initiating_page_ = render_frame_host().GetPage().GetWeakPtr();
+ initiating_page_changed_during_install_ = false;
return base::ScopedClosureRunner(
base::BindOnce(&WebInstallServiceImpl::ReleaseInstallInProgress,
weak_ptr_factory_.GetWeakPtr()));
diff --git a/chrome/browser/web_applications/web_install_service_impl_unittest.cc b/chrome/browser/web_applications/web_install_service_impl_unittest.cc
index d4c17052..1c5bddc1 100644
--- a/chrome/browser/web_applications/web_install_service_impl_unittest.cc
+++ b/chrome/browser/web_applications/web_install_service_impl_unittest.cc
@@ -1118,5 +1118,28 @@
1);
}
+// Navigating away before the install completes aborts the install with
+// kAbortError and records kUnexpectedFailure.
+TEST_F(WebInstallServiceImplTest, CurrentDocument_PageNavigated_Aborts) {
+ base::HistogramTester histograms;
+ GURL custom_id("https://requesting-app.com/my_app_id");
+ auto manifest = CreateManifest(GURL(kDocumentUrl), custom_id);
+ SetupPageWithManifest(GURL(kDocumentUrl), std::move(manifest));
+
+ BindService();
+
+ // Navigate away so the bound RenderFrameHost / page is no longer primary.
+ NavigateAndCommit(GURL("https://different-site.com/"));
+
+ blink::mojom::WebInstallServiceResult result =
+ InstallFromApiCurrentDocument();
+
+ EXPECT_EQ(result, blink::mojom::WebInstallServiceResult::kAbortError);
+ histograms.ExpectBucketCount(kInstallApiResultUma,
+ WebInstallServiceResult::kUnexpectedFailure, 1);
+ histograms.ExpectBucketCount(kVariantedInstallResultUma,
+ WebInstallServiceResult::kUnexpectedFailure, 1);
+}
+
} // namespace
} // namespace web_app
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/web_applications/web_install_service_impl_unittest.cc b/chrome/browser/web_applications/web_install_service_impl_unittest.cc
index d4c17052..1c5bddc1 100644
--- a/chrome/browser/web_applications/web_install_service_impl_unittest.cc
+++ b/chrome/browser/web_applications/web_install_service_impl_unittest.cc
@@ -1118,5 +1118,28 @@
1);
}
+// Navigating away before the install completes aborts the install with
+// kAbortError and records kUnexpectedFailure.
+TEST_F(WebInstallServiceImplTest, CurrentDocument_PageNavigated_Aborts) {
+ base::HistogramTester histograms;
+ GURL custom_id("https://requesting-app.com/my_app_id");
+ auto manifest = CreateManifest(GURL(kDocumentUrl), custom_id);
+ SetupPageWithManifest(GURL(kDocumentUrl), std::move(manifest));
+
+ BindService();
+
+ // Navigate away so the bound RenderFrameHost / page is no longer primary.
+ NavigateAndCommit(GURL("https://different-site.com/"));
+
+ blink::mojom::WebInstallServiceResult result =
+ InstallFromApiCurrentDocument();
+
+ EXPECT_EQ(result, blink::mojom::WebInstallServiceResult::kAbortError);
+ histograms.ExpectBucketCount(kInstallApiResultUma,
+ WebInstallServiceResult::kUnexpectedFailure, 1);
+ histograms.ExpectBucketCount(kVariantedInstallResultUma,
+ WebInstallServiceResult::kUnexpectedFailure, 1);
+}
+
} // namespace
} // namespace web_app
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page