Low watch CVSS 6.5 chrome Info Disclosure 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in DataTransfer
DescriptionInformation leak in DataTransfer
ComponentDataTransfer
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker525311654
Fix commit29cb4e0d1ae8 (chromium/src) +97/-49
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-18 (per vendor advisory)
Fix landed2026-07-16 (public git)
Disclosed2026-08-25
Patch gap40 days — fix public in source before the advisory

Background

`SystemClipboard::Snapshot`
a per-scope cache in the renderer (Blink) that memoizes clipboard reads so repeated reads within one scope do not re-query the browser’s clipboard host.
Clipboard buffers
mojom::blink::ClipboardBuffer::kStandard (the normal clipboard) and kSelection (the Linux/X11 primary-selection clipboard), which hold independent contents.
`ScopedSystemClipboardSnapshot`
a scope object that enables the Snapshot cache while a paste/read operation runs.
`BindToBuffer` (removed)
the old routine that locked a snapshot to a single buffer and DCHECK_EQ’d any subsequent buffer, implicitly assuming one buffer per snapshot.

Root Cause Analysis

The Snapshot stored one flat set of cached fields (plain_text_, html_, png_, files_, custom_data_, etc.) plus a single buffer_, and every Has* accessor tested only buffer_.has_value() && <field>.has_value() — buffer-blind. On Linux both kStandard and kSelection can be queried within a single snapshot scope, so once the snapshot cached one buffer’s data, a read for the other buffer saw the cached field as present and returned it: cross-buffer cache poisoning in which a kSelection read incorrectly returned cached kStandard data. The invariant that cached data belongs to the buffer being queried was not enforced (the old BindToBuffer merely asserted a single buffer, which the multi-buffer Linux path violated).

The fix partitions the cache into a per-buffer BufferData struct (standard_data_, selection_data_) selected by GetBufferData/GetOrCreateBufferData, so kStandard and kSelection lookups stay strictly isolated and a miss in one buffer no longer returns the other’s data — as the added CrossBufferSnapshotIsolation test verifies via ReadTextCallCountForTesting.

Key insight
The core mistake was a buffer-blind snapshot cache keyed on a single bound buffer instead of per buffer, letting one buffer’s cached clipboard contents satisfy reads of another; the fix stores and looks up cached data per ClipboardBuffer.

Attack Path

  1. Enter a snapshot scope a paste/read flow opens a ScopedSystemClipboardSnapshot, enabling the cache.
  2. Prime the cache from one buffer a read of kStandard caches the standard clipboard’s contents in the flat snapshot fields and sets buffer_.
  3. Read the other buffer a subsequent read of kSelection finds buffer_.has_value() && <field>.has_value() true and returns the cached kStandard data instead of the selection buffer’s real contents.
  4. Observe leaked data the page receives standard-clipboard contents through a selection-buffer read (or vice versa), disclosing data across buffers.

Impact Assessment

The advisory classifies this as an information leak in DataTransfer. On Linux, web content performing clipboard reads within a snapshot scope could receive one clipboard buffer’s contents (e.g. the standard clipboard) when reading a different buffer (the primary selection), leaking clipboard data in the renderer process. The preconditions are that the selection buffer is available (Linux/Ozone) and that both buffers are queried within a single snapshot scope.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/clipboard/system_clipboard.cc
modified
TEST_F
third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/clipboard/system_clipboard.h
  • third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
  • third_party/blink/renderer/core/clipboard/system_clipboard.cc

Audit Directions

  • Key-blind caches
    audit memoization caches whose hit test omits a dimension that distinguishes the data (here ClipboardBuffer); a cache must key on every dimension that changes its contents.
  • `DCHECK`-enforced single-value assumptions
    look for code that assumes “one X per scope” via a DCHECK_EQ (like the old BindToBuffer) where the real code path can legitimately use multiple values.
  • Cross-context clipboard/data-transfer reads
    review other clipboard/DataTransfer accessors for places where standard vs. selection (or origin-scoped) data could be conflated within a single scope.
