CVE-2026-106345
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/media/session/media_session_impl.cc |
modified | |
forcontent/browser/media/session/media_session_impl.cc |
modified | |
TEST_Fcontent/browser/media/session/media_session_impl_service_routing_unittest.cc |
modified | |
MediaSessionImplServiceRoutingFencedFrameTestcontent/browser/media/session/media_session_impl_service_routing_unittest.cc |
modified |
Files Changed
content/browser/media/session/media_session_impl_browsertest.cccontent/browser/media/session/media_session_impl.cccontent/browser/media/session/media_session_impl_service_routing_unittest.cc
Patch
From e5b12422e3ceac52186ebe12ae92d3f7048d90e4 Mon Sep 17 00:00:00 2001
From: Tommy Steimel <steimel@chromium.org>
Date: Fri, 04 Sep 2026 16:08:28 -0700
Subject: [PATCH] Media Session: Prevent cached/unloading RFH from affecting Media Session
This CL adds checks in MediaSessionImpl to ensure that RenderFrameHosts
that are either in back-forward cache or pending deletion cannot:
1) Own the routed MediaSessionServiceImpl
2) Add active players to the MediaSessionImpl
Bug: 556258544
Change-Id: I160d087e58b5fb697e139665cda95b2fe4fd847e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8341796
Commit-Queue: Tommy Steimel <steimel@chromium.org>
Reviewed-by: Benjamin Keen <bkeen@google.com>
Cr-Commit-Position: refs/heads/main@{#1692823}
---
diff --git a/content/browser/media/session/media_session_impl.cc b/content/browser/media/session/media_session_impl.cc
index 7c443c00..148b732 100644
--- a/content/browser/media/session/media_session_impl.cc
+++ b/content/browser/media/session/media_session_impl.cc
@@ -427,6 +427,7 @@
RemovePlayer(player.first.observer, player.first.player_id);
hidden_players_.insert(player.first);
}
+ UpdateRoutedService();
return;
}
@@ -447,12 +448,18 @@
if (added_players)
OnSuspendInternal(SuspendType::kSystem, State::SUSPENDED);
+ UpdateRoutedService();
return;
}
}
bool MediaSessionImpl::AddPlayer(MediaSessionPlayerObserver* observer,
int player_id) {
+ RenderFrameHost* rfh = observer->render_frame_host();
+ if (rfh && !rfh->IsActive()) {
+ return false;
+ }
+
media::MediaContentType media_content_type = observer->GetMediaContentType();
if (media_content_type == media::MediaContentType::kOneShot) {
@@ -1720,14 +1727,17 @@
}
}
- if (!routed_service_)
+ if (!routed_service_ || !routed_service_->GetRenderFrameHost() ||
+ !routed_service_->GetRenderFrameHost()->IsActive()) {
return;
+ }
routed_service_->GetClient()->DidReceiveAction(action, std::move(details));
}
bool MediaSessionImpl::IsServiceActiveForRenderFrameHost(RenderFrameHost* rfh) {
- return services_.find(rfh->GetGlobalId()) != services_.end();
+ return rfh && rfh->IsActive() &&
+ services_.find(rfh->GetGlobalId()) != services_.end();
}
void MediaSessionImpl::UpdateRoutedService() {
@@ -1756,13 +1766,13 @@
std::set<RenderFrameHost*> frames;
for (const auto& player : normal_players_) {
RenderFrameHost* frame = player.first.observer->render_frame_host();
- if (frame) {
+ if (frame && frame->IsActive()) {
frames.insert(frame);
}
}
for (const auto& player : one_shot_players_) {
RenderFrameHost* frame = player.observer->render_frame_host();
- if (frame) {
+ if (frame && frame->IsActive()) {
frames.insert(frame);
}
}
diff --git a/content/browser/media/session/media_session_impl_browsertest.cc b/content/browser/media/session/media_session_impl_browsertest.cc
index 0db7fe70..e6e7415 100644
--- a/content/browser/media/session/media_session_impl_browsertest.cc
+++ b/content/browser/media/session/media_session_impl_browsertest.cc
@@ -27,6 +27,7 @@
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/back_forward_cache/back_forward_cache_impl.h"
#include "content/browser/media/session/audio_focus_delegate.h"
+#include "content/browser/media/session/media_session_service_impl.h"
#include "content/browser/media/session/mock_media_session_player_observer.h"
#include "content/browser/media/session/mock_media_session_service_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
@@ -282,6 +283,10 @@
bool IsActive() { return media_session_->IsActive(); }
+ MediaSessionServiceImpl* GetRoutedService() const {
+ return media_session_->routed_service_;
+ }
+
std::optional<AudioFocusType> GetSessionAudioFocusType() {
return mock_audio_focus_delegate_->GetCurrentFocusType();
}
@@ -3651,9 +3656,11 @@
EXPECT_TRUE(frame_host->IsInBackForwardCache());
// Add a player on the new page
- StartNewPlayer(player_observer.get());
+ auto new_player_observer = std::make_unique<MockMediaSessionPlayerObserver>(
+ GetPrimaryMainFrame(), media::MediaContentType::kPersistent);
+ StartNewPlayer(new_player_observer.get());
ResolveAudioFocusSuccess();
- EXPECT_TRUE(player_observer->IsPlaying(1));
+ EXPECT_TRUE(new_player_observer->IsPlaying(0));
// Evict the page from the back-forward cache.
shell()->web_contents()->GetController().GetBackForwardCache().Flush();
@@ -3661,7 +3668,62 @@
// The page being removed from the back-forward cache should not affect the
// play state of the current page.
- EXPECT_TRUE(player_observer->IsPlaying(1));
+ EXPECT_TRUE(new_player_observer->IsPlaying(0));
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionImplWithBackForwardCacheBrowserTest,
+ BFCachedFrameCannotHijackMediaSession) {
+ EXPECT_TRUE(NavigateToURL(
+ shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
+
+ RenderFrameHostImplWrapper frame_host_a(GetPrimaryMainFrame());
+ mojo::Remote<blink::mojom::MediaSessionService> service_remote_a;
+ MediaSessionServiceImpl::Create(
+ frame_host_a.get(), service_remote_a.BindNewPipeAndPassReceiver());
+
+ auto metadata_a = blink::mojom::SpecMediaMetadata::New();
+ metadata_a->title = u"Page A Title";
+ service_remote_a->SetMetadata(std::move(metadata_a));
+ service_remote_a.FlushForTesting();
+
+ // Page A is routed.
+ EXPECT_NE(nullptr, GetRoutedService());
+ EXPECT_EQ(frame_host_a.get(), GetRoutedService()->GetRenderFrameHost());
+
+ // Navigate to Page B. Page A is cached in back-forward cache.
+ EXPECT_TRUE(NavigateToURL(
+ shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));
+ EXPECT_TRUE(frame_host_a->IsInBackForwardCache());
+
+ // Page A must no longer be routed while in BFCache.
+ EXPECT_EQ(nullptr, GetRoutedService());
+
+ // A document in BFCache cannot bind a new MediaSessionService.
+ mojo::Remote<blink::mojom::MediaSessionService> bad_service_remote;
+ MediaSessionServiceImpl::Create(
+ frame_host_a.get(), bad_service_remote.BindNewPipeAndPassReceiver());
+ bad_service_remote.FlushForTesting();
+ EXPECT_EQ(nullptr, GetRoutedService());
+
+ // A document in BFCache cannot update metadata on an existing Mojo pipe.
+ auto malicious_metadata = blink::mojom::SpecMediaMetadata::New();
+ malicious_metadata->title = u"Malicious Spoofed Title";
+ service_remote_a->SetMetadata(std::move(malicious_metadata));
+ service_remote_a.FlushForTesting();
+ EXPECT_EQ(nullptr, GetRoutedService());
+
+ // A document in BFCache cannot add a media player.
+ auto bad_player_observer = std::make_unique<MockMediaSessionPlayerObserver>(
+ frame_host_a.get(), media::MediaContentType::kPersistent);
+ EXPECT_FALSE(AddPlayer(bad_player_observer.get(), 0));
+ EXPECT_EQ(nullptr, GetRoutedService());
+
+ // Restoring Page A from BFCache allows it to be routed again.
+ shell()->web_contents()->GetController().GoBack();
+ EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+ EXPECT_FALSE(frame_host_a->IsInBackForwardCache());
+ EXPECT_NE(nullptr, GetRoutedService());
+ EXPECT_EQ(frame_host_a.get(), GetRoutedService()->GetRenderFrameHost());
}
// TODO(crbug.com/521159836): Re-enable on Linux MSAN.
diff --git a/content/browser/media/session/media_session_impl_service_routing_unittest.cc b/content/browser/media/session/media_session_impl_service_routing_unittest.cc
index 807cc898..6ebb6c8 100644
--- a/content/browser/media/session/media_session_impl_service_routing_unittest.cc
+++ b/content/browser/media/session/media_session_impl_service_routing_unittest.cc
@@ -13,6 +13,7 @@
#include "base/time/time.h"
#include "content/browser/media/session/media_session_impl.h"
#include "content/browser/media/session/mock_media_session_service_impl.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/media_session_player_observer.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_media_session_client.h"
@@ -1508,6 +1509,36 @@
}
}
+TEST_F(MediaSessionImplServiceRoutingTest, InactiveFrameIgnoredForRouting) {
+ CreateServiceForFrame(main_frame_);
+ CreateServiceForFrame(sub_frame_);
+ StartPlayerForFrame(sub_frame_);
+
+ ASSERT_EQ(services_[sub_frame_].get(), ComputeServiceForRouting());
+
+ // Make sub_frame inactive.
+ static_cast<RenderFrameHostImpl*>(sub_frame_.get())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ ASSERT_FALSE(sub_frame_->IsActive());
+
+ // sub_frame must not be routed. main_frame (which is active) should be
+ // routed.
+ ASSERT_EQ(services_[main_frame_].get(), ComputeServiceForRouting());
+}
+
+TEST_F(MediaSessionImplServiceRoutingTest, CannotAddPlayerForInactiveFrame) {
+ static_cast<RenderFrameHostImpl*>(sub_frame_.get())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ ASSERT_FALSE(sub_frame_->IsActive());
+
+ auto observer = std::make_unique<NiceMock<MockMediaSessionPlayerObserver>>(
+ sub_frame_, MediaAudioVideoState::kAudioOnly,
+ media::MediaContentType::kPersistent);
+ EXPECT_FALSE(GetMediaSession()->AddPlayer(observer.get(), 0));
+}
+
class MediaSessionImplServiceRoutingFencedFrameTest
: public MediaSessionImplServiceRoutingTest {
public:
Regression Test / PoC
diff --git a/content/browser/media/session/media_session_impl_browsertest.cc b/content/browser/media/session/media_session_impl_browsertest.cc
index 0db7fe70..e6e7415 100644
--- a/content/browser/media/session/media_session_impl_browsertest.cc
+++ b/content/browser/media/session/media_session_impl_browsertest.cc
@@ -27,6 +27,7 @@
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/back_forward_cache/back_forward_cache_impl.h"
#include "content/browser/media/session/audio_focus_delegate.h"
+#include "content/browser/media/session/media_session_service_impl.h"
#include "content/browser/media/session/mock_media_session_player_observer.h"
#include "content/browser/media/session/mock_media_session_service_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
@@ -282,6 +283,10 @@
bool IsActive() { return media_session_->IsActive(); }
+ MediaSessionServiceImpl* GetRoutedService() const {
+ return media_session_->routed_service_;
+ }
+
std::optional<AudioFocusType> GetSessionAudioFocusType() {
return mock_audio_focus_delegate_->GetCurrentFocusType();
}
@@ -3651,9 +3656,11 @@
EXPECT_TRUE(frame_host->IsInBackForwardCache());
// Add a player on the new page
- StartNewPlayer(player_observer.get());
+ auto new_player_observer = std::make_unique<MockMediaSessionPlayerObserver>(
+ GetPrimaryMainFrame(), media::MediaContentType::kPersistent);
+ StartNewPlayer(new_player_observer.get());
ResolveAudioFocusSuccess();
- EXPECT_TRUE(player_observer->IsPlaying(1));
+ EXPECT_TRUE(new_player_observer->IsPlaying(0));
// Evict the page from the back-forward cache.
shell()->web_contents()->GetController().GetBackForwardCache().Flush();
@@ -3661,7 +3668,62 @@
// The page being removed from the back-forward cache should not affect the
// play state of the current page.
- EXPECT_TRUE(player_observer->IsPlaying(1));
+ EXPECT_TRUE(new_player_observer->IsPlaying(0));
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionImplWithBackForwardCacheBrowserTest,
+ BFCachedFrameCannotHijackMediaSession) {
+ EXPECT_TRUE(NavigateToURL(
+ shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));
+
+ RenderFrameHostImplWrapper frame_host_a(GetPrimaryMainFrame());
+ mojo::Remote<blink::mojom::MediaSessionService> service_remote_a;
+ MediaSessionServiceImpl::Create(
+ frame_host_a.get(), service_remote_a.BindNewPipeAndPassReceiver());
+
+ auto metadata_a = blink::mojom::SpecMediaMetadata::New();
+ metadata_a->title = u"Page A Title";
+ service_remote_a->SetMetadata(std::move(metadata_a));
+ service_remote_a.FlushForTesting();
+
+ // Page A is routed.
+ EXPECT_NE(nullptr, GetRoutedService());
+ EXPECT_EQ(frame_host_a.get(), GetRoutedService()->GetRenderFrameHost());
+
+ // Navigate to Page B. Page A is cached in back-forward cache.
+ EXPECT_TRUE(NavigateToURL(
+ shell(), embedded_test_server()->GetURL("b.test", "/title1.html")));
+ EXPECT_TRUE(frame_host_a->IsInBackForwardCache());
+
+ // Page A must no longer be routed while in BFCache.
+ EXPECT_EQ(nullptr, GetRoutedService());
+
+ // A document in BFCache cannot bind a new MediaSessionService.
+ mojo::Remote<blink::mojom::MediaSessionService> bad_service_remote;
+ MediaSessionServiceImpl::Create(
+ frame_host_a.get(), bad_service_remote.BindNewPipeAndPassReceiver());
+ bad_service_remote.FlushForTesting();
+ EXPECT_EQ(nullptr, GetRoutedService());
+
+ // A document in BFCache cannot update metadata on an existing Mojo pipe.
+ auto malicious_metadata = blink::mojom::SpecMediaMetadata::New();
+ malicious_metadata->title = u"Malicious Spoofed Title";
+ service_remote_a->SetMetadata(std::move(malicious_metadata));
+ service_remote_a.FlushForTesting();
+ EXPECT_EQ(nullptr, GetRoutedService());
+
+ // A document in BFCache cannot add a media player.
+ auto bad_player_observer = std::make_unique<MockMediaSessionPlayerObserver>(
+ frame_host_a.get(), media::MediaContentType::kPersistent);
+ EXPECT_FALSE(AddPlayer(bad_player_observer.get(), 0));
+ EXPECT_EQ(nullptr, GetRoutedService());
+
+ // Restoring Page A from BFCache allows it to be routed again.
+ shell()->web_contents()->GetController().GoBack();
+ EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
+ EXPECT_FALSE(frame_host_a->IsInBackForwardCache());
+ EXPECT_NE(nullptr, GetRoutedService());
+ EXPECT_EQ(frame_host_a.get(), GetRoutedService()->GetRenderFrameHost());
}
// TODO(crbug.com/521159836): Re-enable on Linux MSAN.
diff --git a/content/browser/media/session/media_session_impl_service_routing_unittest.cc b/content/browser/media/session/media_session_impl_service_routing_unittest.cc
index 807cc898..6ebb6c8 100644
--- a/content/browser/media/session/media_session_impl_service_routing_unittest.cc
+++ b/content/browser/media/session/media_session_impl_service_routing_unittest.cc
@@ -13,6 +13,7 @@
#include "base/time/time.h"
#include "content/browser/media/session/media_session_impl.h"
#include "content/browser/media/session/mock_media_session_service_impl.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/media_session_player_observer.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_media_session_client.h"
@@ -1508,6 +1509,36 @@
}
}
+TEST_F(MediaSessionImplServiceRoutingTest, InactiveFrameIgnoredForRouting) {
+ CreateServiceForFrame(main_frame_);
+ CreateServiceForFrame(sub_frame_);
+ StartPlayerForFrame(sub_frame_);
+
+ ASSERT_EQ(services_[sub_frame_].get(), ComputeServiceForRouting());
+
+ // Make sub_frame inactive.
+ static_cast<RenderFrameHostImpl*>(sub_frame_.get())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ ASSERT_FALSE(sub_frame_->IsActive());
+
+ // sub_frame must not be routed. main_frame (which is active) should be
+ // routed.
+ ASSERT_EQ(services_[main_frame_].get(), ComputeServiceForRouting());
+}
+
+TEST_F(MediaSessionImplServiceRoutingTest, CannotAddPlayerForInactiveFrame) {
+ static_cast<RenderFrameHostImpl*>(sub_frame_.get())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ ASSERT_FALSE(sub_frame_->IsActive());
+
+ auto observer = std::make_unique<NiceMock<MockMediaSessionPlayerObserver>>(
+ sub_frame_, MediaAudioVideoState::kAudioOnly,
+ media::MediaContentType::kPersistent);
+ EXPECT_FALSE(GetMediaSession()->AddPlayer(observer.get(), 0));
+}
+
class MediaSessionImplServiceRoutingFencedFrameTest
: public MediaSessionImplServiceRoutingTest {
public:
Original Bug Report
Media Session Hijacking and UI Spoofing via BFCached RenderFrameHost
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A RenderFrameHost stored in the Back/Forward Cache (BFCache) retains open Mojo endpoints that lack browser-side lifecycle checks in the media session stack. A compromised renderer for a cached document can bind MediaSessionService and register a media player to capture the tab’s routed media session. This allows a navigated-away document to spoof system media UI attributed to the active victim origin and intercept media key inputs.
Affected files:
content/browser/media/session/media_session_impl.cccontent/browser/media/session/media_session_service_impl.cccontent/browser/media/media_web_contents_observer.cccontent/browser/media/session/media_session_controller.cccontent/browser/browser_interface_binders.cc
Estimated timestamp from git blame: 2021-10-13
Summary
When a page is navigated away from and stored in the Back/Forward Cache (BFCache), its RenderFrameHost enters the kInBackForwardCache lifecycle state while keeping its BrowserInterfaceBroker and channel-associated Mojo connections alive. In the browser process, the media session subsystem lacks continuous lifecycle validation (rfh->IsActive()). Consequently, a compromised renderer hosting a cached document can bind blink.mojom.MediaSessionService, register media playback via media::mojom::MediaPlayerHost, acquire tab-level audio focus, and capture session routing. This enables the stale cached document to display arbitrary metadata and artwork in system media controls attributed to the active victim origin, as well as intercept hardware media key actions.
Technical Details & Root Cause
-
Ungated Interface Binding: In
content/browser/browser_interface_binders.cc:1393,MediaSessionServiceis bound without lifecycle wrappers:map->Add<blink::mojom::MediaSessionService>(&MediaSessionServiceImpl::Create);In
content/browser/media/session/media_session_service_impl.cc:39-46,MediaSessionServiceImpl::Createdoes not checkrender_frame_host->IsActive():void MediaSessionServiceImpl::Create( RenderFrameHost* render_frame_host, mojo::PendingReceiver<blink::mojom::MediaSessionService> receiver) { mojo::MakeSelfOwnedReceiver( base::WrapUnique<MediaSessionServiceImpl>( new MediaSessionServiceImpl(render_frame_host)), std::move(receiver)); }The constructor immediately calls
GetMediaSession()->OnServiceCreated(this)(:21-30), which inserts the cached frame intoMediaSessionImpl::services_[rfh_id](content/browser/media/session/media_session_impl.cc:1639-1644). -
Ungated Player Registration & Focus Acquisition:
MediaSessionImpl::RenderFrameHostStateChanged(:412-431) clears players only during the state transition into BFCache. If the cached frame subsequently registers a player overmedia::mojom::MediaPlayerHost,MediaWebContentsObserver::OnMediaPlayerAdded(content/browser/media/media_web_contents_observer.cc:754-803) only checksRenderFrameHost::FromID(player_id.frame_routing_id)(which is non-null for cached frames). Subsequently, callingOnMediaPlaying()triggersMediaSessionImpl::AddPlayer(:454-542), which acquiresAudioFocusType::kGainaudio focus and adds the player tonormal_players_without lifecycle validation. -
Session Routing Hijack:
MediaSessionImpl::UpdateRoutedService(:1733-1747) callsComputeFrameForRouting(:1754-1795). Unlike the fallback loop (which is restricted toGetPrimaryFrameTree()), the player loop gathers candidate frames directly fromnormal_players_without checking whether the frame is active or part of the primary frame tree. Because the cached frame has depth 1 and an active service inservices_,ComputeFrameForRoutingselects it asbest_frame, settingrouted_service_to the attacker’s service. -
Origin Attribution Mismatch in System UI: When
RebuildAndNotifyMetadataChangedcallsBuildMetadata(:2034-2099), title, artist, album, and artwork are retrieved fromrouted_service_(attacker-controlled), whereassource_titleis derived fromweb_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin()(:2085-2095) (the active victim origin,victim.com). Artwork is downloaded viaDownloadImageInFrame(:1479-1496) from the cached attacker frame. -
Input Interception & Media Suppression: Hardware media keys and Global Media Controls dispatch
MediaSessionImpl::Suspend(SuspendType::kUI)(:731-745), which forwards toDidReceiveAction(:1688-1727). The action is routed to the attacker viarouted_service_->GetClient()->DidReceiveAction(...). The attacker can ignore pause actions, whileDidReceiveAction(kPause)force-pauses any non-routed players (:1708-1721) belonging to legitimate active media onvictim.com.
Potential Impact
- System UI Spoofing: Attackers can display arbitrary text, lock-screen notifications, and media cards (Global Media Controls, Windows SMTC, macOS Now Playing, Android Media Notification) labeled under a trusted active origin (e.g.,
victim.com). - Input Hijacking & DoS: Media key events and control interactions are diverted to a stale document; genuine playback initiated on the active page can be suppressed.
- Scope: Affects the browser process session state; requires a compromised renderer process.
Potential Reproduction Steps
(Note: These are potential steps based on static source-code analysis; analysis tooling does not currently execute live exploit chains.)
- Enable Back/Forward Cache (default in standard Chrome builds).
- Navigate the active tab to
https://attacker.com/. - Assume the renderer process for
attacker.comis compromised. - Navigate the tab from
https://attacker.com/tohttps://victim.com/(puttingattacker.com’sRenderFrameHostinto BFCache). - From the cached renderer process, send IPCs over the existing Mojo channels:
- Call
BrowserInterfaceBroker.GetInterface(blink.mojom.MediaSessionService). - On
MediaSessionService, callSetClient(),SetMetadata({title: 'Account Alert', artist: 'Victim Bank', ...}),SetPlaybackState(PLAYING), andEnableAction(kPause). - On
MediaPlayerHost, callOnMediaPlayerAdded(player_remote, observer_receiver, player_id). - On
MediaPlayerObserver, callOnMediaMetadataChanged(has_audio=true, kPersistent)followed byOnMediaPlaying().
- Call
- Observe in Global Media Controls and system media notification interfaces that the media session is displayed with title/artist/artwork supplied by
attacker.com, but with source origin attributed tovictim.com. - Trigger system media control actions (e.g., Pause) and observe delivery to the cached attacker document.
Suggested Fix
- In
MediaSessionServiceImpl::Create(content/browser/media/session/media_session_service_impl.cc), verifyrender_frame_host->IsActive()before creating the receiver. - In
MediaWebContentsObserver::OnMediaPlayerAddedandMediaSessionImpl::AddPlayer, reject or ignore requests whererender_frame_host->IsActive()is false. - In
MediaSessionImpl::ComputeFrameForRouting(content/browser/media/session/media_session_impl.cc), filter candidate frames innormal_players_to only include frames whereframe->IsActive()(or restrict traversal toGetPrimaryFrameTree()).
Evaluated with Chrome root at commit: 53c437ac4ea52cc89d0265eb9e6d68666fdb303e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.