Low chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactConfused deputy in WebAPKs
DescriptionConfused deputy in WebAPKs
ComponentWebAPKs
Sub-componentPWA
Bug ClassAuthorization Error
Tracker540049672
Fix commit2eda634a59fa (chromium/src) +157/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
chrome/browser/android/shortcut_helper.cc
modified
if
chrome/browser/android/webapk/webapk_install_service.cc
modified

Files Changed

  • chrome/browser/android/webapk/webapk_installer_browsertest.cc
  • chrome/browser/android/webapk/webapk_install_service.cc
  • chrome/browser/android/webapk/webapk_install_service.h
  • chrome/browser/android/webapk/BUILD.gn
  • chrome/browser/android/shortcut_helper.cc
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.