Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in MediaCapture
DescriptionInappropriate implementation in MediaCapture
ComponentMediaCapture
Bug ClassLogic Error
Tracker514013849
Fix commit3b76a7ea5c17 (chromium/src) +330/-88
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/browser/media/webrtc/desktop_capture_access_handler.cc
modified

Files Changed

  • chrome/browser/extensions/api/desktop_capture/desktop_capture_apitest.cc
  • chrome/browser/media/webrtc/desktop_capture_access_handler.cc
  • chrome/browser/media/webrtc/desktop_capture_access_handler.h
  • chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
From 3b76a7ea5c17cb3c4fae0a8cf1f5d251dbdaf9fe Mon Sep 17 00:00:00 2001
From: Tove Petersson <[email protected]>
Date: Wed, 27 May 2026 07:39:28 -0700
Subject: [PATCH] Mitigate display media access risks for crbug.com/514013849

- In DisplayMediaAccessHandler::ProcessQueuedAccessRequest and
  DesktopCaptureAccessHandler::ProcessQueuedAccessRequest, verify that the
  RenderFrameHost associated with the request is still active and that its
  current origin matches the origin captured at the time of the initial request.
- In DisplayMediaAccessHandler::ProcessQueuedPickerRequest, use the
  application_title already stored in the PendingAccessRequest structure
  instead of recalculating it from the WebContents state.

BUG=514013849
TAG=agy
CONV=3b5795ce-c83e-46a6-9fc7-3b2715504219

Change-Id: I5c305008bc628a936a48e3f4f054a2cfaaa4a236
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7859818
Commit-Queue: Tove Petersson <[email protected]>
Reviewed-by: Guido Urdaneta <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1636944}
---

diff --git a/chrome/browser/extensions/api/desktop_capture/desktop_capture_apitest.cc b/chrome/browser/extensions/api/desktop_capture/desktop_capture_apitest.cc
index 468ff644..dc28047 100644
--- a/chrome/browser/extensions/api/desktop_capture/desktop_capture_apitest.cc
+++ b/chrome/browser/extensions/api/desktop_capture/desktop_capture_apitest.cc
@@ -5,6 +5,7 @@
 #include <array>
 
 #include "base/command_line.h"
+#include "base/containers/span.h"
 #include "base/path_service.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
@@ -183,7 +184,7 @@
        .picker_result = DesktopMediaID(DesktopMediaID::TYPE_SCREEN,
                                        webrtc::kFullDesktopScreenId)},
   };
-  picker_factory_.SetTestFlags(test_flags, std::size(test_flags));
+  picker_factory_.SetTestFlags(test_flags);
   ASSERT_TRUE(RunExtensionTest("desktop_capture")) << message_;
 }
 
@@ -229,7 +230,7 @@
            DesktopMediaID(DesktopMediaID::TYPE_SCREEN, DesktopMediaID::kNullId),
        .cancelled = true},
   };
-  picker_factory_.SetTestFlags(test_flags, std::size(test_flags));
+  picker_factory_.SetTestFlags(test_flags);
 
   content::WebContents* web_contents = GetActiveWebContents();
 
@@ -336,7 +337,7 @@
   FakeDesktopMediaPickerFactory::TestFlags test_flags[] = {
       {.expect_tabs = true, .picker_result = MakeFakeWebContentsMediaId(true)},
   };
-  picker_factory_.SetTestFlags(test_flags, std::size(test_flags));
+  picker_factory_.SetTestFlags(test_flags);
 
   ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_;
 }
diff --git a/chrome/browser/media/webrtc/desktop_capture_access_handler.cc b/chrome/browser/media/webrtc/desktop_capture_access_handler.cc
index 6621c0de..63eca98b 100644
--- a/chrome/browser/media/webrtc/desktop_capture_access_handler.cc
+++ b/chrome/browser/media/webrtc/desktop_capture_access_handler.cc
@@ -568,6 +568,20 @@
 
   const PendingAccessRequest& pending_request = *queue.front();
 
