Chrome · WebAppInstalls › PWA
CVE-2026-106270
Authorization Error in WebAppInstalls (PWA)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ServiceWorkerBindingContextchrome/browser/badging/badge_manager.h |
modified | |
forchrome/browser/ui/web_applications/web_app_badging_browsertest.cc |
modified | |
ifchrome/browser/ui/web_applications/web_app_badging_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/web_applications/web_app_badging_browsertest.ccchrome/test/data/web_app_badging/badging_with_frames_and_workers.htmlchrome/browser/badging/badge_manager.ccchrome/browser/badging/badge_manager.h
Patch
From aa50be414aa20c2ef4fed3aced092af3628659e6 Mon Sep 17 00:00:00 2001 From: Dibyajyoti Pal <[email protected]> Date: Tue, 08 Sep 2026 17:58:17 -0700 Subject: [PATCH] [PWA] Prevent cross-partition Badging API manipulation The Badging API browser backend (`badging::BadgeManager`) previously authorized `setAppBadge` and `clearAppBadge` requests based purely on URL and service worker scope matching against installed PWAs, without verifying whether the caller was in a third-party partitioned context. Consequently, embedded cross-site iframes and partitioned service workers could mutate or clear the OS-level badge of an installed first-party PWA. This change: 1. Rejects binding `blink.mojom.BadgeService` in `BadgeManager::BindFrameReceiverIfAllowed` when `frame->GetStorageKey().IsThirdPartyContext()` is true. 2. Rejects binding `blink.mojom.BadgeService` in `BadgeManager::BindServiceWorkerReceiverIfAllowed` when `info.storage_key.IsThirdPartyContext()` is true. 3. Adds defense-in-depth checks in both `FrameBindingContext` and `ServiceWorkerBindingContext` `GetAppIdsAndUrlsForBadging()`. 4. Updates `WebAppBadgingBrowserTest` to verify that cross-site partitioned frames and service workers cannot alter installed PWA badges. TAG=agy CONV=3c5bef9b-629a-426d-8def-9825d894854c Fixed: 553269860, 555293654 Change-Id: I5d5c2e39fccd18263c1c743bd5075705326376f0 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8334309 Reviewed-by: Kunjan Patel <[email protected]> Reviewed-by: Mike Wasserman <[email protected]> Commit-Queue: Dibyajyoti Pal <[email protected]> Cr-Commit-Position: refs/heads/main@{#1694247} --- diff --git a/chrome/browser/badging/badge_manager.cc b/chrome/browser/badging/badge_manager.cc index 012137be..550015b9 100644 --- a/chrome/browser/badging/badge_manager.cc +++ b/chrome/browser/badging/badge_manager.cc @@ -89,6 +89,11 @@ return; } + // The Badging API is only allowed in first-party contexts. + if (frame->GetStorageKey().IsThirdPartyContext()) { + return; + } + auto* profile = Profile::FromBrowserContext(frame->GetBrowserContext()); auto* badge_manager = @@ -115,6 +120,11 @@ return; } + // The Badging API is only allowed in first-party contexts. + if (info.storage_key.IsThirdPartyContext()) { + return; + } + auto* profile = Profile::FromBrowserContext( service_worker_process_host->GetBrowserContext()); @@ -124,7 +134,8 @@ return; auto context = std::make_unique<BadgeManager::ServiceWorkerBindingContext>( - service_worker_process_host->GetDeprecatedID(), info.scope); + service_worker_process_host->GetDeprecatedID(), info.scope, + info.storage_key); badge_manager->receivers_.Add(badge_manager, std::move(receiver), std::move(context)); @@ -175,6 +186,10 @@ return previous; } +void BadgeManager::FlushReceiversForTesting() { + receivers_.FlushForTesting(); +} + void BadgeManager::UpdateBadge(const webapps::AppId& app_id, std::optional<BadgeValue> value) { web_app::WebAppSyncBridge* sync_bridge = @@ -257,8 +272,9 @@ DCHECK_CURRENTLY_ON(content::BrowserThread::UI); content::RenderFrameHost* frame = content::RenderFrameHost::FromID(process_id_, frame_id_); - if (!frame) + if (!frame || frame->GetStorageKey().IsThirdPartyContext()) { return std::vector<std::tuple<webapps::AppId, GURL>>{}; + } const WebAppProvider* provider = WebAppProvider::GetForLocalAppsUnchecked( Profile::FromBrowserContext(frame->GetBrowserContext())); @@ -280,6 +296,10 @@ BadgeManager::ServiceWorkerBindingContext::GetAppIdsAndUrlsForBadging() const { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + if (storage_key_.IsThirdPartyContext()) { + return std::vector<std::tuple<webapps::AppId, GURL>>{}; + } + content::RenderProcessHost* render_process_host = content::RenderProcessHost::FromID(process_id_); if (!render_process_host) diff --git a/chrome/browser/badging/badge_manager.h b/chrome/browser/badging/badge_manager.h index 95d2cb2c..b304c81 100644 --- a/chrome/browser/badging/badge_manager.h +++ b/chrome/browser/badging/badge_manager.h @@ -17,6 +17,7 @@ #include "components/webapps/common/web_app_id.h" #include "content/public/browser/service_worker_version_base_info.h" #include "mojo/public/cpp/bindings/receiver_set.h" +#include "third_party/blink/public/common/storage_key/storage_key.h" #include "third_party/blink/public/mojom/badging/badging.mojom.h" #include "third_party/blink/public/mojom/service_worker/service_worker_ancestor_frame_type.mojom.h" #include "url/gurl.h" @@ -106,6 +107,7 @@ void ClearBadgeForTesting(const webapps::AppId& app_id, ukm::UkmRecorder* test_recorder); const base::Clock* SetClockForTesting(const base::Clock* clock); + void FlushReceiversForTesting(); private: // The BindingContext of a mojo request. Allows mojo calls to be tied back @@ -143,8 +145,10 @@ // The BindingContext for ServiceWorkerGlobalScope execution contexts. class ServiceWorkerBindingContext final : public BindingContext { public: - ServiceWorkerBindingContext(int process_id, const GURL& scope) - : process_id_(process_id), scope_(scope) {} + ServiceWorkerBindingContext(int process_id, + const GURL& scope, + const blink::StorageKey& storage_key) + : process_id_(process_id), scope_(scope), storage_key_(storage_key) {} ~ServiceWorkerBindingContext() override = default; // Returns the list of AppIds within the service worker's scope. Returns @@ -155,6 +159,7 @@ private: int process_id_; GURL scope_; + blink::StorageKey storage_key_; }; // Updates the badge for |app_id| to be |value|, if it is not std::nullopt. diff --git a/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc b/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc index c782226..7e9abb5 100644 --- a/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc +++ b/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc @@ -25,6 +25,8 @@ #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 "services/network/public/cpp/network_switches.h" using content::RenderFrameHost; @@ -35,20 +37,30 @@ WebAppBadgingBrowserTest() : cross_origin_https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {} + // Host resolver rules must be set via command-line switches so out-of-process + // Network Services (e.g., on ChromeOS and Windows) receive the DNS mapping at + // startup. This allows early navigations in SetUpOnMainThread() to resolve + // cross-origin test hosts before Mojo IPC synchronization occurs. + void SetUpCommandLine(base::CommandLine* command_line) override { + WebAppBrowserTestBase::SetUpCommandLine(command_line); + command_line->AppendSwitchASCII(network::switches::kHostResolverRules, + "MAP * 127.0.0.1"); + } + void SetUpOnMainThread() override { WebAppBrowserTestBase::SetUpOnMainThread(); + cross_origin_https_server_.SetSSLConfig( + net::EmbeddedTestServer::CERT_TEST_NAMES); + cross_origin_https_server_.AddDefaultHandlers(GetChromeTestDataDir()); ASSERT_TRUE(cross_origin_https_server_.Start()); - ASSERT_TRUE(embedded_test_server()->Start()); - GURL cross_site_frame_url = - cross_origin_https_server_.GetURL("/web_app_badging/blank.html"); + GURL cross_site_frame_url = cross_origin_https_server_.GetURL( + "b.test", "/web_app_badging/blank.html"); cross_site_app_id_ = InstallPWA(cross_site_frame_url); - // Note: The url for the cross site frame is embedded in the query string. GURL start_url = embedded_https_test_server().GetURL( - "/web_app_badging/badging_with_frames_and_workers.html?url=" + - cross_site_frame_url.spec()); + "/web_app_badging/badging_with_frames_and_workers.html"); main_app_id_ = InstallPWA(start_url); GURL sub_start_url = @@ -64,6 +76,10 @@ apps::AppReadinessWaiter(profile(), sub_app_id_).Await(); content::WebContents* web_contents = OpenApplication(main_app_id_); + ASSERT_TRUE(content::WaitForLoadStop(web_contents)); + ASSERT_TRUE(content::NavigateIframeToURL(web_contents, "cross-site", + cross_site_frame_url)); + // There should be exactly 4 frames: // 1) The main frame. // 2) A frame containing a sub app. @@ -71,7 +87,6 @@ // 4) A sub frame in the app's scope. auto frames = CollectAllRenderFrameHosts(web_contents->GetPrimaryPage()); ASSERT_EQ(4u, frames.size()); - main_frame_ = web_contents->GetPrimaryMainFrame(); for (auto* frame : frames) { if (frame->GetLastCommittedURL() == sub_start_url) { @@ -83,17 +98,17 @@ cross_site_frame_ = frame; } } - ASSERT_TRUE(main_frame_); ASSERT_TRUE(sub_app_frame_); ASSERT_TRUE(in_scope_frame_); ASSERT_TRUE(cross_site_frame_); + ASSERT_EQ(cross_site_frame_url, cross_site_frame_->GetLastCommittedURL()); - // Register two service workers: + // Register two service workers on a.test: // 1) A service worker with a scope that applies to both the main app and // the sub app. // 2) A service worker with a scope that applies to the sub app only. - app_service_worker_scope_ = start_url.GetWithoutFilename(); + // app_service_worker_scope_ = start_url.GetWithoutFilename(); const std::string register_app_service_worker_script = content::JsReplace( kRegisterServiceWorkerScript, app_service_worker_scope_.spec()); ASSERT_EQ("OK", @@ -138,7 +153,7 @@ void OnBadgeChanged() { // This is only set up to deal with one badge change at a time per app, - // in order to make asserting the result of a badge change easier. A single + // in order to make asserting the result of a badge change easier. A single // service worker badge call may affect multiple apps within its scope. const size_t total_changes = delegate_->cleared_badges().size() + delegate_->set_badges().size(); @@ -238,6 +253,12 @@ main_frame_, expected_badge_change_count); } + badging::TestBadgeManagerDelegate* delegate() { return delegate_; } + + badging::BadgeManager* badge_manager() { + return badging::BadgeManagerFactory::GetForProfile(profile()); + } + const webapps::AppId& main_app_id() { return main_app_id_; } const webapps::AppId& sub_app_id() { return sub_app_id_; } const webapps::AppId& cross_site_app_id() { return cross_site_app_id_; } @@ -261,7 +282,7 @@ // Only the sub app is within this scope. GURL sub_app_service_worker_scope_; - // Frame badge updates affect the badge for at most 1 app. However, a single + // Frame badge updates affect the badge for at most 1 app. However, a single // service worker badge update may affect multiple apps. size_t expected_badge_change_count_ = 0; @@ -297,26 +318,115 @@ net::EmbeddedTestServer cross_origin_https_server_; }; -// Tests that the badge for the main frame is not affected by changing the badge -// of a cross site subframe. +// Tests that a cross site subframe (in a third-party partitioned context) +// cannot change the badge of the main frame or the cross site app. IN_PROC_BROWSER_TEST_F(WebAppBadgingBrowserTest, - CrossSiteFrameCannotChangeMainFrameBadge) { - // Clearing from cross site frame should affect only the cross site app. - ExecuteScriptAndWaitForBadgeChange("navigator.clearAppBadge()", - cross_site_frame_); + CrossSiteFrameCannotChangeBadge) { + // Set a baseline badge on the app from the first-party main frame. + ExecuteScriptAndWaitForBadgeChange("navigator.setAppBadge(99)", main_frame_); BadgeChange badge_change; - ASSERT_NO_FATAL_FAILURE(GetBadgeChange(cross_site_app_id(), &badge_change)); - ASSERT_TRUE(badge_change.was_cleared_); - ASSERT_FALSE(badge_change.was_flagged_); + ASSERT_NO_FATAL_FAILURE(GetBadgeChange(main_app_id(), &badge_change)); + EXPECT_EQ(99u, badge_change.last_badge_content_); + delegate()->ResetBadges(); - // Setting from cross site frame should affect only the cross site app. - ExecuteScriptAndWaitForBadgeChange("navigator.setAppBadge(77)", - cross_site_frame_); + // Clearing from cross site frame should not clear or affect any badges. + ASSERT_TRUE(content::ExecJs(cross_site_frame_, "navigator.clearAppBadge()")); + badge_manager()->FlushReceiversForTesting(); + EXPECT_TRUE(delegate()->cleared_badges().empty()); + EXPECT_TRUE(delegate()->set_badges().empty()); + EXPECT_EQ(99u, badge_manager()->GetBadgeValue(main_app_id())); - ASSERT_NO_FATAL_FAILURE(GetBadgeChange(cross_site_app_id(), &badge_change)); - ASSERT_FALSE(badge_change.was_cleared_); - ASSERT_FALSE(badge_change.was_flagged_); - ASSERT_EQ(77u, badge_change.last_badge_content_); + // Setting from cross site frame should not overwrite or affect any badges. + ASSERT_TRUE(content::ExecJs(cross_site_frame_, "navigator.setAppBadge(77)")); + badge_manager()->FlushReceiversForTesting(); + EXPECT_TRUE(delegate()->cleared_badges().empty()); + EXPECT_TRUE(delegate()->set_badges().empty()); + EXPECT_EQ(99u, badge_manager()->GetBadgeValue(main_app_id())); +} + +// Tests that a service worker in a cross-site subframe (in a third-party +// partitioned context) cannot change or clear badges for any app. +IN_PROC_BROWSER_TEST_F(WebAppBadgingBrowserTest, + CrossSiteServiceWorkerCannotChangeBadge) { + // Register a service worker inside the cross-site (b.test) subframe. + GURL cross_site_sw_scope = + cross_site_frame_->GetLastCommittedURL().GetWithoutFilename(); + const std::string register_script = content::JsReplace( + R"( + (async () => { + const reg = await navigator.serviceWorker.register( + '/web_app_badging/service_worker.js', {scope: $1}); + if (reg.active) { + return 'OK'; + } + const worker = reg.waiting || reg.installing; + if (!worker) { + return 'NO_WORKER'; + } + if (worker.state === 'activated') { + return 'OK'; + } + return new Promise((resolve, reject) => { + worker.addEventListener('statechange', () => { + if (worker.state === 'activated') resolve('OK'); + if (worker.state === 'redundant') reject('worker became redundant'); + }); + }); + })(); + )", + cross_site_sw_scope.spec()); + ASSERT_EQ("OK", EvalJs(cross_site_frame_, register_script)); + + // Set a baseline badge on the app from the first-party main frame. + ExecuteScriptAndWaitForBadgeChange("navigator.setAppBadge(99)", main_frame_); + BadgeChange badge_change; + ASSERT_NO_FATAL_FAILURE(GetBadgeChange(main_app_id(), &badge_change)); + EXPECT_EQ(99u, badge_change.last_badge_content_); + delegate()->ResetBadges(); + + // Instruct the partitioned service worker to clear a badge. + const std::string clear_badge_script = content::JsReplace( + R"( + (async () => { + const reg = await navigator.serviceWorker.getRegistration($1); + const worker = reg ? (reg.active || reg.waiting || reg.installing) : null; + if (!worker) { + return 'NO_WORKER'; + } + return new Promise((resolve) => { + navigator.serviceWorker.addEventListener('message', (e) => resolve(e.data), {once: true}); + worker.postMessage({ command: 'clear-app-badge' }); + }); + })(); + )", + cross_site_sw_scope.spec()); + ASSERT_EQ("OK", EvalJs(cross_site_frame_, clear_badge_script)); + badge_manager()->FlushReceiversForTesting(); + EXPECT_TRUE(delegate()->cleared_badges().empty()); + EXPECT_TRUE(delegate()->set_badges().empty()); + EXPECT_EQ(99u, badge_manager()->GetBadgeValue(main_app_id())); + + // Instruct the partitioned service worker to set a badge. + const std::string set_badge_script = content::JsReplace( + R"( + (async () => { + const reg = await navigator.serviceWorker.getRegistration($1); + const worker = reg ? (reg.active || reg.waiting || reg.installing) : null; + if (!worker) { + return 'NO_WORKER'; + } + return new Promise((resolve) => { + navigator.serviceWorker.addEventListener('message', (e) => resolve(e.data), {once: true}); + worker.postMessage({ command: 'set-app-badge', value: 42 }); + }); + })(); + )", + cross_site_sw_scope.spec()); + ASSERT_EQ("OK", EvalJs(cross_site_frame_, set_badge_script)); + badge_manager()->FlushReceiversForTesting(); + EXPECT_TRUE(delegate()->cleared_badges().empty()); + EXPECT_TRUE(delegate()->set_badges().empty()); + EXPECT_EQ(99u, badge_manager()->GetBadgeValue(main_app_id())); } ... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc b/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc
index c782226..7e9abb5 100644
--- a/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc
+++ b/chrome/browser/ui/web_applications/web_app_badging_browsertest.cc
@@ -25,6 +25,8 @@
#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 "services/network/public/cpp/network_switches.h"
using content::RenderFrameHost;
@@ -35,20 +37,30 @@
WebAppBadgingBrowserTest()
: cross_origin_https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
+ // Host resolver rules must be set via command-line switches so out-of-process
+ // Network Services (e.g., on ChromeOS and Windows) receive the DNS mapping at
+ // startup. This allows early navigations in SetUpOnMainThread() to resolve
+ // cross-origin test hosts before Mojo IPC synchronization occurs.
+ void SetUpCommandLine(base::CommandLine* command_line) override {
+ WebAppBrowserTestBase::SetUpCommandLine(command_line);
+ command_line->AppendSwitchASCII(network::switches::kHostResolverRules,
+ "MAP * 127.0.0.1");
+ }
+
void SetUpOnMainThread() override {
WebAppBrowserTestBase::SetUpOnMainThread();
+ cross_origin_https_server_.SetSSLConfig(
+ net::EmbeddedTestServer::CERT_TEST_NAMES);
+ cross_origin_https_server_.AddDefaultHandlers(GetChromeTestDataDir());
ASSERT_TRUE(cross_origin_https_server_.Start());
- ASSERT_TRUE(embedded_test_server()->Start());
- GURL cross_site_frame_url =
- cross_origin_https_server_.GetURL("/web_app_badging/blank.html");
+ GURL cross_site_frame_url = cross_origin_https_server_.GetURL(
+ "b.test", "/web_app_badging/blank.html");
cross_site_app_id_ = InstallPWA(cross_site_frame_url);
- // Note: The url for the cross site frame is embedded in the query string.
GURL start_url = embedded_https_test_server().GetURL(
- "/web_app_badging/badging_with_frames_and_workers.html?url=" +
- cross_site_frame_url.spec());
+ "/web_app_badging/badging_with_frames_and_workers.html");
main_app_id_ = InstallPWA(start_url);
GURL sub_start_url =
@@ -64,6 +76,10 @@
apps::AppReadinessWaiter(profile(), sub_app_id_).Await();
content::WebContents* web_contents = OpenApplication(main_app_id_);
+ ASSERT_TRUE(content::WaitForLoadStop(web_contents));
+ ASSERT_TRUE(content::NavigateIframeToURL(web_contents, "cross-site",
+ cross_site_frame_url));
+
// There should be exactly 4 frames:
// 1) The main frame.
// 2) A frame containing a sub app.
@@ -71,7 +87,6 @@
// 4) A sub frame in the app's scope.
auto frames = CollectAllRenderFrameHosts(web_contents->GetPrimaryPage());
ASSERT_EQ(4u, frames.size());
-
main_frame_ = web_contents->GetPrimaryMainFrame();
for (auto* frame : frames) {
if (frame->GetLastCommittedURL() == sub_start_url) {
@@ -83,17 +98,17 @@
cross_site_frame_ = frame;
}
}
-
ASSERT_TRUE(main_frame_);
ASSERT_TRUE(sub_app_frame_);
ASSERT_TRUE(in_scope_frame_);
ASSERT_TRUE(cross_site_frame_);
+ ASSERT_EQ(cross_site_frame_url, cross_site_frame_->GetLastCommittedURL());
- // Register two service workers:
+ // Register two service workers on a.test:
// 1) A service worker with a scope that applies to both the main app and
// the sub app.
// 2) A service worker with a scope that applies to the sub app only.
- app_service_worker_scope_ = start_url.GetWithoutFilename();
+ // app_service_worker_scope_ = start_url.GetWithoutFilename();
const std::string register_app_service_worker_script = content::JsReplace(
kRegisterServiceWorkerScript, app_service_worker_scope_.spec());
ASSERT_EQ("OK",
@@ -138,7 +153,7 @@
void OnBadgeChanged() {
// This is only set up to deal with one badge change at a time per app,
- // in order to make asserting the result of a badge change easier. A single
+ // in order to make asserting the result of a badge change easier. A single
// service worker badge call may affect multiple apps within its scope.
const size_t total_changes =
delegate_->cleared_badges().size() + delegate_->set_badges().size();
@@ -238,6 +253,12 @@
main_frame_, expected_badge_change_count);
}
+ badging::TestBadgeManagerDelegate* delegate() { return delegate_; }
+
+ badging::BadgeManager* badge_manager() {
+ return badging::BadgeManagerFactory::GetForProfile(profile());
+ }
+
const webapps::AppId& main_app_id() { return main_app_id_; }
const webapps::AppId& sub_app_id() { return sub_app_id_; }
const webapps::AppId& cross_site_app_id() { return cross_site_app_id_; }
@@ -261,7 +282,7 @@
// Only the sub app is within this scope.
GURL sub_app_service_worker_scope_;
- // Frame badge updates affect the badge for at most 1 app. However, a single
+ // Frame badge updates affect the badge for at most 1 app. However, a single
// service worker badge update may affect multiple apps.
size_t expected_badge_change_count_ = 0;
@@ -297,26 +318,115 @@
net::EmbeddedTestServer cross_origin_https_server_;
};
-// Tests that the badge for the main frame is not affected by changing the badge
-// of a cross site subframe.
+// Tests that a cross site subframe (in a third-party partitioned context)
+// cannot change the badge of the main frame or the cross site app.
IN_PROC_BROWSER_TEST_F(WebAppBadgingBrowserTest,
- CrossSiteFrameCannotChangeMainFrameBadge) {
- // Clearing from cross site frame should affect only the cross site app.
- ExecuteScriptAndWaitForBadgeChange("navigator.clearAppBadge()",
- cross_site_frame_);
+ CrossSiteFrameCannotChangeBadge) {
+ // Set a baseline badge on the app from the first-party main frame.
+ ExecuteScriptAndWaitForBadgeChange("navigator.setAppBadge(99)", main_frame_);
BadgeChange badge_change;
- ASSERT_NO_FATAL_FAILURE(GetBadgeChange(cross_site_app_id(), &badge_change));
- ASSERT_TRUE(badge_change.was_cleared_);
- ASSERT_FALSE(badge_change.was_flagged_);
+ ASSERT_NO_FATAL_FAILURE(GetBadgeChange(main_app_id(), &badge_change));
+ EXPECT_EQ(99u, badge_change.last_badge_content_);
+ delegate()->ResetBadges();
- // Setting from cross site frame should affect only the cross site app.
- ExecuteScriptAndWaitForBadgeChange("navigator.setAppBadge(77)",
- cross_site_frame_);
+ // Clearing from cross site frame should not clear or affect any badges.
+ ASSERT_TRUE(content::ExecJs(cross_site_frame_, "navigator.clearAppBadge()"));
+ badge_manager()->FlushReceiversForTesting();
+ EXPECT_TRUE(delegate()->cleared_badges().empty());
+ EXPECT_TRUE(delegate()->set_badges().empty());
+ EXPECT_EQ(99u, badge_manager()->GetBadgeValue(main_app_id()));
- ASSERT_NO_FATAL_FAILURE(GetBadgeChange(cross_site_app_id(), &badge_change));
- ASSERT_FALSE(badge_change.was_cleared_);
- ASSERT_FALSE(badge_change.was_flagged_);
- ASSERT_EQ(77u, badge_change.last_badge_content_);
+ // Setting from cross site frame should not overwrite or affect any badges.
+ ASSERT_TRUE(content::ExecJs(cross_site_frame_, "navigator.setAppBadge(77)"));
+ badge_manager()->FlushReceiversForTesting();
+ EXPECT_TRUE(delegate()->cleared_badges().empty());
+ EXPECT_TRUE(delegate()->set_badges().empty());
+ EXPECT_EQ(99u, badge_manager()->GetBadgeValue(main_app_id()));
+}
+
+// Tests that a service worker in a cross-site subframe (in a third-party
+// partitioned context) cannot change or clear badges for any app.
+IN_PROC_BROWSER_TEST_F(WebAppBadgingBrowserTest,
+ CrossSiteServiceWorkerCannotChangeBadge) {
+ // Register a service worker inside the cross-site (b.test) subframe.
+ GURL cross_site_sw_scope =
+ cross_site_frame_->GetLastCommittedURL().GetWithoutFilename();
+ const std::string register_script = content::JsReplace(
+ R"(
+ (async () => {
+ const reg = await navigator.serviceWorker.register(
+ '/web_app_badging/service_worker.js', {scope: $1});
+ if (reg.active) {
+ return 'OK';
+ }
+ const worker = reg.waiting || reg.installing;
+ if (!worker) {
+ return 'NO_WORKER';
+ }
+ if (worker.state === 'activated') {
+ return 'OK';
+ }
+ return new Promise((resolve, reject) => {
+ worker.addEventListener('statechange', () => {
+ if (worker.state === 'activated') resolve('OK');
+ if (worker.state === 'redundant') reject('worker became redundant');
+ });
+ });
+ })();
+ )",
+ cross_site_sw_scope.spec());
+ ASSERT_EQ("OK", EvalJs(cross_site_frame_, register_script));
+
+ // Set a baseline badge on the app from the first-party main frame.
+ ExecuteScriptAndWaitForBadgeChange("navigator.setAppBadge(99)", main_frame_);
+ BadgeChange badge_change;
+ ASSERT_NO_FATAL_FAILURE(GetBadgeChange(main_app_id(), &badge_change));
+ EXPECT_EQ(99u, badge_change.last_badge_content_);
+ delegate()->ResetBadges();
+
+ // Instruct the partitioned service worker to clear a badge.
+ const std::string clear_badge_script = content::JsReplace(
+ R"(
+ (async () => {
+ const reg = await navigator.serviceWorker.getRegistration($1);
+ const worker = reg ? (reg.active || reg.waiting || reg.installing) : null;
+ if (!worker) {
+ return 'NO_WORKER';
+ }
+ return new Promise((resolve) => {
+ navigator.serviceWorker.addEventListener('message', (e) => resolve(e.data), {once: true});
+ worker.postMessage({ command: 'clear-app-badge' });
+ });
+ })();
+ )",
+ cross_site_sw_scope.spec());
+ ASSERT_EQ("OK", EvalJs(cross_site_frame_, clear_badge_script));
+ badge_manager()->FlushReceiversForTesting();
+ EXPECT_TRUE(delegate()->cleared_badges().empty());
+ EXPECT_TRUE(delegate()->set_badges().empty());
+ EXPECT_EQ(99u, badge_manager()->GetBadgeValue(main_app_id()));
+
+ // Instruct the partitioned service worker to set a badge.
+ const std::string set_badge_script = content::JsReplace(
+ R"(
+ (async () => {
+ const reg = await navigator.serviceWorker.getRegistration($1);
+ const worker = reg ? (reg.active || reg.waiting || reg.installing) : null;
+ if (!worker) {
+ return 'NO_WORKER';
+ }
+ return new Promise((resolve) => {
+ navigator.serviceWorker.addEventListener('message', (e) => resolve(e.data), {once: true});
+ worker.postMessage({ command: 'set-app-badge', value: 42 });
+ });
+ })();
+ )",
+ cross_site_sw_scope.spec());
+ ASSERT_EQ("OK", EvalJs(cross_site_frame_, set_badge_script));
+ badge_manager()->FlushReceiversForTesting();
+ EXPECT_TRUE(delegate()->cleared_badges().empty());
+ EXPECT_TRUE(delegate()->set_badges().empty());
+ EXPECT_EQ(99u, badge_manager()->GetBadgeValue(main_app_id()));
}
// Tests that setting the badge to an integer will be propagated across
diff --git a/chrome/test/data/web_app_badging/badging_with_frames_and_workers.html b/chrome/test/data/web_app_badging/badging_with_frames_and_workers.html
index 2ca8c7a..4ea07004 100644
--- a/chrome/test/data/web_app_badging/badging_with_frames_and_workers.html
+++ b/chrome/test/data/web_app_badging/badging_with_frames_and_workers.html
@@ -9,11 +9,6 @@
<script type="text/javascript">
'use strict';
- // Note: The url for the cross site frame is embedded in the query string.
- const crossFrame = document.getElementById('cross-site');
- const crossSiteUrl = new URLSearchParams(location.search).get('url');
- crossFrame.src = crossSiteUrl;
-
// Copy of //content/test/data/service_worker/create_service_worker.html.
// Simulates navigator.serviceWorker.ready, which can't be used since this
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