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 <guidou@chromium.org>
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 <antoniosartori@chromium.org>
Reviewed-by: Tony Herre <toprice@chromium.org>
Commit-Queue: Guido Urdaneta <guidou@chromium.org>
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