+  content::RenderFrameHost* const rfh = content::RenderFrameHost::FromID(
+      pending_request.request.render_process_id,
+      pending_request.request.render_frame_id);
+  if (!rfh || !rfh->IsActive()) {
+    RejectRequest(web_contents, blink::mojom::MediaStreamRequestResult::
+                                    FAILED_DUE_TO_SHUTDOWN_NO_RFH_IN_HANDLER);
+    return;
+  }
+  if (rfh->GetLastCommittedOrigin() != pending_request.request.url_origin) {
+    RejectRequest(web_contents,
+                  blink::mojom::MediaStreamRequestResult::INVALID_STATE);
+    return;
+  }
+
   if (!pending_request.picker) {
     DCHECK(!pending_request.request.requested_video_device_ids.empty());
     content::WebContentsMediaCaptureId web_contents_id;
@@ -639,6 +653,31 @@
     delegate->ActivateContents(web_contents);
 }
 
+void DesktopCaptureAccessHandler::RejectRequest(
+    content::WebContents* web_contents,
+    blink::mojom::MediaStreamRequestResult result) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  DCHECK(web_contents);
+
+  auto it = pending_requests_.find(web_contents);
+  if (it == pending_requests_.end()) {
+    return;
+  }
+  RequestsQueue& mutable_queue = it->second;
+  if (mutable_queue.empty()) {
+    return;
+  }
+  PendingAccessRequest& mutable_request = *mutable_queue.front();
+  if (mutable_request.callback) {
+    std::move(mutable_request.callback)
+        .Run(blink::mojom::StreamDevicesSet(), result, /*ui=*/nullptr);
+  }
+  mutable_queue.pop_front();
+  if (!mutable_queue.empty()) {
+    ProcessQueuedAccessRequest(mutable_queue, web_contents);
+  }
+}
+
 void DesktopCaptureAccessHandler::OnPickerDialogResults(
     base::WeakPtr<content::WebContents> web_contents,
     const std::u16string& application_title,
@@ -668,9 +707,11 @@
   queue.pop_front();
 
   if (!result.has_value()) {
-    std::move(pending_request->callback)
-        .Run(blink::mojom::StreamDevicesSet(), result.error(),
-             /*ui=*/nullptr);
+    if (pending_request->callback) {
+      std::move(pending_request->callback)
+          .Run(blink::mojom::StreamDevicesSet(), result.error(),
+               /*ui=*/nullptr);
+    }
   } else {
     const content::DesktopMediaID media_id = result.value();
     CHECK(!media_id.is_null());
@@ -772,8 +813,10 @@
   stream_devices_set.stream_devices.emplace_back(
       blink::mojom::StreamDevices::New());
   *(stream_devices_set.stream_devices[0]) = std::move(devices);
-  std::move(pending_request->callback)
-      .Run(stream_devices_set, MediaStreamRequestResult::OK, std::move(ui));
+  if (pending_request->callback) {
+    std::move(pending_request->callback)
+        .Run(stream_devices_set, MediaStreamRequestResult::OK, std::move(ui));
+  }
 }
 
 #if BUILDFLAG(IS_CHROMEOS)
@@ -792,10 +835,12 @@
   }
 
   if (!is_dlp_allowed) {
-    std::move(pending_request->callback)
-        .Run(blink::mojom::StreamDevicesSet(),
-             MediaStreamRequestResult::DLP_PERMISSION_DENIED,
-             /*ui=*/nullptr);
+    if (pending_request->callback) {
+      std::move(pending_request->callback)
+          .Run(blink::mojom::StreamDevicesSet(),
+               MediaStreamRequestResult::DLP_PERMISSION_DENIED,
+               /*ui=*/nullptr);
+    }
     return;
   }
 
