CVE-2026-95317
Overview
Background
- `DesktopCaptureAccessHandler`
- the Chrome browser-side handler that services screen and window capture requests coming through the
chrome.desktopCaptureextension API. - `CaptureAccessHandlerBase`
- the shared base class that tracks each capture session’s target, initializing every new session to a default target of
TYPE_WEB_CONTENTSand expecting subclasses to callUpdateTarget()once the real target is resolved. - `IsInsecureCapturingInProgress()`
- a query, keyed by render process and frame ID, that reports whether a given
WebContentsis currently being captured over an insecure (e.g. full-screen) capture so the UI can surface warnings and protections. - `DesktopMediaID`
- the identifier describing the actual capture target (a screen, a window, or a specific tab) that must be plumbed to the tracker to record what is really being captured.
Root Cause Analysis
When a desktop capture session started via chrome.desktopCapture, CaptureAccessHandlerBase seeded the session’s tracked target with the default TYPE_WEB_CONTENTS value pointing at the requesting extension’s tab. DisplayMediaAccessHandler corrected this by calling UpdateTarget() with the resolved DesktopMediaID, but DesktopCaptureAccessHandler::OnDesktopCaptureDevicesObtained() never did, so a screen or window capture stayed recorded as merely capturing the initiating tab. Consequently IsInsecureCapturingInProgress() returned false for every other tab, even though a full-screen capture was in fact recording their content.
The fix plumbs the resolved media_id into OnDesktopCaptureDevicesObtained() and calls UpdateTarget(pending_request->request, media_id) right after UpdateExtensionTrusted(), so the tracker reflects the true capture target and the insecure-capture query answers correctly for all affected tabs.
DesktopCaptureAccessHandler never overrode the base class’s placeholder capture target with the resolved DesktopMediaID, leaving screen/window captures misattributed to the requesting tab; the fix adds the missing UpdateTarget() call so the true target is tracked and IsInsecureCapturingInProgress() reports accurately.Attack Path
- Initiate desktop capture
A page or extension invokes the
chrome.desktopCaptureAPI and the user grants a full-screen (TYPE_SCREEN) capture throughDesktopCaptureAccessHandler. - Target left as default
Because
OnDesktopCaptureDevicesObtained()never calledUpdateTarget(), the session stays tracked with the base defaultTYPE_WEB_CONTENTSpointing at the requesting tab rather than the whole screen. - Insecure-capture check misfires
For any unrelated tab,
IsInsecureCapturingInProgress()returnsfalse, so protections and warnings that gate on active insecure capture are not applied to those tabs even while their content is on screen and being captured. - Sensitive content captured without warning Content in other tabs (e.g. pages that would normally suppress or warn during insecure capture) is recorded through the screen stream while the browser believes only the initiating tab is affected. (inferred)
Impact Assessment
chrome.desktopCapture obtains an incorrect-authorization/tracking state in the browser process where IsInsecureCapturingInProgress() wrongly reports false for tabs other than the requester, defeating the insecure-capture protections meant to guard their content. The precondition is a granted desktop capture session initiated via the extension API. Any escalation to capturing otherwise-protected content without user awareness is (inferred); the advisory classifies this as incorrect authorization in MediaCapture (LogicError) and states no exploited-in-the-wild status.Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc |
modified |
Files Changed
chrome/browser/media/webrtc/desktop_capture_access_handler.hchrome/browser/media/webrtc/desktop_capture_access_handler.ccchrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
Audit Directions
- Missing base-class contract callsAudit every subclass of
CaptureAccessHandlerBase(and similar base classes seeding placeholder state) to confirm each callsUpdateTarget()once the real target resolves, comparingDesktopCaptureAccessHandleragainstDisplayMediaAccessHandler. - Default-state that must be overriddenLook for objects initialized to a default/placeholder value (like
TYPE_WEB_CONTENTS) that security decisions later depend on, and verify the resolved value is always written before those decisions are made. - Authorization queries keyed by process/frameReview callers of
IsInsecureCapturingInProgress()and related capture-state predicates to ensure their tracking data is populated for all affectedWebContents, not just the requester.
Patch
From 0c9c804164cb32144c334bd2086fdabb9fe4596f Mon Sep 17 00:00:00 2001
From: Sangbaek Park <sangbaekpark@chromium.org>
Date: Wed, 19 Aug 2026 08:35:26 -0700
Subject: [PATCH] media: Track capture target in DesktopCaptureAccessHandler
CaptureAccessHandlerBase initializes each new capture session with a
default target of TYPE_WEB_CONTENTS (pointing to the requesting
tab) and expects subclasses to call UpdateTarget() once the real
capture target is resolved.
While DisplayMediaAccessHandler calls UpdateTarget(),
DesktopCaptureAccessHandler previously did not. As a result, screen
or window captures initiated via the chrome.desktopCapture API
remained tracked as capturing only the requesting extension's tab,
causing IsInsecureCapturingInProgress() to return false for all
other tabs.
This CL resolves the issue by:
- Plumbing the resolved DesktopMediaID into
OnDesktopCaptureDevicesObtained().
- Calling UpdateTarget() immediately after UpdateExtensionTrusted().
- Adding a unit test to verify that full-screen capture is reported
as insecure capture for unrelated WebContents.
Test: unit_tests --gtest_filter="DesktopCaptureAccessHandlerTest.*"
Bug: 514487499
Change-Id: I46ba85a01c98271d4c701ef9d3bcd79f26829229
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8266204
Reviewed-by: Tomas Gunnarsson <tommi@chromium.org>
Commit-Queue: Sangbaek Park <sangbaekpark@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1682334}
---
diff --git a/chrome/browser/media/webrtc/desktop_capture_access_handler.cc b/chrome/browser/media/webrtc/desktop_capture_access_handler.cc
index c3d1f7d..6769ee97 100644
--- a/chrome/browser/media/webrtc/desktop_capture_access_handler.cc
+++ b/chrome/browser/media/webrtc/desktop_capture_access_handler.cc
@@ -823,7 +823,7 @@
auto on_desktop_capture_devices_obtained_callback = base::BindOnce(
&DesktopCaptureAccessHandler::OnDesktopCaptureDevicesObtained,
base::Unretained(this), web_contents->GetWeakPtr(),
- std::move(pending_request));
+ std::move(pending_request), media_id);
std::move(get_devices_for_desktop_capture_callback)
.Run(std::move(on_desktop_capture_devices_obtained_callback));
}
@@ -831,6 +831,7 @@
void DesktopCaptureAccessHandler::OnDesktopCaptureDevicesObtained(
base::WeakPtr<content::WebContents> web_contents,
std::unique_ptr<PendingAccessRequest> pending_request,
+ const content::DesktopMediaID& media_id,
blink::mojom::StreamDevices devices,
std::unique_ptr<content::MediaStreamUI> ui) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
@@ -842,6 +843,7 @@
UpdateExtensionTrusted(pending_request->request,
pending_request->is_allowlisted_extension);
+ UpdateTarget(pending_request->request, media_id);
blink::mojom::StreamDevicesSet stream_devices_set;
stream_devices_set.stream_devices.emplace_back(
diff --git a/chrome/browser/media/webrtc/desktop_capture_access_handler.h b/chrome/browser/media/webrtc/desktop_capture_access_handler.h
index 2c5e0b6..1a287a8 100644
--- a/chrome/browser/media/webrtc/desktop_capture_access_handler.h
+++ b/chrome/browser/media/webrtc/desktop_capture_access_handler.h
@@ -104,6 +104,7 @@
void OnDesktopCaptureDevicesObtained(
base::WeakPtr<content::WebContents> web_contents,
std::unique_ptr<PendingAccessRequest> pending_request,
+ const content::DesktopMediaID& media_id,
blink::mojom::StreamDevices devices,
std::unique_ptr<content::MediaStreamUI> ui);
diff --git a/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc b/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
index 052f3a4..516ff3e 100644
--- a/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
+++ b/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
@@ -26,6 +26,7 @@
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
+#include "content/public/test/web_contents_tester.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "media/base/media_switches.h"
@@ -398,6 +399,36 @@
EXPECT_TRUE(devices.video_device.has_value());
}
+TEST_F(DesktopCaptureAccessHandlerTest,
+ GenerateStreamReportsInsecureCaptureForOtherTabs) {
+ std::unique_ptr<content::WebContents> other_web_contents =
+ content::WebContentsTester::CreateTestWebContents(profile(), nullptr);
+ content::RenderFrameHost* other_rfh =
+ other_web_contents->GetPrimaryMainFrame();
+
+ const GURL origin(kOrigin);
+ const std::string id =
+ content::DesktopStreamsRegistry::GetInstance()->RegisterStream(
+ web_contents()
+ ->GetPrimaryMainFrame()
+ ->GetProcess()
+ ->GetDeprecatedID(),
+ web_contents()->GetPrimaryMainFrame()->GetRoutingID(),
+ url::Origin::Create(origin),
+ content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN,
+ content::DesktopMediaID::kFakeId),
+ content::DesktopStreamRegistryType::kRegistryStreamTypeDesktop);
+
+ blink::mojom::MediaStreamRequestResult result;
+ blink::mojom::StreamDevices devices;
+ ProcessGenerateStreamRequest({id}, origin, /*extension=*/nullptr, &result,
+ &devices);
+ ASSERT_EQ(blink::mojom::MediaStreamRequestResult::OK, result);
+
+ EXPECT_TRUE(access_handler_->IsInsecureCapturingInProgress(
+ other_rfh->GetProcess()->GetDeprecatedID(), other_rfh->GetRoutingID()));
+}
+
TEST_F(DesktopCaptureAccessHandlerTest, ScreenCaptureAccessSuccess) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kEnableUserMediaScreenCapturing);
Regression Test / PoC
diff --git a/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc b/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
index 052f3a4..516ff3e 100644
--- a/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
+++ b/chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
@@ -26,6 +26,7 @@
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
+#include "content/public/test/web_contents_tester.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "media/base/media_switches.h"
@@ -398,6 +399,36 @@
EXPECT_TRUE(devices.video_device.has_value());
}
+TEST_F(DesktopCaptureAccessHandlerTest,
+ GenerateStreamReportsInsecureCaptureForOtherTabs) {
+ std::unique_ptr<content::WebContents> other_web_contents =
+ content::WebContentsTester::CreateTestWebContents(profile(), nullptr);
+ content::RenderFrameHost* other_rfh =
+ other_web_contents->GetPrimaryMainFrame();
+
+ const GURL origin(kOrigin);
+ const std::string id =
+ content::DesktopStreamsRegistry::GetInstance()->RegisterStream(
+ web_contents()
+ ->GetPrimaryMainFrame()
+ ->GetProcess()
+ ->GetDeprecatedID(),
+ web_contents()->GetPrimaryMainFrame()->GetRoutingID(),
+ url::Origin::Create(origin),
+ content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN,
+ content::DesktopMediaID::kFakeId),
+ content::DesktopStreamRegistryType::kRegistryStreamTypeDesktop);
+
+ blink::mojom::MediaStreamRequestResult result;
+ blink::mojom::StreamDevices devices;
+ ProcessGenerateStreamRequest({id}, origin, /*extension=*/nullptr, &result,
+ &devices);
+ ASSERT_EQ(blink::mojom::MediaStreamRequestResult::OK, result);
+
+ EXPECT_TRUE(access_handler_->IsInsecureCapturingInProgress(
+ other_rfh->GetProcess()->GetDeprecatedID(), other_rfh->GetRoutingID()));
+}
+
TEST_F(DesktopCaptureAccessHandlerTest, ScreenCaptureAccessSuccess) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kEnableUserMediaScreenCapturing);
Original Bug Report
Potential DRM Bypass via missing UpdateTarget call in DesktopCaptureAccessHandler
Flapjack, 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: DesktopCaptureAccessHandler fails to call UpdateTarget when initializing a capture session via the chrome.desktopCapture API. This causes the browser to register the capture target as the requesting extension rather than the actual screen or window, bypassing OutputProtectionProxy checks and potentially allowing extensions to record DRM-protected content.
Affected files:
chrome/browser/media/webrtc/desktop_capture_access_handler.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A logic flaw exists in DesktopCaptureAccessHandler that prevents the browser from accurately tracking which screen or window is being recorded via the chrome.desktopCapture extension API. This failure bypasses the OutputProtectionProxy security checks, potentially allowing untrusted extensions to record high-quality DRM-protected content (e.g., Widevine L3) playing in other tabs.
When an extension uses chrome.desktopCapture.chooseDesktopMedia and subsequently calls getUserMedia, the request is handled by DesktopCaptureAccessHandler::HandleRequest(). After resolving the target DesktopMediaID and initializing the capture devices, the handler registers the session with its base class by calling UpdateExtensionTrusted() (which calls CaptureAccessHandlerBase::UpdateTrusted()).
Inside CaptureAccessHandlerBase::AddCaptureSession(), a new capture session is created with a default capturing_type of TYPE_WEB_CONTENTS and the target set to the requesting extension’s WebContents. A comment explicitly notes: // Assume that the target is the same tab that is requesting capture... This can be changed by calling UpdateTarget().
However, DesktopCaptureAccessHandler never calls UpdateTarget() (unlike DisplayMediaAccessHandler, which correctly does so). Consequently, the capture session remains permanently registered as only capturing the extension’s own tab, regardless of whether the user actually selected their entire screen or a specific window.
When a separate tab playing DRM-protected content queries the browser to ensure the display environment is secure, OutputProtectionProxy::ProcessQueryStatusResult() calls CaptureAccessHandlerBase::IsInsecureCapturingInProgress(). This method iterates through active sessions and calls MatchesSession(). Because the attacker’s session is incorrectly marked as TYPE_WEB_CONTENTS targeting the extension, MatchesSession() returns false for the DRM tab. The browser incorrectly concludes that no insecure capture is occurring, the CDM releases high-quality video frames, and the extension successfully records them.
Suggested Reproduction Steps
Note: These steps represent a theoretical execution path based on static analysis, as our tooling cannot run live code.
- Create a Chrome extension with the
desktopCapturepermission and install it. - In a separate browser tab, navigate to a site playing DRM-protected video (e.g., Widevine L3 content).
- Trigger the extension to call
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window']). - As the user, select the entire screen or the specific window containing the DRM tab.
- The extension uses the returned
streamIdto callnavigator.mediaDevices.getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: streamId } } }). - Observe that the extension receives a media stream containing high-quality frames of the DRM content, bypassing the expected output protection blanking.
Suggested Fix
In DesktopCaptureAccessHandler::OnDesktopCaptureDevicesObtained(), call UpdateTarget() after calling UpdateExtensionTrusted() to ensure the capture session accurately reflects the user’s selected DesktopMediaID:
void DesktopCaptureAccessHandler::OnDesktopCaptureDevicesObtained(
// ...
UpdateExtensionTrusted(pending_request->request,
pending_request->is_allowlisted_extension);
// Added fix:
// We need to resolve the stream ID to a DesktopMediaID to call UpdateTarget.
// (This requires passing the media_id from AcceptRequest down to this callback,
// similar to how DisplayMediaAccessHandler does it).
UpdateTarget(pending_request->request, media_id);
// ...
}
Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0
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.