Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in GetUserMedia
DescriptionIncorrect authorization in GetUserMedia
ComponentGetUserMedia
Sub-componentMedia
Bug ClassAuthorization Error
Tracker502648640
Fix commit80aaac59c353 (chromium/src) +26/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/media/media_stream_dispatcher_host.cc
modified

Files Changed

  • tools/metrics/histograms/metadata/stability/enums.xml
  • content/browser/bad_message.h
  • content/browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc
  • content/browser/renderer_host/media/media_stream_dispatcher_host.cc
From 80aaac59c3532f35bd1693a4b68a70618e067407 Mon Sep 17 00:00:00 2001
From: Hongchan Choi <[email protected]>
Date: Fri, 11 Sep 2026 15:49:06 -0700
Subject: [PATCH] media: Validate request_all_screens against video stream type

The renderer always pairs StreamControls::request_all_screens with a
video stream type of DISPLAY_VIDEO_CAPTURE_SET, and the browser keys
multi-screen dispatch on the bool while keying the
IsGetAllScreensMedia() predicate on the enum. Enforce that invariant in
ValidateControlsForGenerateStreams() so the two stay in sync,
terminating the renderer for inconsistent StreamControls the same way
the other cross-field checks in this function do.

Bug: 502648640
Test: content_unittests --gtest_filter="MediaStreamDispatcherHostTest*"
Change-Id: Ied17aac33ee5abb87aa4bb5fb953d7cad4da8362
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8375221
Commit-Queue: Hongchan Choi <[email protected]>
Reviewed-by: Robert Kaplow <[email protected]>
Reviewed-by: Dave Tapuska <[email protected]>
Reviewed-by: Jordan Bayles <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1696679}
---
diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index 39e7ed1..22e43a5 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -419,6 +419,7 @@
   RFHI_BEGIN_NAVIGATION_INVALID_INITIATOR_PROCESS = 391,
   RFHI_INVALID_INITIATOR_STATE_TOKEN_UPDATE = 392,
   RFHI_WEBMCP_INVALID_TOOL_NAME = 393,
+  MSDH_REQUEST_ALL_SCREENS_WITH_INVALID_STREAM_TYPE = 394,
 
   // Please add new elements here. The naming convention is abbreviated class
   // name (e.g. RenderFrameHost becomes RFH) plus a unique description of the
diff --git a/content/browser/renderer_host/media/media_stream_dispatcher_host.cc b/content/browser/renderer_host/media/media_stream_dispatcher_host.cc
index 522921e..a2600d2 100644
--- a/content/browser/renderer_host/media/media_stream_dispatcher_host.cc
+++ b/content/browser/renderer_host/media/media_stream_dispatcher_host.cc
@@ -845,6 +845,12 @@
     return bad_message::MSDH_INVALID_STREAM_TYPE_COMBINATION;
   }
 
+  if (controls.request_all_screens &&
+      controls.video.stream_type !=
+          blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE_SET) {
+    return bad_message::MSDH_REQUEST_ALL_SCREENS_WITH_INVALID_STREAM_TYPE;
+  }
+
   if (!controls.audio.requested()) {
     if (controls.suppress_local_audio_playback) {
       return bad_message::
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index 3c1f955..80ae6f3 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -575,6 +575,7 @@
   <int value="391" label="RFHI_BEGIN_NAVIGATION_INVALID_INITIATOR_PROCESS"/>
   <int value="392" label="RFHI_INVALID_INITIATOR_STATE_TOKEN_UPDATE"/>
   <int value="393" label="RFHI_WEBMCP_INVALID_TOOL_NAME"/>
+  <int value="394" label="MSDH_REQUEST_ALL_SCREENS_WITH_INVALID_STREAM_TYPE"/>
 </enum>
 
 <enum name="BadMessageReasonExtensions">
diff --git a/content/browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc b/content/browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc
index 08601485..330d019 100644
--- a/content/browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc
+++ b/content/browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc
@@ -702,6 +702,24 @@
   host_->OnGenerateStreams(kPageRequestId, controls);
 }
 
+TEST_F(MediaStreamDispatcherHostTest,
+       BadMessageIfRequestAllScreensWithoutVideoCaptureSet) {
+  blink::StreamControls controls;
+  controls.audio.stream_type = MediaStreamType::NO_SERVICE;
+  controls.video.stream_type = MediaStreamType::DISPLAY_VIDEO_CAPTURE;
+  controls.request_all_screens = true;
+
+  SetupFakeUI(/*expect_started=*/true);
+
+  EXPECT_CALL(
+      *this,
+      MockOnBadMessage(
+          main_rfh()->GetGlobalId().child_id,
+          bad_message::MSDH_REQUEST_ALL_SCREENS_WITH_INVALID_STREAM_TYPE))
+      .Times(1);
+  host_->OnGenerateStreams(kPageRequestId, controls);
+}
+
 // This test simulates a shutdown scenario: we don't setup a fake UI proxy for
 // MediaStreamManager, so it will create an ordinary one which will not find
 // a RenderFrameHostDelegate. This normally should only be the case at shutdown.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc b/content/browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc
index 08601485..330d019 100644
--- a/content/browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc
+++ b/content/browser/renderer_host/media/media_stream_dispatcher_host_unittest.cc
@@ -702,6 +702,24 @@
   host_->OnGenerateStreams(kPageRequestId, controls);
 }
 