diff --git a/chrome/browser/media/webrtc/desktop_capture_access_handler.h b/chrome/browser/media/webrtc/desktop_capture_access_handler.h
index 7221c24..df2ddd86 100644
--- a/chrome/browser/media/webrtc/desktop_capture_access_handler.h
+++ b/chrome/browser/media/webrtc/desktop_capture_access_handler.h
@@ -90,6 +90,8 @@
       std::unique_ptr<PendingAccessRequest> pending_request);
   void ProcessQueuedAccessRequest(const RequestsQueue& queue,
                                   content::WebContents* web_contents);
+  void RejectRequest(content::WebContents* web_contents,
+                     blink::mojom::MediaStreamRequestResult result);
   void OnPickerDialogResults(
       base::WeakPtr<content::WebContents> web_contents,
       const std::u16string& application_title,
diff --git a/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc b/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
index 5e00be6..fcb5d81 100644
--- a/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
+++ b/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
@@ -50,6 +50,7 @@
 
   void SetUp() override {
     ChromeRenderViewHostTestHarness::SetUp();
+    NavigateAndCommit(GURL(kOrigin));
     auto picker_factory = std::make_unique<FakeDesktopMediaPickerFactory>();
     picker_factory_ = picker_factory.get();
     access_handler_ = std::make_unique<DesktopCaptureAccessHandler>(
@@ -121,12 +122,15 @@
          .expect_current_tab = false,
          .expect_audio = request_audio,
          .picker_result = response}};
-    picker_factory_->SetTestFlags(test_flags, std::size(test_flags));
+    picker_factory_->SetTestFlags(test_flags);
     blink::mojom::MediaStreamType audio_type =
         request_audio ? blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE
                       : blink::mojom::MediaStreamType::NO_SERVICE;
     content::MediaStreamRequest request(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/api/desktop_capture/desktop_capture_apitest.cc b/chrome/browser/extensions/api/desktop_capture/desktop_capture_apitest.cc
index 468ff644..dc28047 100644
--- a/chrome/browser/extensions/api/desktop_capture/desktop_capture_apitest.cc
+++ b/chrome/browser/extensions/api/desktop_capture/desktop_capture_apitest.cc
@@ -5,6 +5,7 @@
 #include <array>
 
 #include "base/command_line.h"
+#include "base/containers/span.h"
 #include "base/path_service.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
@@ -183,7 +184,7 @@
        .picker_result = DesktopMediaID(DesktopMediaID::TYPE_SCREEN,
                                        webrtc::kFullDesktopScreenId)},
   };
-  picker_factory_.SetTestFlags(test_flags, std::size(test_flags));
+  picker_factory_.SetTestFlags(test_flags);
   ASSERT_TRUE(RunExtensionTest("desktop_capture")) << message_;
 }
 
@@ -229,7 +230,7 @@
            DesktopMediaID(DesktopMediaID::TYPE_SCREEN, DesktopMediaID::kNullId),
        .cancelled = true},
   };
-  picker_factory_.SetTestFlags(test_flags, std::size(test_flags));
+  picker_factory_.SetTestFlags(test_flags);
 
   content::WebContents* web_contents = GetActiveWebContents();
 
@@ -336,7 +337,7 @@
   FakeDesktopMediaPickerFactory::TestFlags test_flags[] = {
       {.expect_tabs = true, .picker_result = MakeFakeWebContentsMediaId(true)},
   };
-  picker_factory_.SetTestFlags(test_flags, std::size(test_flags));
+  picker_factory_.SetTestFlags(test_flags);
 
   ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_;
 }
diff --git a/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc b/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
index 5e00be6..fcb5d81 100644
--- a/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
+++ b/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
@@ -50,6 +50,7 @@
 
   void SetUp() override {
     ChromeRenderViewHostTestHarness::SetUp();
+    NavigateAndCommit(GURL(kOrigin));
     auto picker_factory = std::make_unique<FakeDesktopMediaPickerFactory>();
     picker_factory_ = picker_factory.get();
     access_handler_ = std::make_unique<DesktopCaptureAccessHandler>(
@@ -121,12 +122,15 @@
          .expect_current_tab = false,
          .expect_audio = request_audio,
          .picker_result = response}};
