Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in GetUserMedia
DescriptionInsufficient policy enforcement in GetUserMedia
ComponentGetUserMedia
Bug ClassLogic Error
Tracker497358012
Fix commit1e43d4524939 (chromium/src) +106/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/media/media_devices_dispatcher_host.cc
modified
TEST_P
content/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc
modified

Files Changed

  • content/browser/browser_interface_binders.cc
  • content/browser/renderer_host/media/media_devices_dispatcher_host.cc
  • content/browser/renderer_host/media/media_devices_dispatcher_host.h
  • content/browser/renderer_host/media/media_devices_dispatcher_host_unittest.cc
  • third_party/blink/renderer/modules/mediastream/media_devices.cc
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.