diff --git a/chrome/browser/extensions/api/management/management_apitest.cc b/chrome/browser/extensions/api/management/management_apitest.cc index d2f8009d26f03..5ea67c7d8d3be 100644 --- a/chrome/browser/extensions/api/management/management_apitest.cc +++ b/chrome/browser/extensions/api/management/management_apitest.cc @@ -56,6 +56,12 @@ #include "chrome/browser/web_applications/web_app_provider.h" #include "chrome/browser/web_applications/web_app_registrar.h" #include "chrome/test/base/ui_test_utils.h" +// For UAF_CapturedByOpenPWA: +#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h" +#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h" +#include "chrome/browser/web_applications/web_app_install_info.h" +#include "third_party/blink/public/mojom/manifest/display_mode.mojom-shared.h" +#include "third_party/blink/public/mojom/manifest/manifest_launch_handler.mojom-shared.h" #endif static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); @@ -398,6 +404,92 @@ IN_PROC_BROWSER_TEST_F(InstallReplacementWebAppApiTest, RunInstallableWebAppTest(kAppManifest, kGoodWebAppURL, kGoodWebAppURL); } + +// Regression / security test for a browser-process use-after-free in +// OnWebAppInstallabilityChecked +// (chrome/browser/extensions/api/management/chrome_management_api_delegate_non_android.cc). +// Preconditions: +// * An installed PWA whose scope covers the |replacement_web_app| URL, +// declares launch_handler.client_mode = "focus-existing", and is open in a +// standalone app window (so PWA Navigation Capturing returns +// NavigationCapturingOverride{browser=nullptr} for that URL). +// * A from-webstore extension declaring |replacement_web_app| pointing at a +// *different*, installable manifest inside that scope. +// Calling chrome.management.installReplacementWebApp() then reaches +// case kInstallable: +// containing_contents = web_contents.get(); +// chrome::AddWebContents(..., std::move(web_contents), ...); // <- frees it +// web_app::CreateWebAppFromManifest(containing_contents, ...); // <- UAF +// because Navigate() drops the moved-in WebContents (focus-existing returns +// browser=nullptr; ~NavigateParams() frees contents_to_insert). +// +// Under ASAN this should report heap-use-after-free in +// WebAppProvider::GetForWebContents() -> +// content::WebContentsImpl::GetBrowserContext() (virtual call through freed +// WebContentsImpl). +IN_PROC_BROWSER_TEST_F(InstallReplacementWebAppApiTest, UAF_CapturedByOpenPWA) { + // The replacement_web_app target. Must be installable (has a manifest with a + // distinct id) and live inside the capturing PWA's scope. + static constexpr char kTargetUrlPath[] = + "/management/install_replacement_web_app/uaf_target_app/index.html"; + const GURL target_url = https_test_server_.GetURL(kTargetUrlPath); + + // 1. Install the "capturing" PWA. Its scope is the parent directory, so it + // covers |target_url|. Its manifest_id (== start_url) is different from + // |target_url|, so InstallOrLaunchReplacementWebApp does not short-circuit + // via AppMatches() and FetchInstallabilityForChromeManagement reports + // kInstallable. + const GURL pwa_start_url = https_test_server_.GetURL( + "/management/install_replacement_web_app/uaf_capturing_pwa.html"); + const GURL pwa_scope = + https_test_server_.GetURL("/management/install_replacement_web_app/"); + + auto install_info = web_app::WebAppInstallInfo::CreateForTesting( + pwa_start_url, blink::mojom::DisplayMode::kStandalone, + web_app::mojom::UserDisplayMode::kStandalone, + blink::mojom::ManifestLaunchHandler_ClientMode::kFocusExisting); + install_info->scope = pwa_scope; + webapps::AppId pwa_app_id = + web_app::test::InstallWebApp(profile(), std::move(install_info)); + + // The capturing PWA must be the link-capturing app for |target_url|. (On + // Win/Mac/Linux this is the production default — kReimplDefaultOn — so no + // explicit user opt-in is required.) + auto& registrar = + web_app::WebAppProvider::GetForTest(profile())->registrar_unsafe(); + ASSERT_TRUE(registrar.CapturesLinksInScope(pwa_app_id)); + ASSERT_EQ(registrar.FindAppThatCapturesLinksInScope(target_url), pwa_app_id); + + // 2. Open the capturing PWA in a standalone app window so that + // GetEffectiveClientModeAndBrowser() finds an existing top-level browsing + // context for it and selects kFocusExisting -> browser=nullptr. + Browser* app_browser = + web_app::LaunchWebAppBrowserAndWait(profile(), pwa_app_id); + ASSERT_TRUE(app_browser); + ASSERT_TRUE(app_browser->is_type_app()); + + // ScopedTabbedBrowserDisplayer (in the kInstallable branch) must find a + // tabbed browser with at least one tab — otherwise navigation capturing + // bails out with is_for_new_browser=true. + ASSERT_GE(browser()->tab_strip_model()->count(), 1); + + // 3. Install a "from webstore" extension declaring |replacement_web_app| = + // |target_url| and call chrome.management.installReplacementWebApp() with + // a user gesture from its service worker. + static constexpr char kBackground[] = + R"(chrome.test.runWithUserGesture(function() { + chrome.management.installReplacementWebApp(function() { + // Under ASAN the browser process should have crashed before this + // callback is invoked. If it survives (e.g. fix applied), report + // success so the ResultCatcher unblocks. + chrome.test.notifyPass(); + }); + });)"; + + // This drives the API call. ASAN should catch the heap-use-after-free in the + // browser process during chrome::AddWebContents() / CreateWebAppFromManifest. + RunTest(kManifest, kTargetUrlPath, kBackground, /*from_webstore=*/true); +} #endif // BUILDFLAG(ENABLE_EXTENSIONS) // Tests actions on extensions when no management policy is in place. diff --git a/chrome/test/data/extensions/api_test/management/install_replacement_web_app/uaf_capturing_pwa.html b/chrome/test/data/extensions/api_test/management/install_replacement_web_app/uaf_capturing_pwa.html new file mode 100644 index 0000000000000..f886602f8678a --- /dev/null +++ b/chrome/test/data/extensions/api_test/management/install_replacement_web_app/uaf_capturing_pwa.html @@ -0,0 +1,5 @@ + + +Capturing PWA +Capturing PWA start page. + diff --git a/chrome/test/data/extensions/api_test/management/install_replacement_web_app/uaf_target_app/index.html b/chrome/test/data/extensions/api_test/management/install_replacement_web_app/uaf_target_app/index.html new file mode 100644 index 0000000000000..1452b20989668 --- /dev/null +++ b/chrome/test/data/extensions/api_test/management/install_replacement_web_app/uaf_target_app/index.html @@ -0,0 +1,8 @@ + + + + UAF target installable app + + +Installable app whose URL is inside the capturing PWA's scope. + diff --git a/chrome/test/data/extensions/api_test/management/install_replacement_web_app/uaf_target_app/manifest.json b/chrome/test/data/extensions/api_test/management/install_replacement_web_app/uaf_target_app/manifest.json new file mode 100644 index 0000000000000..04fcb10a0d2bc --- /dev/null +++ b/chrome/test/data/extensions/api_test/management/install_replacement_web_app/uaf_target_app/manifest.json @@ -0,0 +1,12 @@ +{ + "name": "UAF target installable app", + "icons": [ + { + "src": "../acceptable_web_app/image-512px.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "start_url": "index.html", + "display": "standalone" +}