-    picker_factory_->SetTestFlags(test_flags, std::size(test_flags));
+    picker_factory_->SetTestFlags(test_flags);
     blink::mojom::MediaStreamType audio_type =
         request_audio ? blink::mojom::MediaStreamType::GUM_DESKTOP_AUDIO_CAPTURE
                       : blink::mojom::MediaStreamType::NO_SERVICE;
     content::MediaStreamRequest request(
-        0, 0, 0, url::Origin::Create(GURL(kOrigin)), false, request_type,
+        web_contents()->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(),
+        web_contents()->GetPrimaryMainFrame()->GetRoutingID(),
+        /*page_request_id=*/0, url::Origin::Create(GURL(kOrigin)), false,
+        request_type,
         /*requested_audio_device_ids=*/{},
         /*requested_video_device_ids=*/{}, audio_type,
         blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE,
@@ -227,8 +231,10 @@
 
 TEST_F(DesktopCaptureAccessHandlerTest,
        ChangeSourceUpdateMediaRequestStateWithClosing) {
-  const int render_process_id = 0;
-  const int render_frame_id = 0;
+  const int render_process_id =
+      web_contents()->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID();
+  const int render_frame_id =
+      web_contents()->GetPrimaryMainFrame()->GetRoutingID();
   const int page_request_id = 0;
   const blink::mojom::MediaStreamType stream_type =
       blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE;
@@ -237,7 +243,7 @@
        true /* expect_tabs */, false /* expect_current_tab */,
        false /* expect_audio */, content::DesktopMediaID(),
        true /* cancelled */}};
-  picker_factory_->SetTestFlags(test_flags, std::size(test_flags));
+  picker_factory_->SetTestFlags(test_flags);
   content::MediaStreamRequest request(
       render_process_id, render_frame_id, page_request_id,
       url::Origin::Create(GURL(kOrigin)), false, blink::MEDIA_DEVICE_UPDATE,
@@ -271,9 +277,11 @@
        true /* expect_tabs */, false /* expect_current_tab */,
        false /* expect_audio */, content::DesktopMediaID(),
        true /* cancelled */}};
-  picker_factory_->SetTestFlags(test_flags, std::size(test_flags));
+  picker_factory_->SetTestFlags(test_flags);
   content::MediaStreamRequest request(
-      0, 0, 0, url::Origin::Create(GURL(kOrigin)), false,
+      web_contents()->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(),
+      web_contents()->GetPrimaryMainFrame()->GetRoutingID(),
+      /*page_request_id=*/0, url::Origin::Create(GURL(kOrigin)), false,
       blink::MEDIA_DEVICE_UPDATE, /*requested_audio_device_ids=*/{},
       /*requested_video_device_ids=*/{},
       blink::mojom::MediaStreamType::NO_SERVICE,
@@ -309,14 +317,16 @@
            content::DesktopMediaID::TYPE_WINDOW,
            content::DesktopMediaID::kNullId) /* selected_source */}};
   const size_t kTestFlagCount = 2;
