CVE-2026-8019
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/browser/web_applications/web_contents/web_app_data_retriever_unittest.cc |
modified | |
TEST_Fcomponents/webapps/browser/android/shortcut_info_unittest.cc |
modified |
Files Changed
chrome/browser/web_applications/commands/fetch_manifest_and_install_command.ccchrome/browser/web_applications/web_contents/web_app_data_retriever.ccchrome/browser/web_applications/web_contents/web_app_data_retriever_unittest.cccomponents/webapps/browser/android/shortcut_info.cccomponents/webapps/browser/android/shortcut_info_unittest.cc
Patch
From c61b9844f19794ea11031449ddc7f0065d3200af Mon Sep 17 00:00:00 2001 From: Dibyajyoti Pal <[email protected]> Date: Fri, 03 Apr 2026 14:33:51 -0700 Subject: [PATCH] [PWA] Enforce same origin constraints for application url metadata In the event of a PWA installation needing to populate the start_url from the application url obtained from the page metadata, this CL enforces strict same origin guarantees, comparing to the last committed url of the web contents. A change has also been made in the user installation flow, to reuse the validated install_info parsed from the manifest while installing an app with generated icons, to prioritize manifest fields that are validated. Fixed: 498353173 Change-Id: I44dc983ab3e21d780cef6729236f84a582f61cf7 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7728736 Commit-Queue: Dibyajyoti Pal <[email protected]> Reviewed-by: Daniel Murphy <[email protected]> Cr-Commit-Position: refs/heads/main@{#1609957} --- diff --git a/chrome/browser/web_applications/commands/fetch_manifest_and_install_command.cc b/chrome/browser/web_applications/commands/fetch_manifest_and_install_command.cc index edd0fc1..c56da847 100644 --- a/chrome/browser/web_applications/commands/fetch_manifest_and_install_command.cc +++ b/chrome/browser/web_applications/commands/fetch_manifest_and_install_command.cc @@ -583,7 +583,10 @@ GetMutableDebugValue().Set("used_fallback_after_icon_download_failed", true); valid_manifest_for_crafted_web_app_ = false; + + web_app_info_ = std::move(install_info); web_app_info_->is_diy_app = true; + data_retriever_->GetIcons( web_contents_.get(), {}, /*download_page_favicons=*/true, diff --git a/chrome/browser/web_applications/web_contents/web_app_data_retriever.cc b/chrome/browser/web_applications/web_contents/web_app_data_retriever.cc index bfad9c8..2989237a 100644 --- a/chrome/browser/web_applications/web_contents/web_app_data_retriever.cc +++ b/chrome/browser/web_applications/web_contents/web_app_data_retriever.cc @@ -317,6 +317,16 @@ } CHECK(metadata); + // Ensure that the metadata's application URL is same origin as the page, to + // prevent a compromised renderer from installing cross-origin apps. Setting + // this to an empty GURL allows a fallback to the `start_url`, which is + // computed from the web contents itself. + if (metadata->application_url.is_valid() && + !url::IsSameOriginWith(metadata->application_url, + contents->GetLastCommittedURL())) { + metadata->application_url = GURL(); + } + std::unique_ptr<WebAppInstallInfo> info = std::move(fallback_install_info_); PopulateWebAppInfoFromMetadata(info.get(), *metadata); std::move(get_web_app_info_callback_).Run(std::move(info)); diff --git a/chrome/browser/web_applications/web_contents/web_app_data_retriever_unittest.cc b/chrome/browser/web_applications/web_contents/web_app_data_retriever_unittest.cc index db59e28a..72c67cb 100644 --- a/chrome/browser/web_applications/web_contents/web_app_data_retriever_unittest.cc +++ b/chrome/browser/web_applications/web_contents/web_app_data_retriever_unittest.cc @@ -187,9 +187,9 @@ TEST_F(WebAppDataRetrieverTest, GetWebAppInstallInfo_AppUrlPresent) { SetFakeWebPageMetadataAgent(); - web_contents_tester()->NavigateAndCommit(GURL("https://foo.example")); - GURL other_app_url = GURL("https://bar.example"); + web_contents_tester()->NavigateAndCommit(other_app_url); + std::u16string other_app_title = u"Other App Title"; SetRendererWebPageMetadata(other_app_url, other_app_title, /*description=*/u""); @@ -206,6 +206,30 @@ EXPECT_EQ(other_app_title, web_app_info()->title.value()); } +TEST_F(WebAppDataRetrieverTest, GetWebAppInstallInfo_AppUrlCrossOrigin) { + SetFakeWebPageMetadataAgent(); + + const GURL kFooUrl("https://foo.example"); + web_contents_tester()->NavigateAndCommit(kFooUrl); + + GURL other_app_url = GURL("https://bar.example"); + std::u16string other_app_title = u"Other App Title"; + SetRendererWebPageMetadata(other_app_url, other_app_title, + /*description=*/u""); + + base::RunLoop run_loop; + WebAppDataRetriever retriever; + retriever.GetWebAppInstallInfo( + web_contents(), + base::BindOnce(&WebAppDataRetrieverTest::GetWebAppInstallInfoCallback, + base::Unretained(this), run_loop.QuitClosure())); + run_loop.Run(); + + // If the origin differs, we fallback to the url that was already in the + // web_app_info(). + EXPECT_EQ(kFooUrl, web_app_info()->start_url()); +} + TEST_F(WebAppDataRetrieverTest, GetWebAppInstallInfo_TitleAbsentFromRenderer) { SetFakeWebPageMetadataAgent(); diff --git a/components/webapps/browser/android/shortcut_info.cc b/components/webapps/browser/android/shortcut_info.cc index 705a6d1..cd255d7a 100644 --- a/components/webapps/browser/android/shortcut_info.cc +++ b/components/webapps/browser/android/shortcut_info.cc @@ -135,7 +135,8 @@ if (!metadata.description.empty()) { description = metadata.description; } - if (metadata.application_url.is_valid()) { + if (metadata.application_url.is_valid() && + url::IsSameOriginWith(metadata.application_url, url)) { url = metadata.application_url; scope = metadata.application_url.GetWithoutFilename(); } diff --git a/components/webapps/browser/android/shortcut_info_unittest.cc b/components/webapps/browser/android/shortcut_info_unittest.cc index fb4dc05..37f0379 100644 --- a/components/webapps/browser/android/shortcut_info_unittest.cc +++ b/components/webapps/browser/android/shortcut_info_unittest.cc @@ -147,12 +147,13 @@ } TEST_F(ShortcutInfoTest, UpdateFromWebPageMetadata) { - info_ = ShortcutInfo(GURL()); + const GURL kAppUrl("https://new.com/start"); + info_ = ShortcutInfo(kAppUrl); webapps::mojom::WebPageMetadataPtr metadata = webapps::mojom::WebPageMetadata::New(); metadata->application_name = u"new title"; metadata->description = u"new description"; - metadata->application_url = GURL("https://new.com/start"); + metadata->application_url = kAppUrl; metadata->mobile_capable = mojom::WebPageMobileCapable::ENABLED; info_.UpdateFromWebPageMetadata(*metadata); @@ -166,6 +167,19 @@ ASSERT_EQ(blink::mojom::DisplayMode::kStandalone, info_.display); } +TEST_F(ShortcutInfoTest, UpdateFromWebPageMetadataCrossOrigin) { + const GURL kInitialUrl("https://old.com/start"); + info_ = ShortcutInfo(kInitialUrl); + webapps::mojom::WebPageMetadataPtr metadata = + webapps::mojom::WebPageMetadata::New(); + metadata->application_url = GURL("https://new.com/start"); + + info_.UpdateFromWebPageMetadata(*metadata); + + // URL should not change if cross-origin + ASSERT_EQ(kInitialUrl, info_.url); +} + TEST_F(ShortcutInfoTest, WebPageMetadataTitleAppName) { info_ = ShortcutInfo(GURL()); webapps::mojom::WebPageMetadataPtr metadata =
Regression Test / PoC
diff --git a/chrome/browser/web_applications/web_contents/web_app_data_retriever_unittest.cc b/chrome/browser/web_applications/web_contents/web_app_data_retriever_unittest.cc
index db59e28a..72c67cb 100644
--- a/chrome/browser/web_applications/web_contents/web_app_data_retriever_unittest.cc
+++ b/chrome/browser/web_applications/web_contents/web_app_data_retriever_unittest.cc
@@ -187,9 +187,9 @@
TEST_F(WebAppDataRetrieverTest, GetWebAppInstallInfo_AppUrlPresent) {
SetFakeWebPageMetadataAgent();
- web_contents_tester()->NavigateAndCommit(GURL("https://foo.example"));
-
GURL other_app_url = GURL("https://bar.example");
+ web_contents_tester()->NavigateAndCommit(other_app_url);
+
std::u16string other_app_title = u"Other App Title";
SetRendererWebPageMetadata(other_app_url, other_app_title,
/*description=*/u"");
@@ -206,6 +206,30 @@
EXPECT_EQ(other_app_title, web_app_info()->title.value());
}
+TEST_F(WebAppDataRetrieverTest, GetWebAppInstallInfo_AppUrlCrossOrigin) {
+ SetFakeWebPageMetadataAgent();
+
+ const GURL kFooUrl("https://foo.example");
+ web_contents_tester()->NavigateAndCommit(kFooUrl);
+
+ GURL other_app_url = GURL("https://bar.example");
+ std::u16string other_app_title = u"Other App Title";
+ SetRendererWebPageMetadata(other_app_url, other_app_title,
+ /*description=*/u"");
+
+ base::RunLoop run_loop;
+ WebAppDataRetriever retriever;
+ retriever.GetWebAppInstallInfo(
+ web_contents(),
+ base::BindOnce(&WebAppDataRetrieverTest::GetWebAppInstallInfoCallback,
+ base::Unretained(this), run_loop.QuitClosure()));
+ run_loop.Run();
+
+ // If the origin differs, we fallback to the url that was already in the
+ // web_app_info().
+ EXPECT_EQ(kFooUrl, web_app_info()->start_url());
+}
+
TEST_F(WebAppDataRetrieverTest, GetWebAppInstallInfo_TitleAbsentFromRenderer) {
SetFakeWebPageMetadataAgent();
diff --git a/components/webapps/browser/android/shortcut_info_unittest.cc b/components/webapps/browser/android/shortcut_info_unittest.cc
index fb4dc05..37f0379 100644
--- a/components/webapps/browser/android/shortcut_info_unittest.cc
+++ b/components/webapps/browser/android/shortcut_info_unittest.cc
@@ -147,12 +147,13 @@
}
TEST_F(ShortcutInfoTest, UpdateFromWebPageMetadata) {
- info_ = ShortcutInfo(GURL());
+ const GURL kAppUrl("https://new.com/start");
+ info_ = ShortcutInfo(kAppUrl);
webapps::mojom::WebPageMetadataPtr metadata =
webapps::mojom::WebPageMetadata::New();
metadata->application_name = u"new title";
metadata->description = u"new description";
- metadata->application_url = GURL("https://new.com/start");
+ metadata->application_url = kAppUrl;
metadata->mobile_capable = mojom::WebPageMobileCapable::ENABLED;
info_.UpdateFromWebPageMetadata(*metadata);
@@ -166,6 +167,19 @@
ASSERT_EQ(blink::mojom::DisplayMode::kStandalone, info_.display);
}
+TEST_F(ShortcutInfoTest, UpdateFromWebPageMetadataCrossOrigin) {
+ const GURL kInitialUrl("https://old.com/start");
+ info_ = ShortcutInfo(kInitialUrl);
+ webapps::mojom::WebPageMetadataPtr metadata =
+ webapps::mojom::WebPageMetadata::New();
+ metadata->application_url = GURL("https://new.com/start");
+
+ info_.UpdateFromWebPageMetadata(*metadata);
+
+ // URL should not change if cross-origin
+ ASSERT_EQ(kInitialUrl, info_.url);
+}
+
TEST_F(ShortcutInfoTest, WebPageMetadataTitleAppName) {
info_ = ShortcutInfo(GURL());
webapps::mojom::WebPageMetadataPtr metadata =
Original Bug Report
Potential cross-origin PWA identity spoofing via metadata and icon fallback bypass
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: A logic flaw in PWA installation allows a malicious site to supply a cross-origin application-url via page metadata. By intentionally failing a manifest icon download, an attacker can bypass manifest validation and install or overwrite a PWA using the victim’s identity but attacker-controlled icons and names. This also bypasses AppLock integrity checks and affects Android shortcuts.
Affected files:
chrome/browser/web_applications/web_contents/web_app_data_retriever.ccchrome/browser/web_applications/commands/fetch_manifest_and_install_command.cccomponents/webapps/browser/android/shortcut_info.cc
Estimated timestamp from git blame: 2025-07-10
Description
A potential vulnerability exists in the Progressive Web App (PWA) installation flow that allows a malicious renderer to spoof the identity of an installed app or overwrite an existing victim’s app with attacker-controlled icons and titles. This requires user interaction (accepting the install prompt) but results in persistent UI spoofing.
The vulnerability is a chain of three distinct issues:
1. Unvalidated Origin in Page Metadata
During the initial phase of PWA installation, WebAppDataRetriever::GetWebAppInstallInfo requests page metadata from the renderer. In WebAppDataRetriever::PopulateWebAppInfoFromMetadata(), the metadata.application_url is blindly trusted and used to set the fallback start_url and manifest_id for the app. There is no check to ensure this URL is same-origin with the document’s last committed URL.
2. Logic Error in Manifest Fallback
FetchManifestAndInstallCommand receives this tainted fallback metadata and stores it in web_app_info_. It then fetches the attacker’s actual manifest (which passes validation) and acquires an AppLock for the attacker’s true App ID.
However, if the manifest’s icons fail to download (e.g., return a 404), the ManifestToWebAppInstallInfoJob flags install_info->is_generated_icon = true. In the callback OnInstallInfoObtainedMergeAndShowDialog (around line 580), this triggers an early return to fall back to page favicons. Critically, this early return skips the assignment web_app_info_ = std::move(install_info); (line 598). As a result, the command retains the tainted fallback metadata (the victim’s ID) instead of the validated manifest data.
3. AppLock Integrity Bypass
When the user accepts the prompt, FinalizeInstallJob writes the app to the WebAppRegistry using the tainted web_app_info_->manifest_id() (the victim’s ID). Because the system does not enforce that the modified App ID matches the currently held AppLock (which is for the attacker’s ID), the write succeeds. This allows an attacker to silently overwrite an existing victim’s app name and OS launcher icon.
Android Impact
A similar lack of validation exists in Android’s components/webapps/browser/android/shortcut_info.cc. ShortcutInfo::UpdateFromMetadata blindly trusts metadata.application_url to set the shortcut’s url and scope, enabling Android homescreen shortcut spoofing.
Potential Reproduction Steps
(Note: These are suggested steps based on static code analysis; we do not have a working exploit to automatically verify this.)
- Host an attacker page at
https://attacker.example. Include the following meta tag to spoof the origin:<meta name="application-url" content="https://victim.example/">. - Set the
<title>and<link rel="icon">(favicons) on the page to attacker-controlled values (e.g., “Victim Bank” and a fake bank logo). - Host a manifest at
https://attacker.example/manifest.jsonthat passeskValidManifestIgnoreDisplaybut includes aniconsarray pointing to a URL that returns a 404 Not Found error. - Have the user navigate to
https://attacker.exampleand trigger the PWA install (e.g., via the omnibox icon or Chrome menu). - The user will see an install dialog with the attacker’s title and favicon. If they accept, the app is installed with the
manifest_idofhttps://victim.example/, potentially overwriting the legitimate app if it was already installed.
Suggested Fix
- Validate Metadata Origin: In
WebAppDataRetriever::PopulateWebAppInfoFromMetadata, verify thatmetadata.application_urlis same-origin with theWebContents’ last committed URL before trusting it. - Fix Command Logic: In
FetchManifestAndInstallCommand::OnInstallInfoObtainedMergeAndShowDialog, ensure thatweb_app_info_is updated with the validatedinstall_infodata before returning early to fetch favicons, or restructure the fallback logic so that validated manifest fields are always prioritized and retained. - Enforce AppLock Integrity:
ScopedRegistryUpdateorFinalizeInstallJobshould verify that the App ID being written to the registry corresponds to theAppLockcurrently held by the command. - Patch Android ShortcutInfo: Add origin validation to
ShortcutInfo::UpdateFromMetadataincomponents/webapps/browser/android/shortcut_info.cc.
Evaluated with Chrome root at commit: e9e0fcbb690b1a8c1a26c81c2a9ea23d6e178368
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.