Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Media
DescriptionInsufficient validation of untrusted input in Media
ComponentMedia
Bug ClassLogic Error
Tracker496617698
Fix commitc1f3553ce538 (chromium/src) +153/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
AudibleMetrics
content/browser/media/media_web_contents_observer.h
modified
WebContentsImpl
content/browser/media/media_web_contents_observer.h
modified
CONTENT_EXPORT
content/browser/media/media_web_contents_observer.h
modified
TEST_F
content/browser/media/media_web_contents_observer_unittest.cc
modified

Files Changed

  • content/browser/media/media_interface_proxy.cc
  • content/browser/media/media_web_contents_observer.cc
  • content/browser/media/media_web_contents_observer.h
  • content/browser/media/media_web_contents_observer_unittest.cc
From c1f3553ce5387e92b9e53c29a81729856e5d3399 Mon Sep 17 00:00:00 2001
From: Benjamin Keen <bkeen@google.com>
Date: Wed, 08 Apr 2026 17:27:43 -0700
Subject: [PATCH] Verify renderer authorization for browser audibility bypass

This change prevents a compromised renderer from spoofing tab audibility
by claiming to bypass the browser's audio service. Previously, the
browser process unconditionally trusted the renderer's
`OnUseAudioServiceChanged(false)` IPC. This allowed a compromised
renderer to trick the browser into marking a silent tab as "audible",
which satisfies the `WasRecentlyAudible()` gate for AutoPiP and also
displays a misleading speaker icon on the tab strip.

The validation is implemented by tracking whether a specific frame has
actually been granted permission to bypass the audio service. This
authorization is currently only granted to the MediaFoundationRenderer.

With this change, now the `MediaWebContentsObserver` verifies
authorization before allowing a bypass claim. This ensures only
authorized frames can register as audible, preventing compromised
renderers from triggering unauthorized AutoPiP windows.

Bug: 496617698
Change-Id: I9d40db07c4d715fe1e373e1ba9dbc0ecbc4c39d9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735911
Reviewed-by: Xiaohan Wang <xhwang@chromium.org>
Commit-Queue: Benjamin Keen <bkeen@google.com>
Cr-Commit-Position: refs/heads/main@{#1611887}
---

diff --git a/content/browser/media/media_interface_proxy.cc b/content/browser/media/media_interface_proxy.cc
index 57774b9..3b5ba29 100644
--- a/content/browser/media/media_interface_proxy.cc
+++ b/content/browser/media/media_interface_proxy.cc
@@ -19,6 +19,7 @@
 #include "base/unguessable_token.h"
 #include "build/build_config.h"
 #include "content/browser/media/cdm_storage_common.h"
+#include "content/browser/media/media_web_contents_observer.h"
 #include "content/browser/renderer_host/render_frame_host_delegate.h"
 #include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/public/browser/media_service.h"
@@ -373,6 +374,11 @@
     factory->CreateMediaFoundationRenderer(
         std::move(media_log_remote), std::move(receiver),
         std::move(renderer_extension_receiver));
+
+    // `MediaFoundationRenderer` bypasses the browser's audio service.
+    // Authorize the frame for audibility bypass claims.
+    AudibilityBypassAuthorization::GetOrCreateForCurrentDocument(
+        &render_frame_host());
   }
 }
 #endif  // BUILDFLAG(IS_WIN)
diff --git a/content/browser/media/media_web_contents_observer.cc b/content/browser/media/media_web_contents_observer.cc
index 291d9ea2..202ffa5 100644
--- a/content/browser/media/media_web_contents_observer.cc
+++ b/content/browser/media/media_web_contents_observer.cc
@@ -567,7 +567,14 @@
   if (!player_info)
     return;
 
-  bool should_add_client = player_info->IsAudible() && !uses_audio_service_;
+  // Register as an audible client if the player is audible and bypasses the
+  // standard audio service (currently only for `MediaFoundationRenderer`).
+  // This requires explicit browser-side authorization to prevent spoofing.
+  bool should_add_client =
+      player_info->IsAudible() && !uses_audio_service_ &&
+      AudibilityBypassAuthorization::IsAuthorized(
+          RenderFrameHost::FromID(media_player_id_.frame_routing_id));
+
   auto* audio_stream_monitor =
       media_web_contents_observer_->web_contents_impl()->audio_stream_monitor();
 
