CVE-2026-9959
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmodules/desktop_capture/cropping_window_capturer.cc |
modified |
Files Changed
modules/desktop_capture/cropping_window_capturer.ccmodules/desktop_capture/cropping_window_capturer.h
Patch
From 22f071a33eb46cbabcbf05961127dde7a2e3d908 Mon Sep 17 00:00:00 2001 From: mark a. foltz <[email protected]> Date: Mon, 27 Apr 2026 11:55:24 -0700 Subject: [PATCH] [desktop capture] Fix TOCTOU race condition in CroppingWindowCapturer. Crop the frame using the window rectangle captured when the capture was initiated, rather than re-querying the rectangle after the capture finishes. Bug: chromium:504557432 Change-Id: I459d528ac343163fde9a9dbf6c40a542c1a04c61 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/467820 Reviewed-by: Stefan Holmer <[email protected]> Reviewed-by: Alexander Cooper <[email protected]> Commit-Queue: Mark Foltz <[email protected]> Cr-Commit-Position: refs/heads/main@{#47575} --- diff --git a/modules/desktop_capture/cropping_window_capturer.cc b/modules/desktop_capture/cropping_window_capturer.cc index b0cbc70..3479044 100644 --- a/modules/desktop_capture/cropping_window_capturer.cc +++ b/modules/desktop_capture/cropping_window_capturer.cc @@ -47,6 +47,9 @@ void CroppingWindowCapturer::CaptureFrame() { if (ShouldUseScreenCapturer()) { + // We record the window position here at capture time; it may differ at + // frame delivery time. + last_window_rect_ = GetWindowRectInVirtualScreen(); if (!screen_capturer_) { screen_capturer_ = DesktopCapturer::CreateRawScreenCapturer(options_); if (excluded_window_) { @@ -74,6 +77,7 @@ bool CroppingWindowCapturer::SelectSource(SourceId id) { if (window_capturer_->SelectSource(id)) { selected_window_ = id; + last_window_rect_ = {}; return true; } return false; @@ -98,15 +102,14 @@ return; } - DesktopRect window_rect = GetWindowRectInVirtualScreen(); - if (window_rect.is_empty()) { + if (last_window_rect_.is_empty()) { RTC_LOG(LS_WARNING) << "Window rect is empty"; callback_->OnCaptureResult(Result::ERROR_TEMPORARY, nullptr); return; } std::unique_ptr<DesktopFrame> cropped_frame = - CreateCroppedDesktopFrame(std::move(screen_frame), window_rect); + CreateCroppedDesktopFrame(std::move(screen_frame), last_window_rect_); if (!cropped_frame) { RTC_LOG(LS_WARNING) << "Window is outside of the captured display"; diff --git a/modules/desktop_capture/cropping_window_capturer.h b/modules/desktop_capture/cropping_window_capturer.h index 5647803..fe95fc4 100644 --- a/modules/desktop_capture/cropping_window_capturer.h +++ b/modules/desktop_capture/cropping_window_capturer.h @@ -77,6 +77,11 @@ std::unique_ptr<DesktopCapturer> screen_capturer_; SourceId selected_window_; WindowId excluded_window_; + + // The window rectangle in the virtual screen, relative to the top-left corner + // of the virtual screen. This is the rectangle used to crop the frame. + // It is updated in CaptureFrame() and used in OnCaptureResult(). + DesktopRect last_window_rect_; }; } // namespace webrtc
Original Bug Report
Potential arbitrary screen pixel leak via TOCTOU in CroppingWindowCapturerWin
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential time-of-check to time-of-use (TOCTOU) race condition exists in the Windows desktop capturer when falling back to the cropping window capturer. By rapidly moving a shared popup window during capture, a malicious web page could potentially cause the capturer to crop a stale full-screen frame using new window coordinates. This could allow an attacker to read arbitrary screen pixels outside the shared window, bypassing intended sharing boundaries.
Affected files:
third_party/webrtc/modules/desktop_capture/cropping_window_capturer_win.ccthird_party/webrtc/modules/desktop_capture/cropping_window_capturer.cc
Estimated timestamp from git blame: 2025-06-11
Description
A potential Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists in CroppingWindowCapturerWin within the WebRTC desktop capture module on Windows. When Windows Graphics Capture (WGC) is unavailable or disabled, Chrome falls back to CroppingWindowCapturer. This capturer optimizes window capture by taking a full-screen snapshot and cropping it to the target window’s boundaries.
The vulnerability occurs because the target window’s coordinates are re-queried after the full-screen capture has completed, but these new coordinates are then used to crop the old full-screen image. If the window is moved during the screen capture process, pixels from the window’s new location are extracted from the stale full-screen snapshot, potentially leaking the contents of other applications or the underlying desktop.
Root Cause Analysis
In third_party/webrtc/modules/desktop_capture/cropping_window_capturer.cc, the CaptureFrame() method first calls ShouldUseScreenCapturer() to verify the window is on top, and then calls screen_capturer_->CaptureFrame() to perform a synchronous OS-level full-screen capture.
Once the full-screen capture finishes, CroppingWindowCapturer::OnCaptureResult() executes:
void CroppingWindowCapturer::OnCaptureResult(...) {
if (!ShouldUseScreenCapturer()) {
// ...
return;
}
...
DesktopRect window_rect = GetWindowRectInVirtualScreen();
...
std::unique_ptr<DesktopFrame> cropped_frame =
CreateCroppedDesktopFrame(std::move(screen_frame), window_rect);
...
}
The second call to ShouldUseScreenCapturer() invokes the Windows-specific implementation in cropping_window_capturer_win.cc. This function calls the Windows OS API GetWindowRect() and updates the member variable window_region_rect_ with the live, current position of the window:
bool CroppingWindowCapturerWin::ShouldUseScreenCapturer() {
...
if (!GetWindowRect(selected, &window_region_rect_)) {
return false;
}
...
}
Because the full-screen capture takes time, a race window exists. If the shared window is moved during this delay, the second ShouldUseScreenCapturer() call updates window_region_rect_ to the new coordinates. GetWindowRectInVirtualScreen() then retrieves these new coordinates, and CreateCroppedDesktopFrame blindly crops the stale screen_frame using them.
Potential Exploitation Steps
Note: These are suggested steps; our tooling agent does not have the ability to run active code to provide a working proof-of-concept.
- A malicious site opens a popup window and prompts the user to share it via
navigator.mediaDevices.getDisplayMedia(). - The user grants permission, expecting only the popup window’s contents to be shared.
- The malicious site executes a high-frequency JavaScript loop calling
window.moveTo(Target_X, Target_Y), rapidly moving the popup between its original location and an area of the screen the attacker wishes to read (e.g., overlapping another application). - A race condition occurs:
- The browser captures the full desktop while the popup is at its original position.
- The
window.moveTo()IPC moves the window to the target coordinates. - The browser re-queries the window bounds, getting the target coordinates.
- The browser crops the full desktop image using the target coordinates.
- The resulting video frame delivered to the malicious site contains the desktop pixels at the target coordinates, successfully bypassing cross-application and cross-origin boundaries.
Suggested Fix
To resolve this issue, the cropping logic should use the window coordinates that were valid at the exact time the screen capture was initiated, rather than re-querying live coordinates afterward.
- Modify
CroppingWindowCapturerto cache thewindow_rect(orwindow_region_rect_) immediately before callingscreen_capturer_->CaptureFrame(). - Pass this cached rectangle explicitly to
CreateCroppedDesktopFrameinsideOnCaptureResult, ignoring any subsequent positional changes of the window. - Ensure that the second call to
ShouldUseScreenCapturer()does not implicitly mutate state that dictates the cropping boundaries for the current frame.
Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646
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.