Medium watch CVSS 3.1 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in MediaCapture
DescriptionIncorrect authorization in MediaCapture
ComponentMediaCapture
Sub-componentMedia
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker514487499
Fix commit0c9c804164cb (chromium/src) +35/-1
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-19 (per vendor advisory)
Fix landed2026-08-19 (public git)
Disclosed2026-09-22
Patch gap34 days — fix public in source before the advisory

Background

`DesktopCaptureAccessHandler`
the Chrome browser-side handler that services screen and window capture requests coming through the chrome.desktopCapture extension API.
`CaptureAccessHandlerBase`
the shared base class that tracks each capture session’s target, initializing every new session to a default target of TYPE_WEB_CONTENTS and expecting subclasses to call UpdateTarget() once the real target is resolved.
`IsInsecureCapturingInProgress()`
a query, keyed by render process and frame ID, that reports whether a given WebContents is 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.

Key insight
The single mistake is that 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

  1. Initiate desktop capture A page or extension invokes the chrome.desktopCapture API and the user grants a full-screen (TYPE_SCREEN) capture through DesktopCaptureAccessHandler.
  2. Target left as default Because OnDesktopCaptureDevicesObtained() never called UpdateTarget(), the session stays tracked with the base default TYPE_WEB_CONTENTS pointing at the requesting tab rather than the whole screen.
  3. Insecure-capture check misfires For any unrelated tab, IsInsecureCapturingInProgress() returns false, 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.
  4. 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

An attacker performing a screen/window capture through 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

FunctionChangeNotes
TEST_F
chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc
modified

Files Changed

  • chrome/browser/media/webrtc/desktop_capture_access_handler.h
  • chrome/browser/media/webrtc/desktop_capture_access_handler.cc
  • chrome/browser/media/webrtc/desktop_capture_access_handler_unittest.cc

Audit Directions

  • Missing base-class contract calls
    Audit every subclass of CaptureAccessHandlerBase (and similar base classes seeding placeholder state) to confirm each calls UpdateTarget() once the real target resolves, comparing DesktopCaptureAccessHandler against DisplayMediaAccessHandler.
  • Default-state that must be overridden
    Look 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/frame
    Review callers of IsInsecureCapturingInProgress() and related capture-state predicates to ensure their tracking data is populated for all affected WebContents, not just the requester.
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);
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by rj...@google.com

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.

  1. Create a Chrome extension with the desktopCapture permission and install it.
  2. In a separate browser tab, navigate to a site playing DRM-protected video (e.g., Widevine L3 content).
  3. Trigger the extension to call chrome.desktopCapture.chooseDesktopMedia(['screen', 'window']).
  4. As the user, select the entire screen or the specific window containing the DRM tab.
  5. The extension uses the returned streamId to call navigator.mediaDevices.getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: streamId } } }).
  6. 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.

View on issue tracker