From 29cb4e0d1ae84a8e76ad6d5699281bcdeaa2c3ab Mon Sep 17 00:00:00 2001
From: Tom Anderson <[email protected]>
Date: Thu, 16 Jul 2026 09:57:14 -0700
Subject: [PATCH] Partition SystemClipboard::Snapshot cache per-buffer.

Currently, SystemClipboard::Snapshot has buffer-blind HasPng,
HasPlainText, etc. accessors. On Linux, standard and selection
buffers can be queried within a single snapshot scope, leading to
cross-buffer cache poisoning (where selection buffer reads incorrectly
return cached standard clipboard data).

This CL partitions the snapshot cache into a per-buffer data structure,
ensuring that kStandard and kSelection lookups are kept strictly
isolated.

Bug: 525311654
Change-Id: Ie50e0b81597e9c68f91c9bafdbc046e32c2aacc1
Fixed: 525311654
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8033382
Reviewed-by: Kentaro Hara <[email protected]>
Commit-Queue: Kentaro Hara <[email protected]>
Auto-Submit: Thomas Anderson <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1663224}
---
diff --git a/third_party/blink/renderer/core/clipboard/system_clipboard.cc b/third_party/blink/renderer/core/clipboard/system_clipboard.cc
index a945987e..6872571 100644
--- a/third_party/blink/renderer/core/clipboard/system_clipboard.cc
+++ b/third_party/blink/renderer/core/clipboard/system_clipboard.cc
@@ -521,49 +521,48 @@
 
 bool SystemClipboard::Snapshot::HasPlainText(
     mojom::blink::ClipboardBuffer buffer) const {
-  return buffer_.has_value() && plain_text_.has_value();
+  return GetBufferData(buffer)->plain_text_.has_value();
 }
 
 const String& SystemClipboard::Snapshot::PlainText(
     mojom::blink::ClipboardBuffer buffer) const {
   DCHECK(HasPlainText(buffer));
-  return plain_text_.value();
+  return GetBufferData(buffer)->plain_text_.value();
 }
 
 void SystemClipboard::Snapshot::SetPlainText(
     mojom::blink::ClipboardBuffer buffer,
     const String& text) {
-  BindToBuffer(buffer);
-  plain_text_ = text;
+  GetOrCreateBufferData(buffer)->plain_text_ = text;
 }
 
 bool SystemClipboard::Snapshot::HasHtml(
     mojom::blink::ClipboardBuffer buffer) const {
-  return buffer_.has_value() && html_.has_value();
+  return GetBufferData(buffer)->html_.has_value();
 }
 
 const KURL& SystemClipboard::Snapshot::Url(
     mojom::blink::ClipboardBuffer buffer) const {
   DCHECK(HasHtml(buffer));
-  return url_;
+  return GetBufferData(buffer)->url_;
 }
 
 unsigned SystemClipboard::Snapshot::FragmentStart(
     mojom::blink::ClipboardBuffer buffer) const {
   DCHECK(HasHtml(buffer));
-  return fragment_start_;
+  return GetBufferData(buffer)->fragment_start_;
 }
 
 unsigned SystemClipboard::Snapshot::FragmentEnd(
     mojom::blink::ClipboardBuffer buffer) const {
   DCHECK(HasHtml(buffer));
-  return fragment_end_;
+  return GetBufferData(buffer)->fragment_end_;
 }
 
 const String& SystemClipboard::Snapshot::Html(
     mojom::blink::ClipboardBuffer buffer) const {
   DCHECK(HasHtml(buffer));
-  return html_.value();
+  return GetBufferData(buffer)->html_.value();
 }
 
 void SystemClipboard::Snapshot::SetHtml(mojom::blink::ClipboardBuffer buffer,
@@ -571,89 +570,87 @@
                                         const KURL& url,
                                         unsigned fragment_start,
                                         unsigned fragment_end) {
-  BindToBuffer(buffer);
-  html_ = html;
-  url_ = url;
-  fragment_start_ = fragment_start;
-  fragment_end_ = fragment_end;
+  BufferData* data = GetOrCreateBufferData(buffer);
+  data->html_ = html;
+  data->url_ = url;
+  data->fragment_start_ = fragment_start;
+  data->fragment_end_ = fragment_end;
 }
 
 bool SystemClipboard::Snapshot::HasRtf(
     mojom::blink::ClipboardBuffer buffer) const {
-  return buffer_.has_value() && rtf_.has_value();
+  return GetBufferData(buffer)->rtf_.has_value();
 }
 
 const String& SystemClipboard::Snapshot::Rtf(
     mojom::blink::ClipboardBuffer buffer) const {
   DCHECK(HasRtf(buffer));
-  return rtf_.value();
+  return GetBufferData(buffer)->rtf_.value();
 }
 
 void SystemClipboard::Snapshot::SetRtf(mojom::blink::ClipboardBuffer buffer,
                                        const String& rtf) {
-  BindToBuffer(buffer);
-  rtf_ = rtf;
+  GetOrCreateBufferData(buffer)->rtf_ = rtf;
 }
 
 bool SystemClipboard::Snapshot::HasPng(
     mojom::blink::ClipboardBuffer buffer) const {
-  return buffer_.has_value() && png_.has_value();
+  return GetBufferData(buffer)->png_.has_value();
 }
 
 mojo_base::BigBuffer SystemClipboard::Snapshot::Png(
     mojom::blink::ClipboardBuffer buffer) const {
   DCHECK(HasPng(buffer));
   // Make an owning copy of the png to return to user.
-  base::span<const uint8_t> span = base::span(png_.value());
+  base::span<const uint8_t> span =
+      base::span(GetBufferData(buffer)->png_.value());
   return mojo_base::BigBuffer(span);
 }
 
 // TODO(https://crbug.com/1412180): Reduce data copies.
 void SystemClipboard::Snapshot::SetPng(mojom::blink::ClipboardBuffer buffer,
                                        const mojo_base::BigBuffer& png) {
-  BindToBuffer(buffer);
+  BufferData* data = GetOrCreateBufferData(buffer);
   // Make an owning copy of the png to save locally.
   base::span<const uint8_t> span = base::span(png);
-  png_ = mojo_base::BigBuffer(span);
+  data->png_ = mojo_base::BigBuffer(span);
 }
 
 bool SystemClipboard::Snapshot::HasFiles(
     mojom::blink::ClipboardBuffer buffer) const {
-  return buffer_.has_value() && files_.has_value();
+  return GetBufferData(buffer)->files_.has_value();
 }
 
 mojom::blink::ClipboardFilesPtr SystemClipboard::Snapshot::Files(
     mojom::blink::ClipboardBuffer buffer) const {
   DCHECK(HasFiles(buffer));
-  return CloneFiles(files_.value());
+  return CloneFiles(GetBufferData(buffer)->files_.value());
 }
 
 void SystemClipboard::Snapshot::SetFiles(
     mojom::blink::ClipboardBuffer buffer,
     mojom::blink::ClipboardFilesPtr& files) {
-  BindToBuffer(buffer);
-  files_ = CloneFiles(files);
+  GetOrCreateBufferData(buffer)->files_ = CloneFiles(files);
 }
 
 bool SystemClipboard::Snapshot::HasCustomData(
     mojom::blink::ClipboardBuffer buffer,
     const String& type) const {
-  return buffer_.has_value() && custom_data_.Contains(type);
+  return GetBufferData(buffer)->custom_data_.Contains(type);
 }
 
 String SystemClipboard::Snapshot::CustomData(
     mojom::blink::ClipboardBuffer buffer,
     const String& type) const {
   DCHECK(HasCustomData(buffer, type));
-  return custom_data_.at(type);
+  return GetBufferData(buffer)->custom_data_.at(type);
 }
 
 void SystemClipboard::Snapshot::SetCustomData(
     mojom::blink::ClipboardBuffer buffer,
     const String& type,
     const String& data) {
-  BindToBuffer(buffer);
-  custom_data_.Set(type, data);
+  GetOrCreateBufferData(buffer)->custom_data_.Set(type, data);
 }
 
 void SystemClipboard::OnClipboardDataChanged(const Vector<String>& types,
@@ -703,13 +700,22 @@
                                            files->file_system_id);
 }
 
-void SystemClipboard::Snapshot::BindToBuffer(
-    mojom::blink::ClipboardBuffer buffer) {
-  if (!buffer_) {
-    buffer_ = buffer;
-  } else {
-    DCHECK_EQ(*buffer_, buffer);
+const SystemClipboard::Snapshot::BufferData*
+SystemClipboard::Snapshot::GetBufferData(
+    mojom::blink::ClipboardBuffer buffer) const {
+  if (buffer == mojom::blink::ClipboardBuffer::kSelection) {
+    return &selection_data_;
   }
+  return &standard_data_;
+}
+
+SystemClipboard::Snapshot::BufferData*
+SystemClipboard::Snapshot::GetOrCreateBufferData(
+    mojom::blink::ClipboardBuffer buffer) {
+  if (buffer == mojom::blink::ClipboardBuffer::kSelection) {
+    return &selection_data_;
+  }
+  return &standard_data_;
 }
 
 ScopedSystemClipboardSnapshot::ScopedSystemClipboardSnapshot(
diff --git a/third_party/blink/renderer/core/clipboard/system_clipboard.h b/third_party/blink/renderer/core/clipboard/system_clipboard.h
index fe49f82..37f2103 100644
--- a/third_party/blink/renderer/core/clipboard/system_clipboard.h
+++ b/third_party/blink/renderer/core/clipboard/system_clipboard.h
@@ -209,26 +209,28 @@
         mojom::blink::ClipboardFilesPtr& files);
 
    private:
-    // Called in the set methods to bind this snapshot to the specified buffer.
-    // All calls to set data for all types need to specify the same buffer.
-    void BindToBuffer(mojom::blink::ClipboardBuffer buffer);
+    struct BufferData {
+      std::optional<String> plain_text_;
 
-    std::optional<mojom::blink::ClipboardBuffer> buffer_;
+      std::optional<String> html_;
+      KURL url_;
+      unsigned fragment_start_ = 0;
+      unsigned fragment_end_ = 0;
 
-    std::optional<String> plain_text_;
+      std::optional<String> rtf_;
 
-    std::optional<String> html_;
-    KURL url_;
-    unsigned fragment_start_ = 0;
-    unsigned fragment_end_ = 0;
+      std::optional<mojo_base::BigBuffer> png_;
 
-    std::optional<String> rtf_;
+      mutable std::optional<mojom::blink::ClipboardFilesPtr> files_;
 
-    std::optional<mojo_base::BigBuffer> png_;
+      HashMap<String, String> custom_data_;
+    };
 
-    mutable std::optional<mojom::blink::ClipboardFilesPtr> files_;
+    const BufferData* GetBufferData(mojom::blink::ClipboardBuffer buffer) const;
+    BufferData* GetOrCreateBufferData(mojom::blink::ClipboardBuffer buffer);
 
-    HashMap<String, String> custom_data_;
+    BufferData standard_data_;
+    BufferData selection_data_;
   };
 
   bool IsValidBufferType(mojom::blink::ClipboardBuffer);
diff --git a/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc b/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
index 927d8b7..717ab835 100644
--- a/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
+++ b/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
@@ -95,6 +95,9 @@
     EXPECT_TRUE(
         system_clipboard().IsValidBufferType(system_clipboard().buffer_));
   }
+  void SetSelectionBufferAvailable(bool available) {
+    system_clipboard().is_selection_buffer_available_ = available;
+  }
 
   void RunUntilIdle() { test::RunPendingTasks(); }
 
@@ -733,6 +736,43 @@
   EXPECT_TRUE(data.empty());
 }
 
+TEST_F(SystemClipboardTest, CrossBufferSnapshotIsolation) {
+  SetSelectionBufferAvailable(true);
+
+  // Populate standard clipboard text.
+  clipboard_host()->WriteText("standard_text");
+  clipboard_host()->CommitWrite();
+
+  // Enter snapshot scope.
+  ScopedSystemClipboardSnapshot snapshot(system_clipboard());
+
+  // 1. Read plain text from kStandard.
+  // This should call MockClipboardHost::ReadText.
+  int initial_calls = mock_clipboard_host()->ReadTextCallCountForTesting();
+  EXPECT_EQ(system_clipboard().ReadPlainText(
+                mojom::blink::ClipboardBuffer::kStandard),
+            "standard_text");
+  EXPECT_EQ(mock_clipboard_host()->ReadTextCallCountForTesting(),
+            initial_calls + 1);
+
+  // 2. Read plain text from kStandard again.
+  // This must hit the cache and not call MockClipboardHost::ReadText.
+  EXPECT_EQ(system_clipboard().ReadPlainText(
+                mojom::blink::ClipboardBuffer::kStandard),
+            "standard_text");
+  EXPECT_EQ(mock_clipboard_host()->ReadTextCallCountForTesting(),
+            initial_calls + 1);
+
+  // 3. Read plain text from kSelection.
+  // Because they are in different buffers, the cache must not hit across
+  // buffers. This should call MockClipboardHost::ReadText.
+  EXPECT_EQ(system_clipboard().ReadPlainText(
+                mojom::blink::ClipboardBuffer::kSelection),
+            "standard_text");
+  EXPECT_EQ(mock_clipboard_host()->ReadTextCallCountForTesting(),
+            initial_calls + 2);
+}
+
 #if BUILDFLAG(IS_OZONE)
 TEST_F(SystemClipboardTest, DataObjectItemGetAsFileRespectsSelectionMode) {
   ScopedClipboardPasteImageRespectBufferForTest scoped_feature(true);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc b/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
index 927d8b7..717ab835 100644
--- a/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
+++ b/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
@@ -95,6 +95,9 @@
     EXPECT_TRUE(
         system_clipboard().IsValidBufferType(system_clipboard().buffer_));
   }