@@ -807,4 +814,17 @@
   return result.first->second->GetWeakPtr();
 }
 
+AudibilityBypassAuthorization::AudibilityBypassAuthorization(
+    RenderFrameHost* rfh)
+    : DocumentUserData<AudibilityBypassAuthorization>(rfh) {}
+
+AudibilityBypassAuthorization::~AudibilityBypassAuthorization() = default;
+
+// static
+bool AudibilityBypassAuthorization::IsAuthorized(RenderFrameHost* rfh) {
+  return rfh && GetForCurrentDocument(rfh) != nullptr;
+}
+
+DOCUMENT_USER_DATA_KEY_IMPL(AudibilityBypassAuthorization);
+
 }  // namespace content
diff --git a/content/browser/media/media_web_contents_observer.h b/content/browser/media/media_web_contents_observer.h
index c7f9d7a..f9b8da6 100644
--- a/content/browser/media/media_web_contents_observer.h
+++ b/content/browser/media/media_web_contents_observer.h
@@ -20,6 +20,7 @@
 #include "content/browser/media/media_power_experiment_manager.h"
 #include "content/browser/media/session/media_session_controllers_manager.h"
 #include "content/common/content_export.h"
+#include "content/public/browser/document_user_data.h"
 #include "content/public/browser/global_routing_id.h"
 #include "content/public/browser/media_player_id.h"
 #include "content/public/browser/render_frame_host.h"
@@ -59,6 +60,26 @@
 class AudibleMetrics;
 class WebContentsImpl;
 
+// Used to authorize a frame to bypass the browser's audio service for
+// audibility, when using `MediaFoundationRenderer`. This is stored as
+// `DocumentUserData` on the `RenderFrameHost`.
+class CONTENT_EXPORT AudibilityBypassAuthorization
+    : public DocumentUserData<AudibilityBypassAuthorization> {
+ public:
+  ~AudibilityBypassAuthorization() override;
+
+  // Returns true if the given `rfh` has been authorized to bypass the
+  // browser's audio service for audibility. This is used for renderers that
+  // handle their own audio output, currently only `MediaFoundationRenderer` on
+  // Windows.
+  static bool IsAuthorized(RenderFrameHost* rfh);
+
+ private:
+  friend class DocumentUserData<AudibilityBypassAuthorization>;
+  explicit AudibilityBypassAuthorization(RenderFrameHost* rfh);
+  DOCUMENT_USER_DATA_KEY_DECL();
+};
+
 // This class manages all RenderFrame based media related managers at the
 // browser side. It receives IPC messages from media RenderFrameObservers and
 // forwards them to the corresponding managers. The managers are responsible
diff --git a/content/browser/media/media_web_contents_observer_unittest.cc b/content/browser/media/media_web_contents_observer_unittest.cc
index 10b9d3c..ab6e648 100644
--- a/content/browser/media/media_web_contents_observer_unittest.cc
+++ b/content/browser/media/media_web_contents_observer_unittest.cc
@@ -77,6 +77,13 @@
     return setup;
   }
 
+  // Overload for tests that do not care about specific IDs.
+  auto CreateAndAddPlayer(
+      mojo::AssociatedRemote<media::mojom::MediaPlayerHost>& player_host)
+      -> PlayerSetup {
+    return CreateAndAddPlayer(player_host, next_player_id_++);
+  }
+
   void SetMediaMetadata(
       mojo::AssociatedRemote<media::mojom::MediaPlayerObserver>& observer,
       bool has_audio,
@@ -105,6 +112,15 @@
     observer.FlushForTesting();
   }
 
