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 <dibyapal@google.com>
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 <ksukh@chromium.org>
Reviewed-by: Mike Wasserman <msw@chromium.org>
Commit-Queue: Dibyajyoti Pal <dibyapal@chromium.org>
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