Chrome · GetUserMedia
CVE-2026-14039
Logic Error in GetUserMedia
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/media/media_devices_dispatcher_host.cc |
modified | |
TEST_Pcontent/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc |
modified |
Files Changed
content/browser/browser_interface_binders.cccontent/browser/renderer_host/media/media_devices_dispatcher_host.cccontent/browser/renderer_host/media/media_devices_dispatcher_host.hcontent/browser/renderer_host/media/media_devices_dispatcher_host_unittest.ccthird_party/blink/renderer/modules/mediastream/media_devices.cc
Patch
From 1e43d4524939fd2ff8061f4f6b52e2d368618f29 Mon Sep 17 00:00:00 2001 From: Guido Urdaneta <[email protected]> Date: Thu, 21 May 2026 07:20:20 -0700 Subject: [PATCH] [SetPreferredSink] Ensure setPreferredSink can only be called from top-level frames Fixed: 497358012 Change-Id: Ic336d5eca007822a18179cbbf05646204c036df3 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7867635 Reviewed-by: Antonio Sartori <[email protected]> Reviewed-by: Tony Herre <[email protected]> Commit-Queue: Guido Urdaneta <[email protected]> Cr-Commit-Position: refs/heads/main@{#1634252} --- diff --git a/content/browser/browser_interface_binders.cc b/content/browser/browser_interface_binders.cc index e6bef53..68f7fb7 100644 --- a/content/browser/browser_interface_binders.cc +++ b/content/browser/browser_interface_binders.cc @@ -1025,10 +1025,12 @@ BrowserMainLoop::GetInstance()->media_stream_manager(); map->Add<blink::mojom::MediaDevicesDispatcherHost>( - base::BindRepeating(&MediaDevicesDispatcherHost::Create, - host->GetMainFrame()->GetGlobalFrameToken(), - host->GetGlobalId(), - base::Unretained(media_stream_manager)), + base::BindRepeating( + &MediaDevicesDispatcherHost::Create, + host->GetMainFrame()->GetGlobalFrameToken(), host->GetGlobalId(), + base::Unretained(media_stream_manager), + /*is_outermost_main_frame=*/host->GetParentOrOuterDocument() == + nullptr), GetIOThreadTaskRunner({})); map->Add<blink::mojom::MediaStreamDispatcherHost>( diff --git a/content/browser/renderer_host/media/media_devices_dispatcher_host.cc b/content/browser/renderer_host/media/media_devices_dispatcher_host.cc index d59d25f..d320e07f 100644 --- a/content/browser/renderer_host/media/media_devices_dispatcher_host.cc +++ b/content/browser/renderer_host/media/media_devices_dispatcher_host.cc @@ -88,20 +88,24 @@ const GlobalRenderFrameHostToken& main_frame_host_token, GlobalRenderFrameHostId render_frame_host_id, MediaStreamManager* media_stream_manager, + bool is_outermost_main_frame, mojo::PendingReceiver<blink::mojom::MediaDevicesDispatcherHost> receiver) { DCHECK_CURRENTLY_ON(BrowserThread::IO); media_stream_manager->media_devices_manager()->RegisterDispatcherHost( std::make_unique<MediaDevicesDispatcherHost>( - main_frame_host_token, render_frame_host_id, media_stream_manager), + main_frame_host_token, render_frame_host_id, media_stream_manager, + is_outermost_main_frame), std::move(receiver)); } MediaDevicesDispatcherHost::MediaDevicesDispatcherHost( const GlobalRenderFrameHostToken& main_frame_host_token, GlobalRenderFrameHostId render_frame_host_id, - MediaStreamManager* media_stream_manager) + MediaStreamManager* media_stream_manager, + bool is_outermost_main_frame) : main_frame_host_token_(main_frame_host_token), render_frame_host_id_(render_frame_host_id), + is_outermost_main_frame_(is_outermost_main_frame), media_stream_manager_(media_stream_manager), num_pending_audio_input_parameters_(0), authorization_handler_factory_callback_(base::BindRepeating( @@ -351,6 +355,15 @@ const std::string& hashed_sink_id, SetPreferredSinkIdCallback callback) { CHECK_CURRENTLY_ON(BrowserThread::IO); + + if (!is_outermost_main_frame_) { + mojo::ReportBadMessage( + "setPreferredSinkId can only be called from the top-level document."); + std::move(callback).Run( + media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_ERROR_NOT_AUTHORIZED); + return; + } + if (!base::FeatureList::IsEnabled( blink::features::kPreferredAudioOutputDevices)) { ReceivedBadMessage(render_frame_host_id_.child_id, diff --git a/content/browser/renderer_host/media/media_devices_dispatcher_host.h b/content/browser/renderer_host/media/media_devices_dispatcher_host.h index 1b94c9bd..0e75d66 100644 --- a/content/browser/renderer_host/media/media_devices_dispatcher_host.h +++ b/content/browser/renderer_host/media/media_devices_dispatcher_host.h @@ -38,7 +38,8 @@ MediaDevicesDispatcherHost( const GlobalRenderFrameHostToken& main_frame_host_token, GlobalRenderFrameHostId render_frame_host_id, - MediaStreamManager* media_stream_manager); + MediaStreamManager* media_stream_manager, + bool is_outermost_main_frame); MediaDevicesDispatcherHost(const MediaDevicesDispatcherHost&) = delete; MediaDevicesDispatcherHost& operator=(const MediaDevicesDispatcherHost&) = @@ -50,6 +51,7 @@ const GlobalRenderFrameHostToken& main_frame_host_token, GlobalRenderFrameHostId render_frame_host_id, MediaStreamManager* media_stream_manager, + bool is_outermost_main_frame, mojo::PendingReceiver<blink::mojom::MediaDevicesDispatcherHost> receiver); // blink::mojom::MediaDevicesDispatcherHost implementation. @@ -205,6 +207,7 @@ // The following const fields can be accessed on any thread. const GlobalRenderFrameHostId render_frame_host_id_; + const bool is_outermost_main_frame_; // The following fields can only be accessed on the IO thread. const raw_ptr<MediaStreamManager> media_stream_manager_; diff --git a/content/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc b/content/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc index 52116c17..56d62eed 100644 --- a/content/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc +++ b/content/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc @@ -53,6 +53,7 @@ #include "media/capture/video/video_capture_system_impl.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/receiver_set.h" +#include "mojo/public/cpp/system/functions.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/features_generated.h" @@ -217,7 +218,8 @@ InitializeRenderFrameHost(); host_ = std::make_unique<MediaDevicesDispatcherHost>( render_frame_host_->GetMainFrame()->GetGlobalFrameToken(), - render_frame_host_->GetGlobalId(), media_stream_manager_.get()); + render_frame_host_->GetGlobalId(), media_stream_manager_.get(), + true /* is_outermost_main_frame */); media_stream_manager_->media_devices_manager() ->set_get_salt_and_origin_cb_for_testing(base::BindRepeating( &MediaDevicesDispatcherHostTest::GetSaltAndOrigin, @@ -953,6 +955,7 @@ MediaDevicesDispatcherHost::Create( render_frame_host_->GetMainFrame()->GetGlobalFrameToken(), render_frame_host_->GetGlobalId(), media_stream_manager_.get(), + true /* is_outermost_main_frame */, client.BindNewPipeAndPassReceiver()); EXPECT_TRUE(client.is_bound()); EXPECT_EQ(media_stream_manager_->media_devices_manager() @@ -978,6 +981,34 @@ host_->SetPreferredSinkId(kDefaultAudioDeviceID, base::DoNothing()); } +TEST_P(MediaDevicesDispatcherHostTest, SetPreferredSinkIdNotMainFrame) { + base::test::ScopedFeatureList feature_list; + feature_list.InitAndEnableFeature( + blink::features::kPreferredAudioOutputDevices); + + std::string received_error; + mojo::SetDefaultProcessErrorHandler(base::BindLambdaForTesting( + [&](const std::string& error) { received_error = error; })); + + mojo::Remote<blink::mojom::MediaDevicesDispatcherHost> client; + MediaDevicesDispatcherHost::Create( + render_frame_host_->GetMainFrame()->GetGlobalFrameToken(), + render_frame_host_->GetGlobalId(), media_stream_manager_.get(), + /*is_outermost_main_frame=*/false, client.BindNewPipeAndPassReceiver()); + + base::test::TestFuture<media::OutputDeviceStatus> future; + client->SetPreferredSinkId(kDefaultAudioDeviceID, future.GetCallback()); + + EXPECT_EQ( + future.Get(), + media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_ERROR_NOT_AUTHORIZED); + EXPECT_EQ( + received_error, + "setPreferredSinkId can only be called from the top-level document."); + + mojo::SetDefaultProcessErrorHandler(base::NullCallback()); +} + TEST_P(MediaDevicesDispatcherHostTest, SelectAudioOutputNoFeature) { EXPECT_CALL( *this, diff --git a/third_party/blink/renderer/modules/mediastream/media_devices.cc b/third_party/blink/renderer/modules/mediastream/media_devices.cc index 74c67ef..b74ee0e2 100644 --- a/third_party/blink/renderer/modules/mediastream/media_devices.cc +++ b/third_party/blink/renderer/modules/mediastream/media_devices.cc @@ -985,11 +985,17 @@ return ScriptPromise<IDLUndefined>(); } + LocalFrame* frame = LocalDOMWindow::From(script_state)->GetFrame(); + if (!frame || !frame->IsOutermostMainFrame()) { + exception_state.ThrowDOMException( + DOMExceptionCode::kInvalidStateError, + "Can only be called from the top-level document."); + return ScriptPromise<IDLUndefined>(); + } + auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(script_state); auto promise = resolver->Promise(); - - LocalFrame* frame = LocalDOMWindow::From(script_state)->GetFrame(); GetDispatcherHost(frame).SetPreferredSinkId( sink_id,
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc b/content/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc
index 52116c17..56d62eed 100644
--- a/content/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc
+++ b/content/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc
@@ -53,6 +53,7 @@
#include "media/capture/video/video_capture_system_impl.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "mojo/public/cpp/system/functions.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features_generated.h"
@@ -217,7 +218,8 @@
InitializeRenderFrameHost();
host_ = std::make_unique<MediaDevicesDispatcherHost>(
render_frame_host_->GetMainFrame()->GetGlobalFrameToken(),
- render_frame_host_->GetGlobalId(), media_stream_manager_.get());
+ render_frame_host_->GetGlobalId(), media_stream_manager_.get(),
+ true /* is_outermost_main_frame */);
media_stream_manager_->media_devices_manager()
->set_get_salt_and_origin_cb_for_testing(base::BindRepeating(
&MediaDevicesDispatcherHostTest::GetSaltAndOrigin,
@@ -953,6 +955,7 @@
MediaDevicesDispatcherHost::Create(
render_frame_host_->GetMainFrame()->GetGlobalFrameToken(),
render_frame_host_->GetGlobalId(), media_stream_manager_.get(),
+ true /* is_outermost_main_frame */,
client.BindNewPipeAndPassReceiver());
EXPECT_TRUE(client.is_bound());
EXPECT_EQ(media_stream_manager_->media_devices_manager()
@@ -978,6 +981,34 @@
host_->SetPreferredSinkId(kDefaultAudioDeviceID, base::DoNothing());
}
+TEST_P(MediaDevicesDispatcherHostTest, SetPreferredSinkIdNotMainFrame) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kPreferredAudioOutputDevices);
+
+ std::string received_error;
+ mojo::SetDefaultProcessErrorHandler(base::BindLambdaForTesting(
+ [&](const std::string& error) { received_error = error; }));
+
+ mojo::Remote<blink::mojom::MediaDevicesDispatcherHost> client;
+ MediaDevicesDispatcherHost::Create(
+ render_frame_host_->GetMainFrame()->GetGlobalFrameToken(),
+ render_frame_host_->GetGlobalId(), media_stream_manager_.get(),
+ /*is_outermost_main_frame=*/false, client.BindNewPipeAndPassReceiver());
+
+ base::test::TestFuture<media::OutputDeviceStatus> future;
+ client->SetPreferredSinkId(kDefaultAudioDeviceID, future.GetCallback());
+
+ EXPECT_EQ(
+ future.Get(),
+ media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_ERROR_NOT_AUTHORIZED);
+ EXPECT_EQ(
+ received_error,
+ "setPreferredSinkId can only be called from the top-level document.");
+
+ mojo::SetDefaultProcessErrorHandler(base::NullCallback());
+}
+
TEST_P(MediaDevicesDispatcherHostTest, SelectAudioOutputNoFeature) {
EXPECT_CALL(
*this,
diff --git a/third_party/blink/renderer/modules/mediastream/media_devices_test.cc b/third_party/blink/renderer/modules/mediastream/media_devices_test.cc
index 8fdc97cdd..f42893d 100644
--- a/third_party/blink/renderer/modules/mediastream/media_devices_test.cc
+++ b/third_party/blink/renderer/modules/mediastream/media_devices_test.cc
@@ -30,6 +30,7 @@
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
#include "third_party/blink/renderer/bindings/core/v8/to_v8_traits.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_boolean_string.h"
@@ -49,8 +50,12 @@
#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
#include "third_party/blink/renderer/core/event_type_names.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_element.h"
+#include "third_party/blink/renderer/core/html/html_iframe_element.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
+#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/modules/mediastream/crop_target.h"
#include "third_party/blink/renderer/modules/mediastream/input_device_info.h"
#include "third_party/blink/renderer/modules/mediastream/media_device_info.h"
@@ -1632,6 +1637,42 @@
EXPECT_TRUE(base::Uuid::ParseLowercase(id.Ascii()).is_valid());
}
+class MediaDevicesSimTest : public SimTest {};
+
+TEST_F(MediaDevicesSimTest, SetPreferredSinkIdRejectsInIframe) {
+ SimRequest main_resource("https://example.com", "text/html");
+ LoadURL("https://example.com");
+ main_resource.Complete(R"(
+ <iframe id="test-iframe" src="about:blank"></iframe>
+ )");
+
+ auto* iframe = To<HTMLIFrameElement>(
+ GetDocument().getElementById(AtomicString("test-iframe")));
+ ASSERT_TRUE(iframe);
+ auto* child_frame = To<LocalFrame>(iframe->ContentFrame());
+ ASSERT_TRUE(child_frame);
+
+ LocalDOMWindow* child_window = child_frame->DomWindow();
+ ASSERT_TRUE(child_window);
+ ASSERT_FALSE(child_frame->IsOutermostMainFrame());
+
+ ScriptState* child_script_state = ToScriptStateForMainWorld(child_frame);
+ ScriptState::Scope child_scope(child_script_state);
+
+ MediaDevices* child_media_devices =
+ MakeGarbageCollected<MediaDevices>(*child_window->navigator());
+
+ DummyExceptionStateForTesting exception_state;
+ child_media_devices->setPreferredSinkId(child_script_state, kValidSinkId,
+ exception_state);
+
+ EXPECT_TRUE(exception_state.HadException());
+ EXPECT_EQ(exception_state.Code(),
+ ToExceptionCode(DOMExceptionCode::kInvalidStateError));
+ EXPECT_EQ(exception_state.Message(),
+ "Can only be called from the top-level document.");
+}
+
// TODO(crbug.com/1418194): Add tests after MediaDevicesDispatcherHost
// has been updated.
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page