-  picker_factory_->SetTestFlags(test_flags, kTestFlagCount);
+  picker_factory_->SetTestFlags(test_flags);
 
   blink::mojom::MediaStreamRequestResult result;
   blink::MediaStreamDevices devices;
   base::RunLoop wait_loop[kTestFlagCount];
   for (base::RunLoop& loop : wait_loop) {
     content::MediaStreamRequest request(
-        0, 0, 0, url::Origin::Create(GURL(kOrigin)), false,
+        web_contents()->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(),
+        web_contents()->GetPrimaryMainFrame()->GetRoutingID(),
+        /*page_request_id=*/0, url::Origin::Create(GURL(kOrigin)), false,
         blink::MEDIA_DEVICE_UPDATE, /*requested_audio_device_ids=*/{},
         /*requested_video_device_ids=*/{},
         blink::mojom::MediaStreamType::NO_SERVICE,
diff --git a/chrome/browser/media/webrtc/display_media_access_handler_unittest.cc b/chrome/browser/media/webrtc/display_media_access_handler_unittest.cc
index c568c16..0d5b80b 100644
--- a/chrome/browser/media/webrtc/display_media_access_handler_unittest.cc
+++ b/chrome/browser/media/webrtc/display_media_access_handler_unittest.cc
@@ -19,12 +19,13 @@
 #include "base/types/expected.h"
 #include "build/build_config.h"
 #include "chrome/browser/media/webrtc/fake_desktop_media_picker_factory.h"
+#include "chrome/browser/web_applications/test/web_app_test.h"
 #include "chrome/common/pref_names.h"
-#include "chrome/test/base/chrome_render_view_host_test_harness.h"
 #include "components/prefs/pref_service.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/desktop_media_id.h"
 #include "content/public/browser/media_stream_request.h"
+#include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/content_features.h"
 #include "content/public/test/browser_test_utils.h"
@@ -33,6 +34,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/common/mediastream/media_stream_request.h"
 #include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
+#include "url/origin.h"
 
 #if BUILDFLAG(IS_CHROMEOS)
 #include "chrome/browser/chromeos/policy/dlp/test/mock_dlp_content_manager.h"
@@ -42,9 +44,15 @@
 #include "base/test/gmock_expected_support.h"
 #include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
 #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
+#include "chrome/browser/web_applications/isolated_web_apps/iwa_permissions_policy_cache.h"
 #include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
 #include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
+#include "components/webapps/isolated_web_apps/types/iwa_origin.h"
+#include "content/public/test/web_contents_tester.h"
+#include "net/http/http_response_headers.h"
 #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
+#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
+#include "third_party/blink/public/common/permissions_policy/policy_helper_public.h"
 #endif  // !BUILDFLAG(IS_ANDROID)
 
 #if BUILDFLAG(IS_WIN)
@@ -54,13 +62,18 @@
 #include "media/audio/application_loopback_device_helper.h"
 #endif  // BUILDFLAG(IS_WIN)
 
-class DisplayMediaAccessHandlerTest : public ChromeRenderViewHostTestHarness {
+class DisplayMediaAccessHandlerTest : public WebAppTest {
  public:
   DisplayMediaAccessHandlerTest() = default;
   ~DisplayMediaAccessHandlerTest() override = default;
 
   void SetUp() override {
-    ChromeRenderViewHostTestHarness::SetUp();
+    WebAppTest::SetUp();
+    std::unique_ptr<content::NavigationSimulator> navigation =
+        content::NavigationSimulator::CreateBrowserInitiated(
+            GURL("http://origin/"), web_contents());
+    navigation->Commit();
+
     auto picker_factory = std::make_unique<FakeDesktopMediaPickerFactory>();
     picker_factory_ = picker_factory.get();
     access_handler_ = std::make_unique<DisplayMediaAccessHandler>(
@@ -70,7 +83,7 @@
   content::WebContentsMediaCaptureId GetWebContentsMediaCaptureId() {
     return content::WebContentsMediaCaptureId(
         web_contents()->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(),
-        1);
+        web_contents()->GetPrimaryMainFrame()->GetRoutingID());
   }
 
   FakeDesktopMediaPickerFactory::TestFlags MakePickerTestFlags(
@@ -90,7 +103,7 @@
     return content::MediaStreamRequest(
         web_contents()->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(),
         web_contents()->GetPrimaryMainFrame()->GetRoutingID(), 0,
-        url::Origin::Create(GURL("http://origin/")), false,
+        web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(), false,
         blink::MEDIA_GENERATE_STREAM, /*requested_audio_device_ids=*/{},
         /*requested_video_device_ids=*/{},
         request_audio ? blink::mojom::MediaStreamType::DISPLAY_AUDIO_CAPTURE
@@ -161,7 +174,7 @@
   void SetTestFlags(
       std::vector<FakeDesktopMediaPickerFactory::TestFlags> test_flags_vector) {
     test_flags_ = std::move(test_flags_vector);
-    picker_factory_->SetTestFlags(&test_flags_[0], test_flags_.size());
+    picker_factory_->SetTestFlags(test_flags_);
   }
 
   void ProcessRequest(
@@ -219,7 +232,12 @@
   }
 
   DesktopMediaPicker::Params GetParams() {
-    return picker_factory_->picker()->GetParams();
+    FakeDesktopMediaPicker* picker = picker_factory_->picker();
+    if (!picker) {
+      ADD_FAILURE() << "Picker was destroyed prematurely!";
+      return DesktopMediaPicker::Params();
+    }
+    return picker->GetParams();
   }
 
   const DisplayMediaAccessHandler::RequestsQueues& GetRequestQueues() {
@@ -556,10 +574,6 @@
 }
 
 TEST_F(DisplayMediaAccessHandlerTest, CorrectHostAsksForPermissions) {
-  const int render_process_id =
-      web_contents()->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID();
-  const int render_frame_id =
-      web_contents()->GetPrimaryMainFrame()->GetRoutingID();
   const int page_request_id = 0;
   const blink::mojom::MediaStreamType video_stream_type =
       blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE;
@@ -569,20 +583,27 @@
                  true /* expect_tabs */, false /* expect_current_tab */,
                  true /* expect_audio */, content::DesktopMediaID(),
                  true /* cancelled */}});
-  content::MediaStreamRequest request(
-      render_process_id, render_frame_id, page_request_id,
-      url::Origin::Create(GURL("http://origin/")), false,
-      blink::MEDIA_GENERATE_STREAM, /*requested_audio_device_ids=*/{},
-      /*requested_video_device_ids=*/{}, audio_stream_type, video_stream_type,
-      /*disable_local_echo=*/false, /*request_pan_tilt_zoom_permission=*/false,
-      /*captured_surface_control_active=*/false);
-  content::MediaResponseCallback callback;
+
   content::WebContents* test_web_contents = web_contents();
   std::unique_ptr<content::NavigationSimulator> navigation =
       content::NavigationSimulator::CreateBrowserInitiated(
           GURL("blob:http://127.0.0.1:8000/says: www.google.com"),
           test_web_contents);
   navigation->Commit();
+
+  const int render_process_id =
+      test_web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID();
+  const int render_frame_id =
+      test_web_contents->GetPrimaryMainFrame()->GetRoutingID();
+
+  content::MediaStreamRequest request(
+      render_process_id, render_frame_id, page_request_id,
+      url::Origin::Create(GURL("http://127.0.0.1:8000")), false,
+      blink::MEDIA_GENERATE_STREAM, /*requested_audio_device_ids=*/{},
+      /*requested_video_device_ids=*/{}, audio_stream_type, video_stream_type,
+      /*disable_local_echo=*/false, /*request_pan_tilt_zoom_permission=*/false,
+      /*captured_surface_control_active=*/false);
+  content::MediaResponseCallback callback;
   access_handler_->HandleRequest(test_web_contents, request,
                                  std::move(callback), nullptr /* extension */);
   DesktopMediaPicker::Params params = GetParams();
@@ -593,10 +614,6 @@
 }
 
 TEST_F(DisplayMediaAccessHandlerTest, CorrectHostAsksForPermissionsNormalURLs) {
-  const int render_process_id =
-      web_contents()->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID();
-  const int render_frame_id =
-      web_contents()->GetPrimaryMainFrame()->GetRoutingID();
   const int page_request_id = 0;
   const blink::mojom::MediaStreamType video_stream_type =
       blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE;
@@ -606,19 +623,26 @@
                  true /* expect_tabs */, false /* expect_current_tab */,
                  true /* expect_audio */, content::DesktopMediaID(),
                  true /* cancelled */}});
-  content::MediaStreamRequest request(
-      render_process_id, render_frame_id, page_request_id,
-      url::Origin::Create(GURL("http://origin/")), false,
-      blink::MEDIA_GENERATE_STREAM, /*requested_audio_device_ids=*/{},
-      /*requested_video_device_ids=*/{}, audio_stream_type, video_stream_type,
-      /*disable_local_echo=*/false, /*request_pan_tilt_zoom_permission=*/false,
-      /*captured_surface_control_active=*/false);
-  content::MediaResponseCallback callback;
+
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Origin spoofing in getDisplayMedia via queued requests after navigation

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in DisplayMediaAccessHandler allows a compromised renderer to spoof the origin displayed in the screen capture picker. By queuing multiple requests and initiating a navigation, the attacker can cause the subsequent permission UI to mis-attribute the request to the new, post-navigation site origin.

Affected files:

  • chrome/browser/media/webrtc/display_media_access_handler.cc
  • content/browser/renderer_host/media/media_stream_manager.cc
  • components/web_modal/web_contents_modal_dialog_manager.cc
  • chrome/browser/ui/views/desktop_capture/desktop_media_picker_views.cc
  • chrome/browser/ui/views/media_picker_utils.cc

Estimated timestamp from git blame: 2018-08-14

Potential Vulnerability: Origin Spoofing in getDisplayMedia Picker

A logic error exists in DisplayMediaAccessHandler (the handler for the getDisplayMedia API) that may allow a compromised renderer to spoof the origin shown in the media picker dialog and the subsequent capture notification.

When getDisplayMedia is called, the request is processed by DisplayMediaAccessHandler::HandleRequest. If multiple requests are made, they are queued in a RequestsQueue associated with the WebContents. If a picker is already visible, subsequent requests wait in this queue.

When a top-level navigation occurs, any active modal dialog is closed. In the case of the screen capture picker, this dismissal triggers the dequeuing of the next request in DisplayMediaAccessHandler::RejectRequest, which calls ProcessQueuedAccessRequest and then ProcessQueuedPickerRequest.

There are two primary issues in this path:

  1. Lack of RFH Validation: The dequeuing path fails to re-verify if the RenderFrameHost that originated the request is still active. While the initial HandleRequest performs an IsActive() check, the dequeuing logic does not.
  2. UI Origin Mis-attribution: ProcessQueuedPickerRequest populates the picker’s application title by calling GetApplicationTitle(web_contents). This helper retrieves the origin of the current primary main frame of the WebContents. If the page has navigated to a new site before the queued request is processed, the picker will display the new site’s origin instead of the originating one.

Because a compromised renderer can keep the Mojo connection to MediaStreamDispatcherHost alive across navigations, the resulting capture stream will be delivered to the attacker’s process once the user approves the spoofed prompt.

Potential Attack Scenario

  1. A compromised renderer on attacker.com calls getDisplayMedia twice. Request A displays a picker; Request B is queued.
  2. The renderer initiates a navigation to victim.com.
  3. Upon navigation commit, the browser closes the picker for Request A.
  4. DisplayMediaAccessHandler immediately dequeues Request B and shows a new picker.
  5. The new picker retrieves the current origin from the WebContents, which is now victim.com, and displays it as the requester: “victim.com wants to share your screen”.
  6. The user, trusting the browser’s UI, approves the request.
  7. The screen capture session is initiated, and the stream is delivered to the attacker via the original, still-open Mojo connection.

Suggested Fix

To mitigate this, the browser should:

  1. In ProcessQueuedAccessRequest, verify that the RenderFrameHost associated with the request is still active and that its current origin matches the origin captured at the time of the initial request.
  2. In ProcessQueuedPickerRequest, use the application_title already stored in the PendingAccessRequest structure (which was captured at the time of the request) instead of recalculating it from the WebContents state.

Note: These steps are based on source code analysis. We have not yet verified them with a working proof-of-concept.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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.

View on issue tracker