+TEST_F(MediaStreamDispatcherHostTest,
+       BadMessageIfRequestAllScreensWithoutVideoCaptureSet) {
+  blink::StreamControls controls;
+  controls.audio.stream_type = MediaStreamType::NO_SERVICE;
+  controls.video.stream_type = MediaStreamType::DISPLAY_VIDEO_CAPTURE;
+  controls.request_all_screens = true;
+
+  SetupFakeUI(/*expect_started=*/true);
+
+  EXPECT_CALL(
+      *this,
+      MockOnBadMessage(
+          main_rfh()->GetGlobalId().child_id,
+          bad_message::MSDH_REQUEST_ALL_SCREENS_WITH_INVALID_STREAM_TYPE))
+      .Times(1);
+  host_->OnGenerateStreams(kPageRequestId, controls);
+}
+
 // This test simulates a shutdown scenario: we don't setup a fake UI proxy for
 // MediaStreamManager, so it will create an ordinary one which will not find
 // a RenderFrameHostDelegate. This normally should only be the case at shutdown.
Loading diff…

Original Bug Report

reported by [email protected]

Silent multi-screen capture via stream_type desynchronization in MediaStreamManager

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.

Overview: A logic flaw in MediaStreamManager on ChromeOS potentially allows a compromised renderer to bypass mandatory privacy UI indicators during multi-screen capture. By desynchronizing the request_all_screens flag and the stream_type parameter, an attacker can silently capture all connected monitors. This requires the renderer’s origin to be allowlisted by the MultiScreenCaptureAllowedForUrls enterprise policy.

Affected files:

  • content/browser/renderer_host/media/media_stream_manager.cc
  • content/browser/renderer_host/media/media_stream_dispatcher_host.cc
  • chrome/browser/media/webrtc/chrome_screen_enumerator.cc
  • chrome/browser/chrome_content_browser_client.cc

Estimated timestamp from git blame: 2025-07-31

Summary

Under specific enterprise policy conditions on ChromeOS, a compromised renderer can potentially initiate a multi-screen capture session without triggering any mandatory privacy indicators (such as the standard sharing notification bar or the system tray icons managed by MultiCaptureUsageIndicatorService). This allows for the silent exfiltration of all visible screens.

Prerequisites

  • The target device is running ChromeOS.
  • The origin is allowlisted via the MultiScreenCaptureAllowedForUrls enterprise policy.
  • The attacker has achieved Remote Code Execution (RCE) in the renderer process hosting the allowlisted origin.

Root Cause Analysis

In blink.mojom.MediaStreamDispatcherHost.GenerateStreams, a multi-screen capture request triggers specific logic via the request_all_screens boolean flag. However, the UI and privacy indicator mechanisms expect multi-screen captures to explicitly use the DISPLAY_VIDEO_CAPTURE_SET stream type.

A compromised renderer can desynchronize these expectations, leading to a complete bypass of all privacy UI:

  1. Missing Invariant Check: MediaStreamDispatcherHost::ValidateControlsForGenerateStreams does not enforce that request_all_screens == true strictly pairs with DISPLAY_VIDEO_CAPTURE_SET. An attacker can supply request_all_screens = true alongside the standard single-screen type video.stream_type = DISPLAY_VIDEO_CAPTURE.
  2. Policy Authorization: On the UI thread, CheckRequestAllScreensAllowed authorizes the multi-screen capture purely based on the request_all_screens flag and the origin’s presence in the enterprise policy allowlist.
  3. Standard UI Proxy Bypass: Back on the IO thread, MediaStreamManager::SetUpRequest executes an if (request->stream_controls().request_all_screens) block, which calls screen_enumerator->EnumerateScreens and executes an early return;. This skips the subsequent call to PostRequestToUI, leaving the session’s request->ui_proxy default-initialized to nullptr.
  4. Indicator Suppression: Once the physical monitors are enumerated, the browser suppresses all privacy indicators:
    • Multi-capture UI bypassed: NotifyMultiCaptureStateChanged evaluates IsGetAllScreensMedia(). Because the attacker provided DISPLAY_VIDEO_CAPTURE rather than DISPLAY_VIDEO_CAPTURE_SET, this returns false, and the ChromeOS multi-capture tray indicators are bypassed.
    • Standard UI bypassed: MediaStreamManager::OnStreamStarted attempts to display the standard sharing bar by checking if (request->ui_proxy). Since the ui_proxy was bypassed during setup and remains nullptr, standard UI is completely suppressed.

Potential Reproduction Steps

(Note: These are suggested steps based on static code analysis; our tooling agent cannot execute code to provide a working proof-of-concept.)

  1. On a ChromeOS device, ensure an origin (e.g., an Isolated Web App or enterprise domain) is allowlisted via the MultiScreenCaptureAllowedForUrls enterprise policy.
  2. Compromise the renderer process for that origin.
  3. Send a crafted blink.mojom.MediaStreamDispatcherHost.GenerateStreams Mojo request with the following StreamControls:
    • video.stream_type = DISPLAY_VIDEO_CAPTURE
    • audio.stream_type = NO_SERVICE
    • request_all_screens = true
  4. The browser process grants valid session IDs for all enumerated physical monitors.
  5. The renderer binds and records the streams silently, without any privacy UI indicators or notifications being displayed to the user.

Suggested Fix

In MediaStreamDispatcherHost::ValidateControlsForGenerateStreams (or its helper AllowedStreamTypeCombination), explicitly enforce an invariant contract between request_all_screens and stream_type.

If request_all_screens is true, the video.stream_type must strictly be DISPLAY_VIDEO_CAPTURE_SET. If this invariant is violated, the validation should fail and immediately return bad_message::BadMessageReason::MSDH_INVALID_STREAM_TYPE_COMBINATION to terminate the misbehaving renderer.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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