Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient data validation in MediaCapture
DescriptionInsufficient data validation in MediaCapture
ComponentMediaCapture
Bug ClassLogic Error
Tracker517183713
Fix commit8dfa225f9f6e (chromium/src) +99/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
FakeWindowCapturer
chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc
modified
TEST_F
chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc
modified
DelegatedFakeScreenCapturer
chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc
modified
NativeDesktopMediaListDelegatedWindowTest
chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc
modified
NativeDesktopMediaListDelegatedWindowTest
chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc
modified
BindLambdaForTesting
chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc
modified
if
chrome/test/BUILD.gn
modified

Files Changed

  • chrome/browser/media/webrtc/native_desktop_media_list.cc
  • chrome/browser/media/webrtc/native_desktop_media_list_unittest.cc
  • chrome/test/BUILD.gn
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) {
Loading diff…

Regression Test / PoC

shipped with the fix
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) {
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • content/browser/media/capture/native_screen_capture_picker_mac.mm
  • content/browser/renderer_host/media/in_process_video_capture_device_launcher.cc
  • content/browser/media/capture/views_widget_video_capture_device_mac.cc
  • chrome/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

  1. TCC Bypass: In DisplayMediaAccessHandler::OnPickerDialogResults(), the presence of a non-null window_id causes Chrome to skip the system-level screen-capture permission check (TCC), assuming internal capture of an owned window is safe.
  2. Launcher Short-Circuit: During device launch in InProcessVideoCaptureDeviceLauncher::LaunchDeviceAsync(), the launcher finds desktop_id.window_id != DesktopMediaID::kNullId and 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.

  1. Run Chrome on macOS 14+ with kUseSCContentSharingPicker active.
  2. Open multiple Chrome windows (such as an Incognito window or a window in another user profile) to register valid windows in the ScopedCGWindowID map.
  3. From an attacker-controlled page, call getDisplayMedia() repeatedly to increment active_picker_source_id_ close to the CGWindowID of one of the target windows.
  4. Once a collision is reached, have the user select any innocuous window from the macOS native picker interface.
  5. 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.

View on issue tracker