CVE-2026-11685
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
FakeWindowCapturerchrome/browser/media/webrtc/native_desktop_media_list_unittest.cc |
modified | |
TEST_Fchrome/browser/media/webrtc/native_desktop_media_list_unittest.cc |
modified | |
DelegatedFakeScreenCapturerchrome/browser/media/webrtc/native_desktop_media_list_unittest.cc |
modified | |
NativeDesktopMediaListDelegatedWindowTestchrome/browser/media/webrtc/native_desktop_media_list_unittest.cc |
modified | |
NativeDesktopMediaListDelegatedWindowTestchrome/browser/media/webrtc/native_desktop_media_list_unittest.cc |
modified | |
BindLambdaForTestingchrome/browser/media/webrtc/native_desktop_media_list_unittest.cc |
modified | |
ifchrome/test/BUILD.gn |
modified |
Files Changed
chrome/browser/media/webrtc/native_desktop_media_list.ccchrome/browser/media/webrtc/native_desktop_media_list_unittest.ccchrome/test/BUILD.gn
Patch
From 8dfa225f9f6eb50d6aad503cf90c71bf5b1e8f01 Mon Sep 17 00:00:00 2001 From: Tove Petersson <[email protected]> Date: Fri, 29 May 2026 07:53:01 -0700 Subject: [PATCH] Fix macOS ScopedCGWindowID type confusion under delegated picker In GetUpdatedWindowId(), gate the remote_cocoa::ScopedCGWindowID lookup with !is_source_list_delegated check to prevent type confusion and TCC bypass when delegated picker session IDs overlap with active CGWindowID values. BUG=517183713 Change-Id: I1a83e3c6e0a05e2d7d830e11b48c438624917224 Fixed: 517183713 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7881651 Reviewed-by: Johannes Kron <[email protected]> Commit-Queue: Tove Petersson <[email protected]> Cr-Commit-Position: refs/heads/main@{#1638472} --- diff --git a/chrome/browser/media/webrtc/native_desktop_media_list.cc b/chrome/browser/media/webrtc/native_desktop_media_list.cc index 353e02cdc..3017f61 100644 --- a/chrome/browser/media/webrtc/native_desktop_media_list.cc +++ b/chrome/browser/media/webrtc/native_desktop_media_list.cc @@ -216,7 +216,8 @@ } } #elif BUILDFLAG(IS_MAC) - if (remote_cocoa::ScopedCGWindowID::Get(desktop_media_id.id)) { + if (!is_source_list_delegated && + remote_cocoa::ScopedCGWindowID::Get(desktop_media_id.id)) { window_id = desktop_media_id.id; } #endif diff --git a/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc b/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc index 28f9fe9..c9fc4e3 100644 --- a/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc +++ b/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc @@ -47,6 +47,10 @@ #include "base/strings/string_util_win.h" #endif +#if BUILDFLAG(IS_MAC) +#include "components/remote_cocoa/browser/scoped_cg_window_id.h" +#endif + using content::DesktopMediaID; using testing::_; using testing::DoAll; @@ -141,6 +145,8 @@ ~FakeScreenCapturer() override = default; + void SetSourceList(const SourceList& screens) { screens_ = screens; } + // ThumbnailCapturer implementation. void Start(Consumer* consumer) override { consumer_ = consumer; } @@ -157,17 +163,17 @@ } bool GetSourceList(SourceList* screens) override { - screens->push_back({0}); + *screens = screens_; return true; } bool SelectSource(SourceId id) override { - EXPECT_EQ(0, id); return true; } protected: raw_ptr<Consumer> consumer_; + SourceList screens_ = {{0}}; }; class FakeWindowCapturer : public ThumbnailCapturer { @@ -806,6 +812,37 @@ } #endif // BUILDFLAG(IS_WIN) +#if BUILDFLAG(IS_MAC) +TEST_F(NativeDesktopMediaListTest, NonDelegatedScopedCGWindowIDCollision) { + CreateCapturerAndModel(); + model_->SetUpdatePeriod(base::Milliseconds(20)); + + constexpr int kTargetWindowId = 12345; + + // 1. Register a ScopedCGWindowID with our target window ID. + viz::FrameSinkId frame_sink_id(1, 1); + remote_cocoa::ScopedCGWindowID scoped_window_id(kTargetWindowId, + frame_sink_id); + + // 2. Set the capturer to return a source with ID kTargetWindowId. + webrtc::DesktopCapturer::SourceList sources; + webrtc::DesktopCapturer::Source source; + source.id = kTargetWindowId; + source.title = "Test Window"; + sources.push_back(source); + window_capturer_->SetWindowList(sources); + + // 3. Trigger an update to the model. + UpdateModel(); + + // 4. Since the capturer is non-delegated (is_source_list_delegated_ is + // false), the window_id should be updated to kTargetWindowId via + // ScopedCGWindowID. + ASSERT_GT(model_->GetSourceCount(), 0); + EXPECT_EQ(model_->GetSource(0).id.window_id, kTargetWindowId); +} +#endif // BUILDFLAG(IS_MAC) + class DelegatedFakeScreenCapturer : public FakeScreenCapturer, public webrtc::DelegatedSourceListController { @@ -1035,3 +1072,58 @@ WaitForCapturerTasks(); EXPECT_EQ(1, capturer_->ensure_visible_call_count()); } + +#if BUILDFLAG(IS_MAC) +class NativeDesktopMediaListDelegatedWindowTest : public ChromeViewsTestBase { + public: + NativeDesktopMediaListDelegatedWindowTest() { + auto capturer = std::make_unique<DelegatedFakeScreenCapturer>(); + capturer_ = capturer.get(); + model_ = std::make_unique<NativeDesktopMediaList>( + DesktopMediaList::Type::kWindow, std::move(capturer)); + } + + ~NativeDesktopMediaListDelegatedWindowTest() override = default; + + void UpdateModel() { + base::RunLoop run_loop; + base::OnceClosure update_consumer = + base::BindLambdaForTesting([&]() { run_loop.Quit(); }); + model_->Update(std::move(update_consumer)); + run_loop.Run(); + } + + protected: + MockObserver observer_; + std::unique_ptr<NativeDesktopMediaList> model_; + raw_ptr<DelegatedFakeScreenCapturer> capturer_; +}; + +TEST_F(NativeDesktopMediaListDelegatedWindowTest, + DelegatedScopedCGWindowIDCollision) { + constexpr int kTargetWindowId = 12345; + + // 1. Register a ScopedCGWindowID with our target window ID. + viz::FrameSinkId frame_sink_id(1, 1); + remote_cocoa::ScopedCGWindowID scoped_window_id(kTargetWindowId, + frame_sink_id); + + // 2. Set the capturer to return a source with ID kTargetWindowId. + webrtc::DesktopCapturer::SourceList sources; + webrtc::DesktopCapturer::Source source; + source.id = kTargetWindowId; + source.title = "Test Window"; + sources.push_back(source); + capturer_->SetSourceList(sources); + + // 3. Trigger an update to the model and wait for it to finish. + UpdateModel(); + + // 4. Since the capturer is delegated (is_source_list_delegated_ is true), + // the window_id should NOT be set to kTargetWindowId (it should remain + // kNullId). + ASSERT_GT(model_->GetSourceCount(), 0); + EXPECT_EQ(model_->GetSource(0).id.window_id, + content::DesktopMediaID::kNullId); +} +#endif // BUILDFLAG(IS_MAC) diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn index a46d2b8f..565856c 100644 --- a/chrome/test/BUILD.gn +++ b/chrome/test/BUILD.gn @@ -10359,6 +10359,9 @@ if (is_win) { deps += [ "//chrome/browser/win:registry_watcher" ] } + if (is_mac) { + deps += [ "//components/remote_cocoa/browser" ] + } } if (toolkit_views) {
Regression Test / PoC
diff --git a/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc b/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc
index 28f9fe9..c9fc4e3 100644
--- a/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc
+++ b/chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc
@@ -47,6 +47,10 @@
#include "base/strings/string_util_win.h"
#endif
+#if BUILDFLAG(IS_MAC)
+#include "components/remote_cocoa/browser/scoped_cg_window_id.h"
+#endif
+
using content::DesktopMediaID;
using testing::_;
using testing::DoAll;
@@ -141,6 +145,8 @@
~FakeScreenCapturer() override = default;
+ void SetSourceList(const SourceList& screens) { screens_ = screens; }
+
// ThumbnailCapturer implementation.
void Start(Consumer* consumer) override { consumer_ = consumer; }
@@ -157,17 +163,17 @@
}
bool GetSourceList(SourceList* screens) override {
- screens->push_back({0});
+ *screens = screens_;
return true;
}
bool SelectSource(SourceId id) override {
- EXPECT_EQ(0, id);
return true;
}
protected:
raw_ptr<Consumer> consumer_;
+ SourceList screens_ = {{0}};
};
class FakeWindowCapturer : public ThumbnailCapturer {
@@ -806,6 +812,37 @@
}
#endif // BUILDFLAG(IS_WIN)
+#if BUILDFLAG(IS_MAC)
+TEST_F(NativeDesktopMediaListTest, NonDelegatedScopedCGWindowIDCollision) {
+ CreateCapturerAndModel();
+ model_->SetUpdatePeriod(base::Milliseconds(20));
+
+ constexpr int kTargetWindowId = 12345;
+
+ // 1. Register a ScopedCGWindowID with our target window ID.
+ viz::FrameSinkId frame_sink_id(1, 1);
+ remote_cocoa::ScopedCGWindowID scoped_window_id(kTargetWindowId,
+ frame_sink_id);
+
+ // 2. Set the capturer to return a source with ID kTargetWindowId.
+ webrtc::DesktopCapturer::SourceList sources;
+ webrtc::DesktopCapturer::Source source;
+ source.id = kTargetWindowId;
+ source.title = "Test Window";
+ sources.push_back(source);
+ window_capturer_->SetWindowList(sources);
+
+ // 3. Trigger an update to the model.
+ UpdateModel();
+
+ // 4. Since the capturer is non-delegated (is_source_list_delegated_ is
+ // false), the window_id should be updated to kTargetWindowId via
+ // ScopedCGWindowID.
+ ASSERT_GT(model_->GetSourceCount(), 0);
+ EXPECT_EQ(model_->GetSource(0).id.window_id, kTargetWindowId);
+}
+#endif // BUILDFLAG(IS_MAC)
+
class DelegatedFakeScreenCapturer
: public FakeScreenCapturer,
public webrtc::DelegatedSourceListController {
@@ -1035,3 +1072,58 @@
WaitForCapturerTasks();
EXPECT_EQ(1, capturer_->ensure_visible_call_count());
}
+
+#if BUILDFLAG(IS_MAC)
+class NativeDesktopMediaListDelegatedWindowTest : public ChromeViewsTestBase {
+ public:
+ NativeDesktopMediaListDelegatedWindowTest() {
+ auto capturer = std::make_unique<DelegatedFakeScreenCapturer>();
+ capturer_ = capturer.get();
+ model_ = std::make_unique<NativeDesktopMediaList>(
+ DesktopMediaList::Type::kWindow, std::move(capturer));
+ }
+
+ ~NativeDesktopMediaListDelegatedWindowTest() override = default;
+
+ void UpdateModel() {
+ base::RunLoop run_loop;
+ base::OnceClosure update_consumer =
+ base::BindLambdaForTesting([&]() { run_loop.Quit(); });
+ model_->Update(std::move(update_consumer));
+ run_loop.Run();
+ }
+
+ protected:
+ MockObserver observer_;
+ std::unique_ptr<NativeDesktopMediaList> model_;
+ raw_ptr<DelegatedFakeScreenCapturer> capturer_;
+};
+
+TEST_F(NativeDesktopMediaListDelegatedWindowTest,
+ DelegatedScopedCGWindowIDCollision) {
+ constexpr int kTargetWindowId = 12345;
+
+ // 1. Register a ScopedCGWindowID with our target window ID.
+ viz::FrameSinkId frame_sink_id(1, 1);
+ remote_cocoa::ScopedCGWindowID scoped_window_id(kTargetWindowId,
+ frame_sink_id);
+
+ // 2. Set the capturer to return a source with ID kTargetWindowId.
+ webrtc::DesktopCapturer::SourceList sources;
+ webrtc::DesktopCapturer::Source source;
+ source.id = kTargetWindowId;
+ source.title = "Test Window";
+ sources.push_back(source);
+ capturer_->SetSourceList(sources);
+
+ // 3. Trigger an update to the model and wait for it to finish.
+ UpdateModel();
+
+ // 4. Since the capturer is delegated (is_source_list_delegated_ is true),
+ // the window_id should NOT be set to kTargetWindowId (it should remain
+ // kNullId).
+ ASSERT_GT(model_->GetSourceCount(), 0);
+ EXPECT_EQ(model_->GetSource(0).id.window_id,
+ content::DesktopMediaID::kNullId);
+}
+#endif // BUILDFLAG(IS_MAC)
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index a46d2b8f..565856c 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -10359,6 +10359,9 @@
if (is_win) {
deps += [ "//chrome/browser/win:registry_watcher" ]
}
+ if (is_mac) {
+ deps += [ "//components/remote_cocoa/browser" ]
+ }
}
if (toolkit_views) {
Original Bug Report
Potential security bypass in macOS GetUpdatedWindowId allows unauthorized cross-window capture
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: On macOS, the lack of an is_source_list_delegated check in GetUpdatedWindowId allows sequential picker session IDs to collide with valid CGWindowID numbers of other Chrome windows. If a collision occurs, the browser can bypass OS-level TCC permission checks and capture an unrelated Chrome window (such as from another profile or incognito session) via the internal viz FrameSink path. This could allow an attacker to obtain a live stream of unauthorized windows.
Affected files:
chrome/browser/media/webrtc/native_desktop_media_list.cccontent/browser/media/capture/native_screen_capture_picker_mac.mmcontent/browser/renderer_host/media/in_process_video_capture_device_launcher.cccontent/browser/media/capture/views_widget_video_capture_device_mac.ccchrome/browser/media/webrtc/display_media_access_handler.cc
Estimated timestamp from git blame: 2024-08-15
Description
Root Cause
In chrome/browser/media/webrtc/native_desktop_media_list.cc, GetUpdatedWindowId() determines if a DesktopMediaID.id maps to one of Chrome’s own windows to leverage the viz FrameSink fast-path. Because delegated-source-list capturers emit IDs from a sequential namespace (such as picker session counters), these sequential IDs can collide with real platform window IDs. To prevent type confusion, an is_source_list_delegated guard is implemented.
While the USE_AURA branch is gated properly by this check, the BUILDFLAG(IS_MAC) branch is not:
#if defined(USE_AURA)
if (!is_source_list_delegated) {
DesktopMediaID::Id search_id = desktop_media_id.id;
...
}
#elif BUILDFLAG(IS_MAC)
if (remote_cocoa::ScopedCGWindowID::Get(desktop_media_id.id)) { // LACKS is_source_list_delegated check
window_id = desktop_media_id.id;
}
#endif
When kUseSCContentSharingPicker is active on macOS 14/15+, delegated picker session IDs are generated as sequential integers representing active picker source IDs:
// content/browser/media/capture/native_screen_capture_picker_mac.mm
active_picker_source_id_++; // Increments per Open()
...
source.id = session_id; // Assigns active_picker_source_id_
std::move(picker_callback_).Run(source);
At the same time, ScopedCGWindowID tracks all active Chrome windows globally. Because macOS CGWindowID numbers are low sequential integers, they can overlap with values from the picker session counter.
On collision, the lack of the is_source_list_delegated guard causes GetUpdatedWindowId() to find a hit via ScopedCGWindowID::Get(desktop_media_id.id) and overwrite window_id with this value. This converts a delegated picker source ID into a valid internal Chromium window_id at the picker UI layer.
Potential Impact & Bypasses
- TCC Bypass: In
DisplayMediaAccessHandler::OnPickerDialogResults(), the presence of a non-nullwindow_idcauses Chrome to skip the system-level screen-capture permission check (TCC), assuming internal capture of an owned window is safe. - Launcher Short-Circuit: During device launch in
InProcessVideoCaptureDeviceLauncher::LaunchDeviceAsync(), the launcher findsdesktop_id.window_id != DesktopMediaID::kNullIdand routes the capture directly to the internal viz FrameSink path (ViewsWidgetVideoCaptureDeviceMac), completely bypassing the ScreenCaptureKit capture path and its safety checks.
Potential Steps to Reproduce
Note: These steps are theoretical, as our tooling has not executed a working proof of concept.
- Run Chrome on macOS 14+ with
kUseSCContentSharingPickeractive. - Open multiple Chrome windows (such as an Incognito window or a window in another user profile) to register valid windows in the
ScopedCGWindowIDmap. - From an attacker-controlled page, call
getDisplayMedia()repeatedly to incrementactive_picker_source_id_close to theCGWindowIDof one of the target windows. - Once a collision is reached, have the user select any innocuous window from the macOS native picker interface.
- Verify if the resulting stream captures the contents of the unrelated target Chrome window rather than the user-selected window.
Suggested Fix
To prevent this type confusion, gate the macOS lookup branch in GetUpdatedWindowId() with the !is_source_list_delegated check, aligning it with the Aura implementation:
#elif BUILDFLAG(IS_MAC)
if (!is_source_list_delegated && remote_cocoa::ScopedCGWindowID::Get(desktop_media_id.id)) {
window_id = desktop_media_id.id;
}
#endif
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.