+  void SetUseAudioService(
+      mojo::AssociatedRemote<media::mojom::MediaPlayerObserver>& observer,
+      bool uses_audio_service) {
+    observer->OnUseAudioServiceChanged(uses_audio_service);
+    observer.FlushForTesting();
+  }
+
+  bool IsWebContentsAudible() { return contents()->IsCurrentlyAudible(); }
+
   MediaPlayerId CreatePlayerId(int32_t player_id) {
     return MediaPlayerId(contents()->GetPrimaryMainFrame()->GetGlobalId(),
                          player_id);
@@ -113,6 +129,9 @@
   MediaWebContentsObserver& media_web_contents_observer() {
     return *contents()->media_web_contents_observer();
   }
+
+ private:
+  int32_t next_player_id_ = 0;
 };
 
 TEST_F(MediaWebContentsObserverTest, GetCurrentlyPlayingVideoCount) {
@@ -308,5 +327,91 @@
   EXPECT_FALSE(media_web_contents_observer().IsPlayerActive(player_id));
 }
 
+TEST_F(MediaWebContentsObserverTest, StandardPlaybackNoAuthorization) {
+  auto player_host = SetupPlayerHost();
+  auto player = CreateAndAddPlayer(player_host);
+
+  SetMediaMetadata(player.observer, /*has_audio=*/true, /*has_video=*/false);
+  SetUseAudioService(player.observer, true);
+  PlayMedia(player.observer);
+
+  // Verify that standard playback (via audio service) does not trigger
+  // `RegisterAudibleClient`, so the `WebContents` remains non-audible.
+  EXPECT_FALSE(IsWebContentsAudible());
+}
+
+TEST_F(MediaWebContentsObserverTest, UnauthorizedBypassDenied) {
+  auto player_host = SetupPlayerHost();
+  auto player = CreateAndAddPlayer(player_host);
+
+  SetMediaMetadata(player.observer, /*has_audio=*/true, /*has_video=*/false);
+
+  // Renderer claims it's audible but bypassing the audio service.
+  // Without a `MediaFoundationRenderer`, this should be rejected.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/media/media_web_contents_observer_unittest.cc b/content/browser/media/media_web_contents_observer_unittest.cc
index 10b9d3c..ab6e648 100644
--- a/content/browser/media/media_web_contents_observer_unittest.cc
+++ b/content/browser/media/media_web_contents_observer_unittest.cc
@@ -77,6 +77,13 @@
     return setup;
   }
 
+  // Overload for tests that do not care about specific IDs.
+  auto CreateAndAddPlayer(
+      mojo::AssociatedRemote<media::mojom::MediaPlayerHost>& player_host)
+      -> PlayerSetup {
+    return CreateAndAddPlayer(player_host, next_player_id_++);
+  }
+
   void SetMediaMetadata(
       mojo::AssociatedRemote<media::mojom::MediaPlayerObserver>& observer,
       bool has_audio,
@@ -105,6 +112,15 @@
     observer.FlushForTesting();
   }
 
+  void SetUseAudioService(
+      mojo::AssociatedRemote<media::mojom::MediaPlayerObserver>& observer,
+      bool uses_audio_service) {
+    observer->OnUseAudioServiceChanged(uses_audio_service);
+    observer.FlushForTesting();
+  }
+
+  bool IsWebContentsAudible() { return contents()->IsCurrentlyAudible(); }
+
   MediaPlayerId CreatePlayerId(int32_t player_id) {
     return MediaPlayerId(contents()->GetPrimaryMainFrame()->GetGlobalId(),
                          player_id);
@@ -113,6 +129,9 @@
   MediaWebContentsObserver& media_web_contents_observer() {
     return *contents()->media_web_contents_observer();
   }
+
+ private:
+  int32_t next_player_id_ = 0;
 };
 
 TEST_F(MediaWebContentsObserverTest, GetCurrentlyPlayingVideoCount) {
@@ -308,5 +327,91 @@
   EXPECT_FALSE(media_web_contents_observer().IsPlayerActive(player_id));
 }
 
