Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse of released resource in Session
DescriptionUse of released resource in Session
ComponentSession
Sub-componentInternals>Media>Session
Bug ClassLogic Error
Tracker556258544
Fix commite5b12422e3ce (chromium/src) +110/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
content/browser/media/session/media_session_impl.cc
modified
for
content/browser/media/session/media_session_impl.cc
modified
TEST_F
content/browser/media/session/media_session_impl_service_routing_unittest.cc
modified
MediaSessionImplServiceRoutingFencedFrameTest
content/browser/media/session/media_session_impl_service_routing_unittest.cc
modified

Files Changed

  • content/browser/media/session/media_session_impl_browsertest.cc
  • content/browser/media/session/media_session_impl.cc
  • content/browser/media/session/media_session_impl_service_routing_unittest.cc
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:
Loading diff…

Regression Test / PoC

shipped with the fix
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:
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • content/browser/media/session/media_session_service_impl.cc
  • content/browser/media/media_web_contents_observer.cc
  • content/browser/media/session/media_session_controller.cc
  • content/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

  1. Ungated Interface Binding: In content/browser/browser_interface_binders.cc:1393, MediaSessionService is bound without lifecycle wrappers:

    map->Add<blink::mojom::MediaSessionService>(&MediaSessionServiceImpl::Create);
    

    In content/browser/media/session/media_session_service_impl.cc:39-46, MediaSessionServiceImpl::Create does not check render_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 into MediaSessionImpl::services_[rfh_id] (content/browser/media/session/media_session_impl.cc:1639-1644).

  2. 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 over media::mojom::MediaPlayerHost, MediaWebContentsObserver::OnMediaPlayerAdded (content/browser/media/media_web_contents_observer.cc:754-803) only checks RenderFrameHost::FromID(player_id.frame_routing_id) (which is non-null for cached frames). Subsequently, calling OnMediaPlaying() triggers MediaSessionImpl::AddPlayer (:454-542), which acquires AudioFocusType::kGain audio focus and adds the player to normal_players_ without lifecycle validation.

  3. Session Routing Hijack: MediaSessionImpl::UpdateRoutedService (:1733-1747) calls ComputeFrameForRouting (:1754-1795). Unlike the fallback loop (which is restricted to GetPrimaryFrameTree()), the player loop gathers candidate frames directly from normal_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 in services_, ComputeFrameForRouting selects it as best_frame, setting routed_service_ to the attacker’s service.

  4. Origin Attribution Mismatch in System UI: When RebuildAndNotifyMetadataChanged calls BuildMetadata (:2034-2099), title, artist, album, and artwork are retrieved from routed_service_ (attacker-controlled), whereas source_title is derived from web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin() (:2085-2095) (the active victim origin, victim.com). Artwork is downloaded via DownloadImageInFrame (:1479-1496) from the cached attacker frame.

  5. Input Interception & Media Suppression: Hardware media keys and Global Media Controls dispatch MediaSessionImpl::Suspend(SuspendType::kUI) (:731-745), which forwards to DidReceiveAction (:1688-1727). The action is routed to the attacker via routed_service_->GetClient()->DidReceiveAction(...). The attacker can ignore pause actions, while DidReceiveAction(kPause) force-pauses any non-routed players (:1708-1721) belonging to legitimate active media on victim.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.)

  1. Enable Back/Forward Cache (default in standard Chrome builds).
  2. Navigate the active tab to https://attacker.com/.
  3. Assume the renderer process for attacker.com is compromised.
  4. Navigate the tab from https://attacker.com/ to https://victim.com/ (putting attacker.com’s RenderFrameHost into BFCache).
  5. From the cached renderer process, send IPCs over the existing Mojo channels:
    • Call BrowserInterfaceBroker.GetInterface(blink.mojom.MediaSessionService).
    • On MediaSessionService, call SetClient(), SetMetadata({title: 'Account Alert', artist: 'Victim Bank', ...}), SetPlaybackState(PLAYING), and EnableAction(kPause).
    • On MediaPlayerHost, call OnMediaPlayerAdded(player_remote, observer_receiver, player_id).
    • On MediaPlayerObserver, call OnMediaMetadataChanged(has_audio=true, kPersistent) followed by OnMediaPlaying().
  6. 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 to victim.com.
  7. Trigger system media control actions (e.g., Pause) and observe delivery to the cached attacker document.

Suggested Fix

  1. In MediaSessionServiceImpl::Create (content/browser/media/session/media_session_service_impl.cc), verify render_frame_host->IsActive() before creating the receiver.
  2. In MediaWebContentsObserver::OnMediaPlayerAdded and MediaSessionImpl::AddPlayer, reject or ignore requests where render_frame_host->IsActive() is false.
  3. In MediaSessionImpl::ComputeFrameForRouting (content/browser/media/session/media_session_impl.cc), filter candidate frames in normal_players_ to only include frames where frame->IsActive() (or restrict traversal to GetPrimaryFrameTree()).

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.

View on issue tracker