CVE-2026-13931
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/media/media_web_contents_observer.cc |
modified | |
CONTENT_EXPORTcontent/browser/media/media_web_contents_observer.h |
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 fb8e546e4990b104e5bb605fbc15a986d58bed54 Mon Sep 17 00:00:00 2001
From: Sangbaek Park <sangbaekpark@google.com>
Date: Mon, 01 Jun 2026 15:26:56 -0700
Subject: [PATCH] media: Fix AudibilityBypassAuthorization surviving player disconnect
This CL addresses a security issue where a compromised renderer
could spoof its audibility status by reusing a media foundation
audibility bypass grant. Previously, the authorization was granted
per-document and survived the disconnection of the original media
player.
The fix introduces an AudibilityBypassTracker that ties the
bypass grant to a specific MediaPlayerId. When a new
MediaFoundationRenderer is created, a pending grant is added.
The first player claiming a bypass consumes the grant, making
it impossible for subsequent spoofed players to reuse it. The
grant is cleared when the authorized player disconnects.
Tests: { MediaWebContentsObserverTest.AudibilityBypass* }
Bug: 512997441, 512636302
Change-Id: If7eb3ed525c75911e86f1c3bdb3e281f329dbcfc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7884211
Commit-Queue: Sangbaek Park <sangbaekpark@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1639727}
---
diff --git a/content/browser/media/media_interface_proxy.cc b/content/browser/media/media_interface_proxy.cc
index 1cad82a3..b7d670b 100644
--- a/content/browser/media/media_interface_proxy.cc
+++ b/content/browser/media/media_interface_proxy.cc
@@ -445,8 +445,7 @@
// `MediaFoundationRenderer` bypasses the browser's audio service.
// Authorize the frame for audibility bypass claims.
- AudibilityBypassAuthorization::GetOrCreateForCurrentDocument(
- &render_frame_host());
+ AudibilityBypassTracker::AddGrant(&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 202ffa5..2b7eba1 100644
--- a/content/browser/media/media_web_contents_observer.cc
+++ b/content/browser/media/media_web_contents_observer.cc
@@ -6,6 +6,7 @@
#include <algorithm>
#include <memory>
+#include <set>
#include <tuple>
#include "base/debug/crash_logging.h"
@@ -572,8 +573,7 @@
// 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));
+ AudibilityBypassTracker::ClaimGrant(media_player_id_);
auto* audio_stream_monitor =
media_web_contents_observer_->web_contents_impl()->audio_stream_monitor();
@@ -773,6 +773,7 @@
}
observer->media_player_remotes_.erase(player_id);
observer->session_controllers_manager_->OnEnd(player_id);
+ AudibilityBypassTracker::ReleaseGrant(player_id);
if (observer->fullscreen_player_ &&
*observer->fullscreen_player_ == player_id) {
observer->fullscreen_player_.reset();
@@ -814,17 +815,48 @@
return result.first->second->GetWeakPtr();
}
-AudibilityBypassAuthorization::AudibilityBypassAuthorization(
- RenderFrameHost* rfh)
- : DocumentUserData<AudibilityBypassAuthorization>(rfh) {}
+AudibilityBypassTracker::AudibilityBypassTracker(RenderFrameHost* rfh)
+ : DocumentUserData<AudibilityBypassTracker>(rfh) {}
-AudibilityBypassAuthorization::~AudibilityBypassAuthorization() = default;
+AudibilityBypassTracker::~AudibilityBypassTracker() = default;
// static
-bool AudibilityBypassAuthorization::IsAuthorized(RenderFrameHost* rfh) {
- return rfh && GetForCurrentDocument(rfh) != nullptr;
+void AudibilityBypassTracker::AddGrant(RenderFrameHost* rfh) {
+ if (rfh) {
+ GetOrCreateForCurrentDocument(rfh)->pending_grants_++;
+ }
}
-DOCUMENT_USER_DATA_KEY_IMPL(AudibilityBypassAuthorization);
+// static
+bool AudibilityBypassTracker::ClaimGrant(const MediaPlayerId& id) {
+ auto* rfh = RenderFrameHost::FromID(id.frame_routing_id);
+ if (!rfh) {
+ return false;
+ }
+ auto* tracker = GetForCurrentDocument(rfh);
+ if (tracker && tracker->authorized_players_.contains(id)) {
+ return true;
+ }
+ if (tracker && tracker->pending_grants_ > 0) {
+ tracker->pending_grants_--;
+ tracker->authorized_players_.insert(id);
+ return true;
+ }
+ return false;
+}
+
+// static
+void AudibilityBypassTracker::ReleaseGrant(const MediaPlayerId& id) {
+ auto* rfh = RenderFrameHost::FromID(id.frame_routing_id);
+ if (!rfh) {
+ return;
+ }
+ auto* tracker = GetForCurrentDocument(rfh);
+ if (tracker) {
+ tracker->authorized_players_.erase(id);
+ }
+}
+
+DOCUMENT_USER_DATA_KEY_IMPL(AudibilityBypassTracker);
} // namespace content
diff --git a/content/browser/media/media_web_contents_observer.h b/content/browser/media/media_web_contents_observer.h
index 6d690dcc..c00bd80 100644
--- a/content/browser/media/media_web_contents_observer.h
+++ b/content/browser/media/media_web_contents_observer.h
@@ -12,6 +12,7 @@
#include <optional>
#include "base/containers/flat_map.h"
+#include "base/containers/flat_set.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "build/build_config.h"
@@ -62,22 +63,25 @@
// 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> {
+// `DocumentUserData` on the `RenderFrameHost`. The authorization is now tied to
+// a specific player instance and is no longer document-wide for the lifetime of
+// the document.
+class CONTENT_EXPORT AudibilityBypassTracker
+ : public DocumentUserData<AudibilityBypassTracker> {
public:
- ~AudibilityBypassAuthorization() override;
+ ~AudibilityBypassTracker() 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);
+ static void AddGrant(RenderFrameHost* rfh);
+ static bool ClaimGrant(const MediaPlayerId& id);
+ static void ReleaseGrant(const MediaPlayerId& id);
private:
- friend class DocumentUserData<AudibilityBypassAuthorization>;
- explicit AudibilityBypassAuthorization(RenderFrameHost* rfh);
+ friend class DocumentUserData<AudibilityBypassTracker>;
+ explicit AudibilityBypassTracker(RenderFrameHost* rfh);
DOCUMENT_USER_DATA_KEY_DECL();
+
+ size_t pending_grants_ = 0;
+ base::flat_set<MediaPlayerId> authorized_players_;
};
// This class manages all RenderFrame based media related managers at the
diff --git a/content/browser/media/media_web_contents_observer_unittest.cc b/content/browser/media/media_web_contents_observer_unittest.cc
index ab6e648..558b67f 100644
--- a/content/browser/media/media_web_contents_observer_unittest.cc
+++ b/content/browser/media/media_web_contents_observer_unittest.cc
@@ -6,6 +6,7 @@
#include <memory>
+#include "base/run_loop.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "media/base/media_content_type.h"
@@ -358,9 +359,8 @@
auto player_host = SetupPlayerHost();
auto player = CreateAndAddPlayer(player_host);
- // Simmulate audibility bypass authorization for the document.
- AudibilityBypassAuthorization::GetOrCreateForCurrentDocument(
- contents()->GetPrimaryMainFrame());
+ // Simulate audibility bypass authorization for the document.
+ AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
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 ab6e648..558b67f 100644
--- a/content/browser/media/media_web_contents_observer_unittest.cc
+++ b/content/browser/media/media_web_contents_observer_unittest.cc
@@ -6,6 +6,7 @@
#include <memory>
+#include "base/run_loop.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "media/base/media_content_type.h"
@@ -358,9 +359,8 @@
auto player_host = SetupPlayerHost();
auto player = CreateAndAddPlayer(player_host);
- // Simmulate audibility bypass authorization for the document.
- AudibilityBypassAuthorization::GetOrCreateForCurrentDocument(
- contents()->GetPrimaryMainFrame());
+ // Simulate audibility bypass authorization for the document.
+ AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
SetMediaMetadata(player.observer, /*has_audio=*/true, /*has_video=*/false);
@@ -392,7 +392,7 @@
auto child_player = CreateAndAddPlayer(child_player_host);
// Authorize only the main frame.
- AudibilityBypassAuthorization::GetOrCreateForCurrentDocument(main_rfh);
+ AudibilityBypassTracker::AddGrant(main_rfh);
// Child frame attempts bypass.
SetMediaMetadata(child_player.observer, /*has_audio=*/true,
@@ -413,5 +413,78 @@
EXPECT_TRUE(IsWebContentsAudible());
}
+TEST_F(MediaWebContentsObserverTest,
+ AudibilityBypassAuthorizationTiedToSpecificPlayer) {
+ auto player_host = SetupPlayerHost();
+ auto original_player = CreateAndAddPlayer(player_host);
+
+ // Simulate MediaFoundationRenderer creation grant.
+ AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
+
+ // Original player claims the grant successfully.
+ SetMediaMetadata(original_player.observer, true, false);
+ SetUseAudioService(original_player.observer, false);
+ PlayMedia(original_player.observer);
+
+ EXPECT_TRUE(IsWebContentsAudible());
+
+ // Original player disconnects, relinquishing its grant/authority.
+ original_player.observer.reset();
+ original_player.player.reset();
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(IsWebContentsAudible());
+
+ // A later spoofed player attempts to claim a bypass using the original
+ // document context.
+ auto later_player = CreateAndAddPlayer(player_host);
+ SetMediaMetadata(later_player.observer, true, false);
+ SetUseAudioService(later_player.observer, false);
+ PlayMedia(later_player.observer);
+
+ // The later player should not be able to reuse the audibility bypass grant.
+ EXPECT_FALSE(IsWebContentsAudible());
+}
+
+// Ensure the tracking counter mechanism works when multiple grants are given
+// and consumed.
+TEST_F(MediaWebContentsObserverTest,
+ AudibilityBypassAuthorizationMultipleGrants) {
+ auto player_host = SetupPlayerHost();
+
+ // Simulate two MediaFoundationRenderer creation grants.
+ AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
+ AudibilityBypassTracker::AddGrant(contents()->GetPrimaryMainFrame());
+
+ auto player1 = CreateAndAddPlayer(player_host);
+ auto player2 = CreateAndAddPlayer(player_host);
+ auto player3 = CreateAndAddPlayer(player_host);
+
+ // Player 1 claims the first grant successfully.
+ SetMediaMetadata(player1.observer, true, false);
+ SetUseAudioService(player1.observer, false);
+ PlayMedia(player1.observer);
+ EXPECT_TRUE(IsWebContentsAudible());
+
+ // Pause Player 1 so we can verify Player 2 independently.
+ PauseMedia(player1.observer);
+ EXPECT_FALSE(IsWebContentsAudible());
+
+ // Player 2 claims the second grant successfully.
+ SetMediaMetadata(player2.observer, true, false);
+ SetUseAudioService(player2.observer, false);
+ PlayMedia(player2.observer);
+ EXPECT_TRUE(IsWebContentsAudible());
+
+ PauseMedia(player2.observer);
+ EXPECT_FALSE(IsWebContentsAudible());
+
+ // Player 3 attempts to claim a grant, but none are left.
+ SetMediaMetadata(player3.observer, true, false);
+ SetUseAudioService(player3.observer, false);
+ PlayMedia(player3.observer);
+ EXPECT_FALSE(IsWebContentsAudible());
+}
+
} // namespace
} // namespace content
Original Bug Report
Potential bypass of AudibilityBypassAuthorization via CreateMediaFoundationRenderer on Windows
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic flaw in Chromium’s media handling on Windows allows a compromised renderer to bypass the AudibilityBypassAuthorization security mechanism. This allows an attacker to spoof the tab’s audibility status, potentially bypassing security gates for features like Automatic Picture-in-Picture.
Affected files:
content/browser/media/media_interface_proxy.cccontent/browser/media/media_web_contents_observer.cccontent/browser/browser_interface_binders.cccontent/browser/renderer_host/render_frame_host_impl_interface_binders.cc
Estimated timestamp from git blame: 2026-04-08
Summary
A logic vulnerability exists in the implementation of AudibilityBypassAuthorization on Windows. This mechanism was introduced to prevent compromised renderers from spoofing tab audibility when using media engines that bypass the browser’s standard audio service (e.g., MediaFoundationRenderer). However, the authorization is granted unconditionally to a document whenever the CreateMediaFoundationRenderer IPC is invoked, allowing a renderer to authorize itself and subsequently send spoofed audibility claims.
Vulnerability Details
The AudibilityBypassAuthorization is a DocumentUserData marker used to verify if a frame is permitted to claim audibility while bypassing the audio service. This check occurs in MediaWebContentsObserver::MediaPlayerObserverHostImpl::NotifyAudioStreamMonitorIfNeeded to ensure that renderer-provided audibility claims are authorized by the browser.
The vulnerability is located in MediaInterfaceProxy::CreateMediaFoundationRenderer within content/browser/media/media_interface_proxy.cc. When a renderer requests the creation of a MediaFoundationRenderer, the browser process unconditionally grants authorization to the document:
void MediaInterfaceProxy::CreateMediaFoundationRenderer(...) {
...
auto* factory = GetMediaFoundationServiceInterfaceFactory(base::FilePath());
if (factory) {
factory->CreateMediaFoundationRenderer(...);
// Authorize the frame for audibility bypass claims.
AudibilityBypassAuthorization::GetOrCreateForCurrentDocument(&render_frame_host());
}
}
On supported Windows systems (Windows 10 20H1+), a compromised renderer can call this IPC once to obtain authorization for the document’s lifetime. It does not need to actually use the resulting renderer. Once authorized, the renderer can use the media.mojom.MediaPlayerHost interface to send spoofed audibility updates (e.g., claiming it is playing audio and not using the audio service via OnUseAudioServiceChanged(false)). The browser accepts these claims because the document is marked as ‘authorized’, leading to several security impacts.
Potential Impact
- UI Spoofing: The attacker can trigger the ‘audio playing’ icon in the tab strip even if no audio is being produced.
- Bypass of Automatic Picture-in-Picture (Auto-PiP) Gates: Features like
AutoPictureInPictureTabHelperuseWasRecentlyAudible()as a condition for entering PiP. This bypass allows a compromised renderer to satisfy this security gate without actual audio activity, potentially popping up windows without user consent. - Unauthorized Wake Locks: The browser may acquire power wake-locks based on spoofed audio state, preventing the system from entering sleep mode.
Suggested Reproductions Steps (Potential)
Note: These steps are theoretical as they have not been executed by a proof-of-concept script.
- From a compromised renderer on Windows, bind
media.mojom.InterfaceFactoryvia theBrowserInterfaceBroker. - Call
CreateMediaFoundationRendererwith arbitrary parameters to trigger the browser-side authorization. - Bind the
media.mojom.MediaPlayerHostassociated interface. - Register a new player using
OnMediaPlayerAddedto obtain aMediaPlayerObserverreceiver. - Send
OnMediaMetadataChanged(has_audio=true),OnUseAudioServiceChanged(false), andOnMediaPlaying()via theMediaPlayerObserverremote. - Verify that the tab strip now displays the audible indicator and the browser has acquired an audio wake-lock.
Recommended Fix
The AudibilityBypassAuthorization should not be granted unconditionally to the document upon a single IPC call. Instead, the authorization should be tied to the lifetime of an active MediaFoundationRenderer instance, or the browser should verify that the audibility signal originates from the trusted Media Foundation Service process rather than relying on claims provided by the renderer process.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.