CVE-2026-11237
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
AudibleMetricscontent/browser/media/media_web_contents_observer.h |
modified | |
WebContentsImplcontent/browser/media/media_web_contents_observer.h |
modified | |
CONTENT_EXPORTcontent/browser/media/media_web_contents_observer.h |
modified | |
TEST_Fcontent/browser/media/media_web_contents_observer_unittest.cc |
modified |
Files Changed
content/browser/media/media_interface_proxy.cccontent/browser/media/media_web_contents_observer.cccontent/browser/media/media_web_contents_observer.hcontent/browser/media/media_web_contents_observer_unittest.cc
Patch
From c1f3553ce5387e92b9e53c29a81729856e5d3399 Mon Sep 17 00:00:00 2001 From: Benjamin Keen <[email protected]> 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 <[email protected]> Commit-Queue: Benjamin Keen <[email protected]> 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.
Regression Test / PoC
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
Original Bug Report
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.ccmedia/mojo/mojom/media_player.mojomchrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.cccontent/browser/media/audio_stream_monitor.ccchrome/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
-
Auto-PiP Bypass: The
AutoPictureInPictureTabHelper::MeetsVideoPlaybackConditionscheck inchrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.ccrequires thatWasRecentlyAudible()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. -
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.
- 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.
- Call
MediaPlayerHost::OnMediaPlayerAdded()to register a player from the primary main frame. - Send
MediaPlayerObserver::OnMediaMetadataChanged(has_audio=true, has_video=true, ...)to sethas_audio_to true. - Send
OnMutedStatusChanged(false)to setmuted_to false. - Send
OnUseAudioServiceChanged(false)to setuses_audio_service_to false. - Send
OnMediaPlaying()to setis_playing_to true and acquire system audio focus. - This sequence triggers
NotifyAudioStreamMonitorIfNeeded(), which registers the tab as an audible client in the browser process. - Observe the speaker icon appears in the tab strip (UI Spoofing).
- 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:
- Validate that the specific renderer/origin is authorized to use media players that bypass the Audio Service (e.g., only specific Windows MediaFoundation configurations).
- 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 theWasRecentlyAudible()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.