+TEST_F(MediaWebContentsObserverTest, StandardPlaybackNoAuthorization) {
+  auto player_host = SetupPlayerHost();
+  auto player = CreateAndAddPlayer(player_host);
+
+  SetMediaMetadata(player.observer, /*has_audio=*/true, /*has_video=*/false);
+  SetUseAudioService(player.observer, true);
+  PlayMedia(player.observer);
+
+  // Verify that standard playback (via audio service) does not trigger
+  // `RegisterAudibleClient`, so the `WebContents` remains non-audible.
+  EXPECT_FALSE(IsWebContentsAudible());
+}
+
+TEST_F(MediaWebContentsObserverTest, UnauthorizedBypassDenied) {
+  auto player_host = SetupPlayerHost();
+  auto player = CreateAndAddPlayer(player_host);
+
+  SetMediaMetadata(player.observer, /*has_audio=*/true, /*has_video=*/false);
+
+  // Renderer claims it's audible but bypassing the audio service.
+  // Without a `MediaFoundationRenderer`, this should be rejected.
+  SetUseAudioService(player.observer, false);
+  PlayMedia(player.observer);
+
+  EXPECT_FALSE(IsWebContentsAudible());
+}
+
+TEST_F(MediaWebContentsObserverTest, AuthorizedBypassAllowed) {
+  auto player_host = SetupPlayerHost();
+  auto player = CreateAndAddPlayer(player_host);
+
+  // Simmulate audibility bypass authorization for the document.
+  AudibilityBypassAuthorization::GetOrCreateForCurrentDocument(
+      contents()->GetPrimaryMainFrame());
+
+  SetMediaMetadata(player.observer, /*has_audio=*/true, /*has_video=*/false);
+
+  // Renderer claims it's audible but bypassing the audio service.
+  // Since we authorized it, this should be allowed.
+  SetUseAudioService(player.observer, false);
+  PlayMedia(player.observer);
+
+  EXPECT_TRUE(IsWebContentsAudible());
+}
+
+TEST_F(MediaWebContentsObserverTest, AuthorizationIsFrameScoped) {
+  auto main_player_host = SetupPlayerHost();
+  auto main_player = CreateAndAddPlayer(main_player_host);
+
+  // Create a child frame.
+  RenderFrameHost* main_rfh = contents()->GetPrimaryMainFrame();
+  RenderFrameHostTester::For(main_rfh)->InitializeRenderFrameIfNeeded();
+  RenderFrameHost* child_rfh =
+      RenderFrameHostTester::For(main_rfh)->AppendChild("child");
+  ASSERT_NE(child_rfh, nullptr);
+  RenderFrameHostTester::For(child_rfh)->InitializeRenderFrameIfNeeded();
+
+  mojo::AssociatedRemote<media::mojom::MediaPlayerHost> child_player_host;
+  contents()->media_web_contents_observer()->BindMediaPlayerHost(
+      child_rfh->GetGlobalId(),
+      child_player_host.BindNewEndpointAndPassDedicatedReceiver());
+
+  auto child_player = CreateAndAddPlayer(child_player_host);
+
+  // Authorize only the main frame.
+  AudibilityBypassAuthorization::GetOrCreateForCurrentDocument(main_rfh);
+
+  // Child frame attempts bypass.
+  SetMediaMetadata(child_player.observer, /*has_audio=*/true,
+                   /*has_video=*/false);
+  SetUseAudioService(child_player.observer, false);
+  PlayMedia(child_player.observer);
+
+  // Should be denied because authorization is not inherited.
+  EXPECT_FALSE(IsWebContentsAudible());
+
+  // Main frame attempts bypass.
+  SetMediaMetadata(main_player.observer, /*has_audio=*/true,
+                   /*has_video=*/false);
+  SetUseAudioService(main_player.observer, false);
+  PlayMedia(main_player.observer);
+
+  // Should be allowed.
+  EXPECT_TRUE(IsWebContentsAudible());
+}
+
 }  // namespace
 }  // namespace content
Loading diff…

Original Bug Report

reported by vm...@google.com

Renderer can spoof tab audibility to bypass Auto-PiP gate via OnUseAudioServiceChanged

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can spoof the audibility state of a tab by sending an unvalidated OnUseAudioServiceChanged IPC to the browser process. This bypasses the actual audio power monitoring, causing the browser to incorrectly register the tab as audible. This spoofed state satisfies the WasRecentlyAudible check, allowing the renderer to trigger unauthorized Auto-Picture-in-Picture windows.

Affected files:

  • content/browser/media/media_web_contents_observer.cc
  • media/mojo/mojom/media_player.mojom
  • chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.cc
  • content/browser/media/audio_stream_monitor.cc
  • chrome/browser/ui/recently_audible_helper.cc

Estimated timestamp from git blame: 2023-04-13

Background