+  void SetSelectionBufferAvailable(bool available) {
+    system_clipboard().is_selection_buffer_available_ = available;
+  }
 
   void RunUntilIdle() { test::RunPendingTasks(); }
 
@@ -733,6 +736,43 @@
   EXPECT_TRUE(data.empty());
 }
 
+TEST_F(SystemClipboardTest, CrossBufferSnapshotIsolation) {
+  SetSelectionBufferAvailable(true);
+
+  // Populate standard clipboard text.
+  clipboard_host()->WriteText("standard_text");
+  clipboard_host()->CommitWrite();
+
+  // Enter snapshot scope.
+  ScopedSystemClipboardSnapshot snapshot(system_clipboard());
+
+  // 1. Read plain text from kStandard.
+  // This should call MockClipboardHost::ReadText.
+  int initial_calls = mock_clipboard_host()->ReadTextCallCountForTesting();
+  EXPECT_EQ(system_clipboard().ReadPlainText(
+                mojom::blink::ClipboardBuffer::kStandard),
+            "standard_text");
+  EXPECT_EQ(mock_clipboard_host()->ReadTextCallCountForTesting(),
+            initial_calls + 1);
+
+  // 2. Read plain text from kStandard again.
+  // This must hit the cache and not call MockClipboardHost::ReadText.
+  EXPECT_EQ(system_clipboard().ReadPlainText(
+                mojom::blink::ClipboardBuffer::kStandard),
+            "standard_text");
+  EXPECT_EQ(mock_clipboard_host()->ReadTextCallCountForTesting(),
+            initial_calls + 1);
+
+  // 3. Read plain text from kSelection.
+  // Because they are in different buffers, the cache must not hit across
+  // buffers. This should call MockClipboardHost::ReadText.
+  EXPECT_EQ(system_clipboard().ReadPlainText(
+                mojom::blink::ClipboardBuffer::kSelection),
+            "standard_text");
+  EXPECT_EQ(mock_clipboard_host()->ReadTextCallCountForTesting(),
+            initial_calls + 2);
+}
+
 #if BUILDFLAG(IS_OZONE)
 TEST_F(SystemClipboardTest, DataObjectItemGetAsFileRespectsSelectionMode) {
   ScopedClipboardPasteImageRespectBufferForTest scoped_feature(true);
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.