Chrome · WebAPKs › PWA
CVE-2026-106221
Authorization Error in WebAPKs (PWA)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/android/shortcut_helper.cc |
modified | |
ifchrome/browser/android/webapk/webapk_install_service.cc |
modified |
Files Changed
chrome/browser/android/webapk/webapk_installer_browsertest.ccchrome/browser/android/webapk/webapk_install_service.ccchrome/browser/android/webapk/webapk_install_service.hchrome/browser/android/webapk/BUILD.gnchrome/browser/android/shortcut_helper.cc
Patch
From 2eda634a59fa3b0aab224a38bb990257251e32b6 Mon Sep 17 00:00:00 2001
From: lukedeng <lukedeng@chromium.org>
Date: Mon, 14 Sep 2026 12:52:54 -0700
Subject: [PATCH] [PWA] Prevent cross-origin splash fetch on WebAPK install fallback
When a WebAPK installation fails, WebApkInstallService falls back to
adding a home screen shortcut. If the initiating tab navigated away to
a different (victim) origin while WebAPK minting was in flight,
ShortcutHelper::AddToLauncherWithSkBitmap previously executed using the
navigated tab context.
This caused ManifestIconDownloader::Download to be called without an
initiator frame ID, resolving to the active primary main frame of the
victim origin in WebContentsImpl::DownloadImageInFrame. The splash image
was then fetched with the victim origin's credentials and SameSite
cookies attached, allowing a blind CSRF / SameSite cookie bypass.
To fix this while maintaining expected shortcut installation behavior:
- In WebApkInstallService::OnFinishedInstall: pass web_contents to ShortcutHelper::AddToLauncherWithSkBitmap only if still same-origin (or nullptr if navigated away or destroyed). Also document the AppBannerManager invariant with CHECK(app_banner_manager) and only notify AppBannerManager or launch notifications if still same-origin.
- In ShortcutHelper::AddToLauncherWithSkBitmap: sanitize web_contents against cross-origin navigation as defense-in-depth. If web_contents has navigated away from the shortcut's origin, the pointer is sanitized to nullptr for splash downloading and UKM recording, preventing cross-origin fetches while still executing Java_ShortcutHelper_addWebapp to create the home screen shortcut using the snapshot data.
- In ShortcutHelper::AddWebappWithSkBitmap: handle nullptr web_contents safely without initiating splash image download. When web_contents is present, explicitly pass the initiator frame's GlobalRenderFrameHostId to ManifestIconDownloader::Download to pin the fetch to that frame.
- Add browser tests in webapk_installer_browsertest.cc covering ShortcutHelper::AddToLauncherWithSkBitmap and WebApkInstallService::OnFinishedInstall with nullptr, cross-origin, and same-origin WebContents, verifying both UKM recording behavior and that no splash image fetch is dispatched to the victim origin.
TAG=agy
CONV=0abcc8bf-ed69-44f0-9498-2d7ea473bdc2
Bug: 540049672
Change-Id: I1178fcd724da58e7b276dcd230e0f21dbe0406ea
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8394417
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Commit-Queue: Luke Deng <lukedeng@google.com>
Reviewed-by: Yaron Friedman <yfriedman@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1697454}
---
diff --git a/chrome/browser/android/shortcut_helper.cc b/chrome/browser/android/shortcut_helper.cc
index 999e5db..97670c3d 100644
--- a/chrome/browser/android/shortcut_helper.cc
+++ b/chrome/browser/android/shortcut_helper.cc
@@ -5,6 +5,7 @@
#include "chrome/browser/android/shortcut_helper.h"
#include <jni.h>
+
#include <limits>
#include <string>
#include <utility>
@@ -16,6 +17,7 @@
#include "base/uuid.h"
#include "components/webapps/browser/android/shortcut_info.h"
#include "content/public/browser/manifest_icon_downloader.h"
+#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "ui/android/color_utils_android.h"
@@ -50,12 +52,18 @@
ui::OptionalSkColorToJavaColor(info.theme_color),
ui::OptionalSkColorToJavaColor(info.background_color));
+ if (!web_contents) {
+ return;
+ }
+
// Start downloading the splash image in parallel with the app install.
content::ManifestIconDownloader::Download(
web_contents, info.splash_image_url, info.ideal_splash_image_size_in_px,
info.minimum_splash_image_size_in_px,
/* maximum_icon_size_in_px= */ std::numeric_limits<int>::max(),
- base::BindOnce(&ShortcutHelper::StoreWebappSplashImage, webapp_id));
+ base::BindOnce(&ShortcutHelper::StoreWebappSplashImage, webapp_id),
+ /* square_only= */ true,
+ web_contents->GetPrimaryMainFrame()->GetGlobalId());
}
// Adds a shortcut which opens in a browser tab to the launcher.
@@ -96,13 +104,21 @@
const webapps::ShortcutInfo& info,
const SkBitmap& icon_bitmap,
webapps::InstallableStatusCode installable_status) {
- RecordAddToHomeScreenUKM(web_contents, info, installable_status);
+ content::WebContents* same_origin_web_contents =
+ (web_contents && web_contents->GetPrimaryMainFrame()
+ ->GetLastCommittedOrigin()
+ .IsSameOriginWith(info.url))
+ ? web_contents
+ : nullptr;
+
+ RecordAddToHomeScreenUKM(same_origin_web_contents, info, installable_status);
std::string webapp_id = base::Uuid::GenerateRandomV4().AsLowercaseString();
if (info.display == blink::mojom::DisplayMode::kStandalone ||
info.display == blink::mojom::DisplayMode::kFullscreen ||
info.display == blink::mojom::DisplayMode::kMinimalUi) {
- AddWebappWithSkBitmap(web_contents, info, webapp_id, icon_bitmap);
+ AddWebappWithSkBitmap(same_origin_web_contents, info, webapp_id,
+ icon_bitmap);
return;
}
AddShortcutWithSkBitmap(info, webapp_id, icon_bitmap);
diff --git a/chrome/browser/android/webapk/webapk_install_service.cc b/chrome/browser/android/webapk/webapk_install_service.cc
index 6e177be..d551df48 100644
--- a/chrome/browser/android/webapk/webapk_install_service.cc
+++ b/chrome/browser/android/webapk/webapk_install_service.cc
@@ -24,8 +24,10 @@
#include "components/webapps/browser/banners/app_banner_manager.h"
#include "components/webapps/browser/features.h"
#include "components/webapps/browser/installable/installable_logging.h"
+#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "ui/gfx/android/java_bitmap.h"
+#include "url/origin.h"
WebApkInstallService::WebApkInstallService(
content::BrowserContext* browser_context)
@@ -112,19 +114,26 @@
const std::string& webapk_package_name) {
install_ids_.erase(shortcut_info.manifest_id);
+ bool is_same_origin =
+ web_contents && web_contents->GetPrimaryMainFrame()
+ ->GetLastCommittedOrigin()
+ .IsSameOriginWith(shortcut_info.url);
+
bool show_failure_notification = base::FeatureList::IsEnabled(
webapps::features::kWebApkInstallFailureNotification);
HandleFinishInstallNotificationsAndMaybeLaunch(
- web_contents.get(), shortcut_info.manifest_id, shortcut_info.url,
- shortcut_info.short_name, primary_icon,
+ is_same_origin ? web_contents.get() : nullptr, shortcut_info.manifest_id,
+ shortcut_info.url, shortcut_info.short_name, primary_icon,
shortcut_info.is_primary_icon_maskable, result, webapk_package_name,
show_failure_notification);
// If the app was successfully installed, we need to notify the app banner
// manager so that the installability status is reflected elsewhere in the UI.
- if (result == webapps::WebApkInstallResult::SUCCESS && web_contents) {
- webapps::AppBannerManager::FromWebContents(web_contents.get())
- ->OnInstall(shortcut_info.display, true);
+ if (result == webapps::WebApkInstallResult::SUCCESS && is_same_origin) {
+ auto* app_banner_manager =
+ webapps::AppBannerManager::FromWebContents(web_contents.get());
+ CHECK(app_banner_manager);
+ app_banner_manager->OnInstall(shortcut_info.display, true);
}
if (show_failure_notification) {
@@ -137,13 +146,14 @@
// this one is still queued (and hence might succeed in the future).
if (result != webapps::WebApkInstallResult::SUCCESS &&
result != webapps::WebApkInstallResult::PROBABLE_FAILURE) {
- if (!web_contents)
- return;
-
+ // Pass the WebContents pointer if still on the same origin (or nullptr if
+ // destroyed or navigated away). ShortcutHelper::AddToLauncherWithSkBitmap
+ // will also verify same-origin as defense-in-depth before initiating splash
+ // image download or recording UKM.
// TODO(crbug.com/40584062): Support maskable icons here.
ShortcutHelper::AddToLauncherWithSkBitmap(
- web_contents.get(), shortcut_info, primary_icon,
- webapps::InstallableStatusCode::WEBAPK_INSTALL_FAILED);
+ is_same_origin ? web_contents.get() : nullptr, shortcut_info,
+ primary_icon, webapps::InstallableStatusCode::WEBAPK_INSTALL_FAILED);
}
}
diff --git a/chrome/browser/android/webapk/webapk_install_service.h b/chrome/browser/android/webapk/webapk_install_service.h
index 56147479..2a82df0 100644
--- a/chrome/browser/android/webapk/webapk_install_service.h
+++ b/chrome/browser/android/webapk/webapk_install_service.h
@@ -139,6 +139,8 @@
bool is_primary_icon_maskable,
webapps::WebApkInstallResult result);
+ friend class WebApkInstallerBrowserTest;
+
raw_ptr<content::BrowserContext> browser_context_;
// In progress installs's id.
diff --git a/chrome/browser/android/webapk/webapk_installer_browsertest.cc b/chrome/browser/android/webapk/webapk_installer_browsertest.cc
index ea26c2b..758a9e8 100644
--- a/chrome/browser/android/webapk/webapk_installer_browsertest.cc
+++ b/chrome/browser/android/webapk/webapk_installer_browsertest.cc
@@ -4,6 +4,7 @@
#include "chrome/browser/android/webapk/webapk_installer.h"
+#include <atomic>
#include <memory>
#include <utility>
@@ -14,7 +15,9 @@
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/thread_restrictions.h"
+#include "chrome/browser/android/shortcut_helper.h"
#include "chrome/browser/android/webapk/webapk_install_service.h"
+#include "chrome/browser/android/webapk/webapk_install_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/android/android_browser_test.h"
@@ -27,6 +30,7 @@
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
+#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -233,6 +237,7 @@
~WebApkInstallerBrowserTest() override = default;
void SetUpOnMainThread() override {
+ host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->AddDefaultHandlers(base::FilePath(kTestDataDir));
embedded_test_server()->RegisterRequestHandler(
base::BindRepeating(&WebApkInstallerBrowserTest::HandleWebApkRequest,
@@ -311,6 +316,30 @@
return Profile::FromBrowserContext(web_contents()->GetBrowserContext());
}
+ webapps::ShortcutInfo CreateValidShortcutInfo(const GURL& url) {
+ webapps::ShortcutInfo info(url);
+ info.manifest_id = url;
+ info.user_title = u"Test App";
+ info.name = u"Test App";
+ info.short_name = u"Test";
+ info.display = blink::mojom::DisplayMode::kStandalone;
+ info.splash_image_url = url.Resolve(kBestSplashIconUrl);
+ return info;
+ }
+
+ void CallOnFinishedInstall(WebApkInstallService* service,
+ base::WeakPtr<content::WebContents> web_contents,
+ const webapps::ShortcutInfo& shortcut_info,
+ const SkBitmap& primary_icon,
+ webapps::WebApkInstallResult result) {
+ service->OnFinishedInstall(web_contents, shortcut_info, primary_icon,
+ result,
+ /*relax_updates=*/false,
+ /*webapk_package_name=*/"");
+ }
+
+ int victim_splash_requests() const { return victim_splash_requests_.load(); }
+
private:
// Sets default configuration for running WebApkInstaller.
void SetDefaults() {
@@ -321,11 +350,16 @@
std::unique_ptr<net::test_server::HttpResponse> HandleWebApkRequest(
const net::test_server::HttpRequest& request) {
+ if (request.relative_url.starts_with("/victim_splash.png")) {
+ victim_splash_requests_++;
+ }
return (request.relative_url == kServerUrl)
? webapk_response_builder_.Run()
: std::unique_ptr<net::test_server::HttpResponse>();
}
+ std::atomic<int> victim_splash_requests_{0};
+
// Builds response to the WebAPK creation request.
WebApkResponseBuilder webapk_response_builder_;
};
@@ -537,3 +571,84 @@
// Clean up
base::DeletePathRecursively(outer_file_path);
}
+
+// Tests that ShortcutHelper::AddToLauncherWithSkBitmap sanitizes a cross-origin
+// WebContents and does not record UKM or fetch splash icons for the navigated
+// origin.
+IN_PROC_BROWSER_TEST_F(WebApkInstallerBrowserTest,
+ AddToLauncherSanitizesCrossOriginWebContents) {
+ GURL victim_url = embedded_test_server()->GetURL("victim.com", "/empty.html");
+ ASSERT_TRUE(content::NavigateToURL(web_contents(), victim_url));
+
+ webapps::ShortcutInfo cross_origin_info =
+ CreateValidShortcutInfo(GURL("https://attacker.com/index.html"));
+ // Point the splash image URL to a path on the victim origin to simulate the
+ // confused-deputy attack scenario from b/540049672.
+ cross_origin_info.splash_image_url = victim_url.Resolve("/victim_splash.png");
+
+ ukm::TestAutoSetUkmRecorder ukm_recorder;
+
+ // With a cross-origin WebContents, the pointer is sanitized to nullptr so no
+ // UKM entry is recorded and no splash icon request is sent to the victim.
+ ShortcutHelper::AddToLauncherWithSkBitmap(
+ web_contents(), cross_origin_info,
+ gfx::test::CreateBitmap(1, SK_ColorRED),
+ webapps::InstallableStatusCode::WEBAPK_INSTALL_FAILED);
+ EXPECT_TRUE(ukm_recorder.GetEntries("Webapp.AddToHomeScreen", {}).empty());
+ EXPECT_EQ(0, victim_splash_requests());
+
+ // With a same-origin WebContents, the pointer is retained and UKM is
+ // recorded.
+ webapps::ShortcutInfo same_origin_info = CreateValidShortcutInfo(victim_url);
+ ShortcutHelper::AddToLauncherWithSkBitmap(
+ web_contents(), same_origin_info, gfx::test::CreateBitmap(1, SK_ColorRED),
+ webapps::InstallableStatusCode::WEBAPK_INSTALL_FAILED);
+ EXPECT_EQ(1u, ukm_recorder.GetEntries("Webapp.AddToHomeScreen", {}).size());
+}
+
+// Tests that ShortcutHelper::AddToLauncherWithSkBitmap handles nullptr
+// WebContents safely without crashing or recording UKM.
+IN_PROC_BROWSER_TEST_F(WebApkInstallerBrowserTest,
+ AddToLauncherHandlesNullWebContents) {
+ webapps::ShortcutInfo shortcut_info =
+ CreateValidShortcutInfo(GURL("https://example.com/index.html"));
+
+ ukm::TestAutoSetUkmRecorder ukm_recorder;
+
+ ShortcutHelper::AddToLauncherWithSkBitmap(
+ /*web_contents=*/nullptr, shortcut_info,
+ gfx::test::CreateBitmap(1, SK_ColorRED),
+ webapps::InstallableStatusCode::WEBAPK_INSTALL_FAILED);
+ EXPECT_TRUE(ukm_recorder.GetEntries("Webapp.AddToHomeScreen", {}).empty());
+}
+
+// Tests that WebApkInstallService::OnFinishedInstall on failure triggers
+// shortcut fallback and does not leak a navigated cross-origin WebContents.
+IN_PROC_BROWSER_TEST_F(WebApkInstallerBrowserTest,
+ OnFinishedInstallFallbackSanitizesNavigatedWebContents) {
+ GURL victim_url = embedded_test_server()->GetURL("victim.com", "/empty.html");
+ ASSERT_TRUE(content::NavigateToURL(web_contents(), victim_url));
+
+ webapps::ShortcutInfo cross_origin_info =
+ CreateValidShortcutInfo(GURL("https://attacker.com/index.html"));
+ cross_origin_info.splash_image_url = victim_url.Resolve("/victim_splash.png");
+
+ ukm::TestAutoSetUkmRecorder ukm_recorder;
+
+ auto* install_service =
+ WebApkInstallServiceFactory::GetForBrowserContext(profile());
+ CallOnFinishedInstall(install_service, web_contents()->GetWeakPtr(),
+ cross_origin_info,
+ gfx::test::CreateBitmap(1, SK_ColorRED),
+ webapps::WebApkInstallResult::SERVER_ERROR);
+ EXPECT_TRUE(ukm_recorder.GetEntries("Webapp.AddToHomeScreen", {}).empty());
+ EXPECT_EQ(0, victim_splash_requests());
+
+ // With a same-origin WebContents, UKM is recorded.
+ webapps::ShortcutInfo same_origin_info = CreateValidShortcutInfo(victim_url);
+ CallOnFinishedInstall(install_service, web_contents()->GetWeakPtr(),
+ same_origin_info,
+ gfx::test::CreateBitmap(1, SK_ColorRED),
+ webapps::WebApkInstallResult::SERVER_ERROR);
+ EXPECT_EQ(1u, ukm_recorder.GetEntries("Webapp.AddToHomeScreen", {}).size());
+}
diff --git a/chrome/browser/android/webapk/BUILD.gn b/chrome/browser/android/webapk/BUILD.gn
index a1535df3..df7eb76 100644
--- a/chrome/browser/android/webapk/BUILD.gn
+++ b/chrome/browser/android/webapk/BUILD.gn
@@ -62,6 +62,7 @@
":webapk_sources",
"//base/test:test_support",
"//chrome/browser/android",
+ "//chrome/browser/android:shortcut_helper",
"//chrome/browser/profiles:profile",
"//chrome/common:constants",
"//chrome/test/base:android_browser_test",
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/android/webapk/webapk_installer_browsertest.cc b/chrome/browser/android/webapk/webapk_installer_browsertest.cc
index ea26c2b..758a9e8 100644
--- a/chrome/browser/android/webapk/webapk_installer_browsertest.cc
+++ b/chrome/browser/android/webapk/webapk_installer_browsertest.cc
@@ -4,6 +4,7 @@
#include "chrome/browser/android/webapk/webapk_installer.h"
+#include <atomic>
#include <memory>
#include <utility>
@@ -14,7 +15,9 @@
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/thread_restrictions.h"
+#include "chrome/browser/android/shortcut_helper.h"
#include "chrome/browser/android/webapk/webapk_install_service.h"
+#include "chrome/browser/android/webapk/webapk_install_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/android/android_browser_test.h"
@@ -27,6 +30,7 @@
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
+#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -233,6 +237,7 @@
~WebApkInstallerBrowserTest() override = default;
void SetUpOnMainThread() override {
+ host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->AddDefaultHandlers(base::FilePath(kTestDataDir));
embedded_test_server()->RegisterRequestHandler(
base::BindRepeating(&WebApkInstallerBrowserTest::HandleWebApkRequest,
@@ -311,6 +316,30 @@
return Profile::FromBrowserContext(web_contents()->GetBrowserContext());
}
+ webapps::ShortcutInfo CreateValidShortcutInfo(const GURL& url) {
+ webapps::ShortcutInfo info(url);
+ info.manifest_id = url;
+ info.user_title = u"Test App";
+ info.name = u"Test App";
+ info.short_name = u"Test";
+ info.display = blink::mojom::DisplayMode::kStandalone;
+ info.splash_image_url = url.Resolve(kBestSplashIconUrl);
+ return info;
+ }
+
+ void CallOnFinishedInstall(WebApkInstallService* service,
+ base::WeakPtr<content::WebContents> web_contents,
+ const webapps::ShortcutInfo& shortcut_info,
+ const SkBitmap& primary_icon,
+ webapps::WebApkInstallResult result) {
+ service->OnFinishedInstall(web_contents, shortcut_info, primary_icon,
+ result,
+ /*relax_updates=*/false,
+ /*webapk_package_name=*/"");
+ }
+
+ int victim_splash_requests() const { return victim_splash_requests_.load(); }
+
private:
// Sets default configuration for running WebApkInstaller.
void SetDefaults() {
@@ -321,11 +350,16 @@
std::unique_ptr<net::test_server::HttpResponse> HandleWebApkRequest(
const net::test_server::HttpRequest& request) {
+ if (request.relative_url.starts_with("/victim_splash.png")) {
+ victim_splash_requests_++;
+ }
return (request.relative_url == kServerUrl)
? webapk_response_builder_.Run()
: std::unique_ptr<net::test_server::HttpResponse>();
}
+ std::atomic<int> victim_splash_requests_{0};
+
// Builds response to the WebAPK creation request.
WebApkResponseBuilder webapk_response_builder_;
};
@@ -537,3 +571,84 @@
// Clean up
base::DeletePathRecursively(outer_file_path);
}
+
+// Tests that ShortcutHelper::AddToLauncherWithSkBitmap sanitizes a cross-origin
+// WebContents and does not record UKM or fetch splash icons for the navigated
+// origin.
+IN_PROC_BROWSER_TEST_F(WebApkInstallerBrowserTest,
+ AddToLauncherSanitizesCrossOriginWebContents) {
+ GURL victim_url = embedded_test_server()->GetURL("victim.com", "/empty.html");
+ ASSERT_TRUE(content::NavigateToURL(web_contents(), victim_url));
+
+ webapps::ShortcutInfo cross_origin_info =
+ CreateValidShortcutInfo(GURL("https://attacker.com/index.html"));
+ // Point the splash image URL to a path on the victim origin to simulate the
+ // confused-deputy attack scenario from b/540049672.
+ cross_origin_info.splash_image_url = victim_url.Resolve("/victim_splash.png");
+
+ ukm::TestAutoSetUkmRecorder ukm_recorder;
+
+ // With a cross-origin WebContents, the pointer is sanitized to nullptr so no
+ // UKM entry is recorded and no splash icon request is sent to the victim.
+ ShortcutHelper::AddToLauncherWithSkBitmap(
+ web_contents(), cross_origin_info,
+ gfx::test::CreateBitmap(1, SK_ColorRED),
+ webapps::InstallableStatusCode::WEBAPK_INSTALL_FAILED);
+ EXPECT_TRUE(ukm_recorder.GetEntries("Webapp.AddToHomeScreen", {}).empty());
+ EXPECT_EQ(0, victim_splash_requests());
+
+ // With a same-origin WebContents, the pointer is retained and UKM is
+ // recorded.
+ webapps::ShortcutInfo same_origin_info = CreateValidShortcutInfo(victim_url);
+ ShortcutHelper::AddToLauncherWithSkBitmap(
+ web_contents(), same_origin_info, gfx::test::CreateBitmap(1, SK_ColorRED),
+ webapps::InstallableStatusCode::WEBAPK_INSTALL_FAILED);
+ EXPECT_EQ(1u, ukm_recorder.GetEntries("Webapp.AddToHomeScreen", {}).size());
+}
+
+// Tests that ShortcutHelper::AddToLauncherWithSkBitmap handles nullptr
+// WebContents safely without crashing or recording UKM.
+IN_PROC_BROWSER_TEST_F(WebApkInstallerBrowserTest,
+ AddToLauncherHandlesNullWebContents) {
+ webapps::ShortcutInfo shortcut_info =
+ CreateValidShortcutInfo(GURL("https://example.com/index.html"));
+
+ ukm::TestAutoSetUkmRecorder ukm_recorder;
+
+ ShortcutHelper::AddToLauncherWithSkBitmap(
+ /*web_contents=*/nullptr, shortcut_info,
+ gfx::test::CreateBitmap(1, SK_ColorRED),
+ webapps::InstallableStatusCode::WEBAPK_INSTALL_FAILED);
+ EXPECT_TRUE(ukm_recorder.GetEntries("Webapp.AddToHomeScreen", {}).empty());
+}
+
+// Tests that WebApkInstallService::OnFinishedInstall on failure triggers
+// shortcut fallback and does not leak a navigated cross-origin WebContents.
+IN_PROC_BROWSER_TEST_F(WebApkInstallerBrowserTest,
+ OnFinishedInstallFallbackSanitizesNavigatedWebContents) {
+ GURL victim_url = embedded_test_server()->GetURL("victim.com", "/empty.html");
+ ASSERT_TRUE(content::NavigateToURL(web_contents(), victim_url));
+
+ webapps::ShortcutInfo cross_origin_info =
+ CreateValidShortcutInfo(GURL("https://attacker.com/index.html"));
+ cross_origin_info.splash_image_url = victim_url.Resolve("/victim_splash.png");
+
+ ukm::TestAutoSetUkmRecorder ukm_recorder;
+
+ auto* install_service =
+ WebApkInstallServiceFactory::GetForBrowserContext(profile());
+ CallOnFinishedInstall(install_service, web_contents()->GetWeakPtr(),
+ cross_origin_info,
+ gfx::test::CreateBitmap(1, SK_ColorRED),
+ webapps::WebApkInstallResult::SERVER_ERROR);
+ EXPECT_TRUE(ukm_recorder.GetEntries("Webapp.AddToHomeScreen", {}).empty());
+ EXPECT_EQ(0, victim_splash_requests());
+
+ // With a same-origin WebContents, UKM is recorded.
+ webapps::ShortcutInfo same_origin_info = CreateValidShortcutInfo(victim_url);
+ CallOnFinishedInstall(install_service, web_contents()->GetWeakPtr(),
+ same_origin_info,
+ gfx::test::CreateBitmap(1, SK_ColorRED),
+ webapps::WebApkInstallResult::SERVER_ERROR);
+ EXPECT_EQ(1u, ukm_recorder.GetEntries("Webapp.AddToHomeScreen", {}).size());
+}
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