In Chromium, the browser process normally monitors actual audio output from a tab via the Audio Service to determine if it is “audible.” This audibility state is used for various security and UI purposes, including the “Recently Audible” gate for Auto-Picture-in-Picture (Auto-PiP) and the audio indicator (speaker icon) in the tab strip. However, for certain media players (such as Windows MediaFoundation), audio is played outside the standard browser audio service. To support this, the browser allows the renderer to signal that it is not using the audio service, in which case the browser trusts the renderer’s reported metadata to determine audibility.

The Vulnerability

The MediaPlayerObserver::OnUseAudioServiceChanged(bool uses_audio_service) IPC (defined in media/mojo/mojom/media_player.mojom) is handled by MediaWebContentsObserver::MediaPlayerObserverHostImpl in the browser process. This IPC sets the uses_audio_service_ flag without any browser-side validation to confirm if the renderer is actually authorized to bypass the Audio Service.

In content/browser/media/media_web_contents_observer.cc:

void MediaWebContentsObserver::MediaPlayerObserverHostImpl::OnUseAudioServiceChanged(bool uses_audio_service) {
  uses_audio_service_ = uses_audio_service;
  NotifyAudioStreamMonitorIfNeeded();
}

When uses_audio_service_ is set to false, NotifyAudioStreamMonitorIfNeeded calculates the audibility client state based purely on renderer-controlled properties:

bool should_add_client = player_info->IsAudible() && !uses_audio_service_;

Where player_info->IsAudible() is defined as:

bool IsAudible() const { return has_audio_ && is_playing_ && !muted_; }

Since has_audio_, is_playing_, and muted_ are all set via unvalidated renderer IPCs (OnMediaMetadataChanged, OnMediaPlaying, and OnMutedStatusChanged), a compromised renderer can trick the browser into explicitly registering the player as an “audible client” in AudioStreamMonitor even if it is producing no sound.

Impact

  1. Auto-PiP Bypass: The AutoPictureInPictureTabHelper::MeetsVideoPlaybackConditions check in chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.cc requires that WasRecentlyAudible() is true. By spoofing audibility, a compromised renderer can bypass this security gate. If the user is on an origin with high Media Engagement or has allowed Auto-PiP, an attacker can force an always-on-top Picture-in-Picture window (e.g., Document PiP) to open with arbitrary content when the tab is backgrounded. This persistent, always-on-top window can be used for sophisticated phishing attacks.

  2. UI Spoofing: The spoofed audibility state causes the audio indicator (speaker icon) to appear in the tab strip for the silent tab, which is a standalone UI spoofing impact.

Potential Reproduction Steps

Note: Fortify LLM agent does not have the ability to run code, so these are suggested steps based on static analysis.

  1. Use a compromised renderer on an HTTPS origin with high Media Engagement (e.g., youtube.com) or where the user has explicitly allowed Auto-PiP.
  2. Call MediaPlayerHost::OnMediaPlayerAdded() to register a player from the primary main frame.
  3. Send MediaPlayerObserver::OnMediaMetadataChanged(has_audio=true, has_video=true, ...) to set has_audio_ to true.
  4. Send OnMutedStatusChanged(false) to set muted_ to false.
  5. Send OnUseAudioServiceChanged(false) to set uses_audio_service_ to false.
  6. Send OnMediaPlaying() to set is_playing_ to true and acquire system audio focus.
  7. This sequence triggers NotifyAudioStreamMonitorIfNeeded(), which registers the tab as an audible client in the browser process.
  8. Observe the speaker icon appears in the tab strip (UI Spoofing).
  9. Switch to another tab or minimize the window; an Auto-PiP window will be triggered because the WasRecentlyAudible() check is bypassed.

Suggested Fix

Do not unconditionally trust the OnUseAudioServiceChanged IPC from the renderer. The browser should either:

  1. Validate that the specific renderer/origin is authorized to use media players that bypass the Audio Service (e.g., only specific Windows MediaFoundation configurations).
  2. For security gates like Auto-PiP, require actual audio power level monitoring to confirm audibility, regardless of the uses_audio_service_ flag. If a player bypasses the Audio Service, it should not be eligible to bypass the WasRecentlyAudible() requirement for Auto-PiP.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker