CVE-2026-106407
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/media/media_stream_dispatcher_host.cc |
modified |
Files Changed
tools/metrics/histograms/metadata/stability/enums.xmlcontent/browser/bad_message.hcontent/browser/renderer_host/media/media_stream_dispatcher_host_unittest.cccontent/browser/renderer_host/media/media_stream_dispatcher_host.cc
Patch
From 80aaac59c3532f35bd1693a4b68a70618e067407 Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@google.com>
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 <hongchan@chromium.org>
Reviewed-by: Robert Kaplow <rkaplow@chromium.org>
Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
Reviewed-by: Jordan Bayles <jophba@chromium.org>
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.
Regression Test / PoC
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.
Original Bug Report
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.cccontent/browser/renderer_host/media/media_stream_dispatcher_host.ccchrome/browser/media/webrtc/chrome_screen_enumerator.ccchrome/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
MultiScreenCaptureAllowedForUrlsenterprise 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:
- Missing Invariant Check:
MediaStreamDispatcherHost::ValidateControlsForGenerateStreamsdoes not enforce thatrequest_all_screens == truestrictly pairs withDISPLAY_VIDEO_CAPTURE_SET. An attacker can supplyrequest_all_screens = truealongside the standard single-screen typevideo.stream_type = DISPLAY_VIDEO_CAPTURE. - Policy Authorization: On the UI thread,
CheckRequestAllScreensAllowedauthorizes the multi-screen capture purely based on therequest_all_screensflag and the origin’s presence in the enterprise policy allowlist. - Standard UI Proxy Bypass: Back on the IO thread,
MediaStreamManager::SetUpRequestexecutes anif (request->stream_controls().request_all_screens)block, which callsscreen_enumerator->EnumerateScreensand executes an earlyreturn;. This skips the subsequent call toPostRequestToUI, leaving the session’srequest->ui_proxydefault-initialized tonullptr. - Indicator Suppression: Once the physical monitors are enumerated, the browser suppresses all privacy indicators:
- Multi-capture UI bypassed:
NotifyMultiCaptureStateChangedevaluatesIsGetAllScreensMedia(). Because the attacker providedDISPLAY_VIDEO_CAPTURErather thanDISPLAY_VIDEO_CAPTURE_SET, this returnsfalse, and the ChromeOS multi-capture tray indicators are bypassed. - Standard UI bypassed:
MediaStreamManager::OnStreamStartedattempts to display the standard sharing bar by checkingif (request->ui_proxy). Since theui_proxywas bypassed during setup and remainsnullptr, standard UI is completely suppressed.
- Multi-capture UI bypassed:
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.)
- On a ChromeOS device, ensure an origin (e.g., an Isolated Web App or enterprise domain) is allowlisted via the
MultiScreenCaptureAllowedForUrlsenterprise policy. - Compromise the renderer process for that origin.
- Send a crafted
blink.mojom.MediaStreamDispatcherHost.GenerateStreamsMojo request with the followingStreamControls:video.stream_type = DISPLAY_VIDEO_CAPTUREaudio.stream_type = NO_SERVICErequest_all_screens = true
- The browser process grants valid session IDs for all enumerated physical monitors.
- 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.