CVE-2026-113069
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcomponents/viz/service/frame_sinks/frame_sink_manager_impl.cc |
modified |
Files Changed
components/viz/service/frame_sinks/frame_sink_manager_unittest.cccomponents/viz/service/frame_sinks/frame_sink_manager_impl.cc
Patch
From 1d197eb5a26b0212a49437d5e32d03bee6215e11 Mon Sep 17 00:00:00 2001
From: Palak Agarwal <agpalak@chromium.org>
Date: Tue, 15 Sep 2026 13:08:53 -0700
Subject: [PATCH] Restrict Region Capture frame sink resolution to target hierarchy
This change modifies the function FindCapturableFrameSink to only allow
candidate frame sinks that match target.frame_sink_id or its child frame
sinks in the hierarchy.
Bug: 550238190, 496644575
Change-Id: Ia2a19830ed79b848c3c7e221b4015185b0e6a18a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8358594
Reviewed-by: Kyle Charbonneau <kylechar@chromium.org>
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Reviewed-by: Jordan Bayles <jophba@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1698326}
---
diff --git a/components/viz/service/frame_sinks/frame_sink_manager_impl.cc b/components/viz/service/frame_sinks/frame_sink_manager_impl.cc
index 743df44..a7c11cba 100644
--- a/components/viz/service/frame_sinks/frame_sink_manager_impl.cc
+++ b/components/viz/service/frame_sinks/frame_sink_manager_impl.cc
@@ -831,26 +831,33 @@
CapturableFrameSink* FrameSinkManagerImpl::FindCapturableFrameSink(
const VideoCaptureTarget& target) {
+ if (!target.frame_sink_id.is_valid()) {
+ return nullptr;
+ }
+
// Search the known CompositorFrameSinkSupport objects for region capture
- // bounds matching the crop ID specified by |target| (if one was set), and
- // return the corresponding frame sink.
+ // bounds matching the crop ID specified by `target` (if one was set), and
+ // return the corresponding frame sink. The search must be restricted to
+ // `target.frame_sink_id` and its child frame sinks (e.g. Out-Of-Process
+ // iframes) to prevent capturing an unrelated frame sink (such as an
+ // unselected tab).
if (IsRegionCapture(target.sub_target)) {
const auto crop_id = std::get<RegionCaptureCropId>(target.sub_target);
for (const auto& id_and_sink : support_map_) {
const RegionCaptureBounds& bounds =
id_and_sink.second->current_capture_bounds();
- if (bounds.bounds().contains(crop_id)) {
+ if (!bounds.bounds().contains(crop_id)) {
+ continue;
+ }
+ if (id_and_sink.first == target.frame_sink_id ||
+ ChildContains(target.frame_sink_id, id_and_sink.first)) {
return id_and_sink.second;
}
}
return nullptr;
}
- FrameSinkId frame_sink_id = target.frame_sink_id;
- if (!frame_sink_id.is_valid())
- return nullptr;
-
- return base::FindPtrOrNull(support_map_, frame_sink_id);
+ return base::FindPtrOrNull(support_map_, target.frame_sink_id);
}
void FrameSinkManagerImpl::OnCapturerConnectionLost(
diff --git a/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc b/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
index 81cb55ee..232553c 100644
--- a/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
+++ b/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
@@ -14,6 +14,7 @@
#include "components/viz/common/constants.h"
#include "components/viz/common/frame_sinks/begin_frame_source.h"
#include "components/viz/common/surfaces/parent_local_surface_id_allocator.h"
+#include "components/viz/common/surfaces/region_capture_bounds.h"
#include "components/viz/service/frame_sinks/compositor_frame_sink_support.h"
#include "components/viz/service/gl/mock_gpu_service_impl.h"
#include "components/viz/service/input/mock_input_manager.h"
@@ -115,6 +116,11 @@
return manager_->FindCapturableFrameSink(VideoCaptureTarget(id));
}
+ CapturableFrameSink* FindCapturableFrameSink(
+ const VideoCaptureTarget& target) {
+ return manager_->FindCapturableFrameSink(target);
+ }
+
// Verifies the frame sinks with provided id in |ids| are throttled at
// |interval|.
void VerifyThrottling(base::TimeDelta interval,
@@ -2189,6 +2195,82 @@
manager_->InvalidateFrameSinkId(kFrameSinkIdC, {});
}
+TEST_F(FrameSinkManagerTest,
+ FindCapturableFrameSinkRegionCaptureRestrictedToHierarchy) {
+ manager_->RegisterFrameSinkId(kFrameSinkIdRoot, true);
+ manager_->RegisterFrameSinkId(kFrameSinkIdA, true);
+ manager_->RegisterFrameSinkId(kFrameSinkIdB, true);
+ // Set up hierarchy: Root -> Child (A).
+ // B is an unrelated frame sink (e.g. another tab).
+ manager_->RegisterFrameSinkHierarchy(kFrameSinkIdRoot, kFrameSinkIdA);
+
+ auto sink_root = CreateCompositorFrameSinkSupport(kFrameSinkIdRoot);
+ auto sink_a = CreateCompositorFrameSinkSupport(kFrameSinkIdA);
+ auto sink_b = CreateCompositorFrameSinkSupport(kFrameSinkIdB);
+
+ ParentLocalSurfaceIdAllocator allocator_root;
+ ParentLocalSurfaceIdAllocator allocator_a;
+ ParentLocalSurfaceIdAllocator allocator_b;
+ allocator_root.GenerateId();
+ allocator_a.GenerateId();
+ allocator_b.GenerateId();
+ LocalSurfaceId surface_id_root = allocator_root.GetCurrentLocalSurfaceId();
+ LocalSurfaceId surface_id_a = allocator_a.GetCurrentLocalSurfaceId();
+ LocalSurfaceId surface_id_b = allocator_b.GetCurrentLocalSurfaceId();
+
+ const auto crop_id = RegionCaptureCropId::CreateRandom();
+ RegionCaptureBounds bounds;
+ bounds.Set(crop_id, gfx::Rect(0, 0, 100, 100));
+
+ constexpr gfx::Rect kDamageRect(0, 0, 100, 100);
+
+ // Set crop ID on sink_b.
+ auto frame_b = MakeDefaultCompositorFrame();
+ frame_b.metadata.capture_bounds = bounds;
+ sink_b->OnSurfaceAggregatedDamage(nullptr, surface_id_b, frame_b, kDamageRect,
+ base::TimeTicks::Now());
+
+ // Set empty capture bounds on root and child A.
+ auto empty_frame = MakeDefaultCompositorFrame();
+ sink_root->OnSurfaceAggregatedDamage(nullptr, surface_id_root, empty_frame,
+ kDamageRect, base::TimeTicks::Now());
+ sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, empty_frame,
+ kDamageRect, base::TimeTicks::Now());
+
+ // Searching for crop_id targeting kFrameSinkIdRoot must NOT return sink_b,
+ // since B is not in the hierarchy of Root.
+ VideoCaptureTarget target_root(kFrameSinkIdRoot, crop_id);
+ EXPECT_EQ(FindCapturableFrameSink(target_root), nullptr);
+
+ // Now set the crop ID on child A.
+ auto frame_a = MakeDefaultCompositorFrame();
+ frame_a.metadata.capture_bounds = bounds;
+ sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, frame_a, kDamageRect,
+ base::TimeTicks::Now());
+
+ // Searching for crop_id targeting kFrameSinkIdRoot should find child A.
+ EXPECT_EQ(FindCapturableFrameSink(target_root), sink_a.get());
+
+ // Clear crop ID from child A and set on root itself.
+ sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, empty_frame,
+ kDamageRect, base::TimeTicks::Now());
+ auto frame_root = MakeDefaultCompositorFrame();
+ frame_root.metadata.capture_bounds = bounds;
+ sink_root->OnSurfaceAggregatedDamage(nullptr, surface_id_root, frame_root,
+ kDamageRect, base::TimeTicks::Now());
+
+ // Searching for crop_id targeting kFrameSinkIdRoot should find root.
+ EXPECT_EQ(FindCapturableFrameSink(target_root), sink_root.get());
+
+ // An invalid target frame sink id must return nullptr.
+ VideoCaptureTarget invalid_target;
+ EXPECT_EQ(FindCapturableFrameSink(invalid_target), nullptr);
+
+ manager_->UnregisterFrameSinkHierarchy(kFrameSinkIdRoot, kFrameSinkIdA);
+ manager_->InvalidateFrameSinkId(kFrameSinkIdRoot, {});
+ manager_->InvalidateFrameSinkId(kFrameSinkIdA, {});
+ manager_->InvalidateFrameSinkId(kFrameSinkIdB, {});
+}
} // namespace viz
Regression Test / PoC
diff --git a/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc b/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
index 81cb55ee..232553c 100644
--- a/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
+++ b/components/viz/service/frame_sinks/frame_sink_manager_unittest.cc
@@ -14,6 +14,7 @@
#include "components/viz/common/constants.h"
#include "components/viz/common/frame_sinks/begin_frame_source.h"
#include "components/viz/common/surfaces/parent_local_surface_id_allocator.h"
+#include "components/viz/common/surfaces/region_capture_bounds.h"
#include "components/viz/service/frame_sinks/compositor_frame_sink_support.h"
#include "components/viz/service/gl/mock_gpu_service_impl.h"
#include "components/viz/service/input/mock_input_manager.h"
@@ -115,6 +116,11 @@
return manager_->FindCapturableFrameSink(VideoCaptureTarget(id));
}
+ CapturableFrameSink* FindCapturableFrameSink(
+ const VideoCaptureTarget& target) {
+ return manager_->FindCapturableFrameSink(target);
+ }
+
// Verifies the frame sinks with provided id in |ids| are throttled at
// |interval|.
void VerifyThrottling(base::TimeDelta interval,
@@ -2189,6 +2195,82 @@
manager_->InvalidateFrameSinkId(kFrameSinkIdC, {});
}
+TEST_F(FrameSinkManagerTest,
+ FindCapturableFrameSinkRegionCaptureRestrictedToHierarchy) {
+ manager_->RegisterFrameSinkId(kFrameSinkIdRoot, true);
+ manager_->RegisterFrameSinkId(kFrameSinkIdA, true);
+ manager_->RegisterFrameSinkId(kFrameSinkIdB, true);
+ // Set up hierarchy: Root -> Child (A).
+ // B is an unrelated frame sink (e.g. another tab).
+ manager_->RegisterFrameSinkHierarchy(kFrameSinkIdRoot, kFrameSinkIdA);
+
+ auto sink_root = CreateCompositorFrameSinkSupport(kFrameSinkIdRoot);
+ auto sink_a = CreateCompositorFrameSinkSupport(kFrameSinkIdA);
+ auto sink_b = CreateCompositorFrameSinkSupport(kFrameSinkIdB);
+
+ ParentLocalSurfaceIdAllocator allocator_root;
+ ParentLocalSurfaceIdAllocator allocator_a;
+ ParentLocalSurfaceIdAllocator allocator_b;
+ allocator_root.GenerateId();
+ allocator_a.GenerateId();
+ allocator_b.GenerateId();
+ LocalSurfaceId surface_id_root = allocator_root.GetCurrentLocalSurfaceId();
+ LocalSurfaceId surface_id_a = allocator_a.GetCurrentLocalSurfaceId();
+ LocalSurfaceId surface_id_b = allocator_b.GetCurrentLocalSurfaceId();
+
+ const auto crop_id = RegionCaptureCropId::CreateRandom();
+ RegionCaptureBounds bounds;
+ bounds.Set(crop_id, gfx::Rect(0, 0, 100, 100));
+
+ constexpr gfx::Rect kDamageRect(0, 0, 100, 100);
+
+ // Set crop ID on sink_b.
+ auto frame_b = MakeDefaultCompositorFrame();
+ frame_b.metadata.capture_bounds = bounds;
+ sink_b->OnSurfaceAggregatedDamage(nullptr, surface_id_b, frame_b, kDamageRect,
+ base::TimeTicks::Now());
+
+ // Set empty capture bounds on root and child A.
+ auto empty_frame = MakeDefaultCompositorFrame();
+ sink_root->OnSurfaceAggregatedDamage(nullptr, surface_id_root, empty_frame,
+ kDamageRect, base::TimeTicks::Now());
+ sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, empty_frame,
+ kDamageRect, base::TimeTicks::Now());
+
+ // Searching for crop_id targeting kFrameSinkIdRoot must NOT return sink_b,
+ // since B is not in the hierarchy of Root.
+ VideoCaptureTarget target_root(kFrameSinkIdRoot, crop_id);
+ EXPECT_EQ(FindCapturableFrameSink(target_root), nullptr);
+
+ // Now set the crop ID on child A.
+ auto frame_a = MakeDefaultCompositorFrame();
+ frame_a.metadata.capture_bounds = bounds;
+ sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, frame_a, kDamageRect,
+ base::TimeTicks::Now());
+
+ // Searching for crop_id targeting kFrameSinkIdRoot should find child A.
+ EXPECT_EQ(FindCapturableFrameSink(target_root), sink_a.get());
+
+ // Clear crop ID from child A and set on root itself.
+ sink_a->OnSurfaceAggregatedDamage(nullptr, surface_id_a, empty_frame,
+ kDamageRect, base::TimeTicks::Now());
+ auto frame_root = MakeDefaultCompositorFrame();
+ frame_root.metadata.capture_bounds = bounds;
+ sink_root->OnSurfaceAggregatedDamage(nullptr, surface_id_root, frame_root,
+ kDamageRect, base::TimeTicks::Now());
+
+ // Searching for crop_id targeting kFrameSinkIdRoot should find root.
+ EXPECT_EQ(FindCapturableFrameSink(target_root), sink_root.get());
+
+ // An invalid target frame sink id must return nullptr.
+ VideoCaptureTarget invalid_target;
+ EXPECT_EQ(FindCapturableFrameSink(invalid_target), nullptr);
+
+ manager_->UnregisterFrameSinkHierarchy(kFrameSinkIdRoot, kFrameSinkIdA);
+ manager_->InvalidateFrameSinkId(kFrameSinkIdRoot, {});
+ manager_->InvalidateFrameSinkId(kFrameSinkIdA, {});
+ manager_->InvalidateFrameSinkId(kFrameSinkIdB, {});
+}
} // namespace viz
Original Bug Report
Site Isolation bypass via Region Capture target hijacking in Viz
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A logic flaw in Viz’s FrameSinkManagerImpl::FindCapturableFrameSink allows a compromised renderer to hijack an active screen capture session. By forging Region Capture metadata, an attacker can force Viz to capture an unrelated tab, bypassing Site Isolation and user consent to exfiltrate cross-origin pixels.
Affected files:
components/viz/service/frame_sinks/frame_sink_manager_impl.ccservices/viz/public/cpp/compositing/region_capture_bounds_mojom_traits.hcontent/browser/renderer_host/media/media_stream_dispatcher_host.cccomponents/viz/service/frame_sinks/video_capture/frame_sink_video_capturer_impl.cccontent/browser/renderer_host/media/media_devices_dispatcher_host.cc
Estimated timestamp from git blame: 2026-01-06
Description
There is a potential Site Isolation bypass in the Viz component related to Region Capture. When a capture session is cropped using a RegionCaptureCropId, the target resolution logic in Viz completely ignores the browser-provided, authorized frame_sink_id. Instead, it searches globally across all frame sinks to find one claiming the crop ID.
A compromised renderer can exploit this by minting a legitimate token in one tab, passing it to a compromised renderer in a background tab, and forging CompositorFrame metadata to hijack the capture session. This allows the attacker to steal pixels from cross-origin iframes loaded in the background tab.
Root Cause
In components/viz/service/frame_sinks/frame_sink_manager_impl.cc, the FrameSinkManagerImpl::FindCapturableFrameSink function resolves the capture target.
CapturableFrameSink* FrameSinkManagerImpl::FindCapturableFrameSink(
const VideoCaptureTarget& target) {
if (IsRegionCapture(target.sub_target)) {
const auto crop_id = std::get<RegionCaptureCropId>(target.sub_target);
for (const auto& id_and_sink : support_map_) { // <-- GLOBAL ITERATION
const RegionCaptureBounds& bounds =
id_and_sink.second->current_capture_bounds();
if (bounds.bounds().contains(crop_id)) {
return id_and_sink.second;
}
}
return nullptr;
}
// ... (code that normally uses target.frame_sink_id is skipped)
}
Because support_map_ contains all CompositorFrameSinkSupport instances across the entire browser, any compromised renderer can claim the crop_id via forged IPC metadata, and Viz will happily attach the capture session to it, completely ignoring the target.frame_sink_id that restricts capture to the user-authorized tab.
Potential Attack Scenario
Note: These are suggested steps based on code analysis; a runnable PoC has not yet been executed.
- Setup: An attacker controls Tab A (an ordinary web page) and has compromised the renderer process for Tab C. Tab C embeds a sensitive cross-origin iframe (e.g.,
banking.com). - Mint Token: Tab A’s JavaScript calls
CropTarget.fromElement(). The browser securely generates acrop_idtoken and associates it with Tab A. - Exfiltrate Token: Tab A sends the minted
crop_idstring to the compromised renderer in Tab C via a side channel (e.g.,BroadcastChannel). - Forge Metadata: The compromised renderer in Tab C submits a
viz::CompositorFramewith a forgedviz::RegionCaptureBoundsobject in its metadata. This maps the stolencrop_idto agfx::Rectcovering thebanking.comiframe. - Initiate Capture: The user is induced to share Tab A (e.g., via
getDisplayMedia({preferCurrentTab: true})). Tab A obtains aMediaStreamTrackcapturing its own content. - Hijack Target: Tab A calls
track.cropTo(crop_id). The browser validates the request (which succeeds since the token was legitimately minted for Tab A) and forwards theVideoCaptureTargetto Viz. - Pixel Exfiltration: Viz’s
FindCapturableFrameSinkignores Tab A’sframe_sink_id, searchessupport_map_globally, and finds Tab C’s forged claim first. The capture session seamlessly redirects to Tab C, sending the cross-originbanking.compixels back to Tab A’s JavaScript.
Suggested Fix
In FrameSinkManagerImpl::FindCapturableFrameSink, do not iterate over the global support_map_. Instead:
- First resolve the legitimate
CapturableFrameSinkusingtarget.frame_sink_id. - Verify that the resolved frame sink (or its descendants in the surface tree) actually contains the
crop_idin theircurrent_capture_bounds_. - Return the specific sink within that authorized surface tree, preventing cross-tree hijacking.
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
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. Please feel free to reach out to me if you have concerns or feedback.