CVE-2026-17895
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/clipboard/system_clipboard_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/clipboard/data_object_item.ccthird_party/blink/renderer/core/clipboard/system_clipboard_test.ccthird_party/blink/renderer/core/testing/mock_clipboard_host.ccthird_party/blink/renderer/core/testing/mock_clipboard_host.h
Patch
From 128e4837a52dcb2cada6770f47e8f28351f69730 Mon Sep 17 00:00:00 2001 From: Tom Anderson <[email protected]> Date: Tue, 23 Jun 2026 08:22:16 -0700 Subject: [PATCH] Blink: Respect active clipboard buffer in DataObjectItem::GetAsFile() During selection-mode pastes (e.g. middle-click paste on Linux/ChromeOS), DataObjectItem::GetAsFile() was explicitly reading PNG data from the standard clipboard (ClipboardBuffer::kStandard) instead of the selection clipboard. This allowed a web page to access standard clipboard image data during a middle-click paste, which poses a privacy and data disclosure risk. This CL updates GetAsFile() to respect the active clipboard buffer (selection mode) when reading PNG data from the system clipboard, if the stable feature ClipboardPasteImageRespectBuffer is enabled. Unit tests updated: { SystemClipboardTest.DataObjectItemGetAsFileRespectsSelectionMode } Fixed: 524931675 Change-Id: Ia960dd3cc1414a61d8eedeed5054907c5ec4276a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7962990 Auto-Submit: Thomas Anderson <[email protected]> Commit-Queue: Thomas Anderson <[email protected]> Reviewed-by: Mason Freed <[email protected]> Cr-Commit-Position: refs/heads/main@{#1651020} --- diff --git a/third_party/blink/renderer/core/clipboard/data_object_item.cc b/third_party/blink/renderer/core/clipboard/data_object_item.cc index 94d5a2f..253290f 100644 --- a/third_party/blink/renderer/core/clipboard/data_object_item.cc +++ b/third_party/blink/renderer/core/clipboard/data_object_item.cc @@ -38,6 +38,7 @@ #include "third_party/blink/renderer/platform/heap/garbage_collected.h" #include "third_party/blink/renderer/platform/image-encoders/image_encoder.h" #include "third_party/blink/renderer/platform/network/mime/mime_type_registry.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" #include "ui/base/clipboard/clipboard_constants.h" namespace blink { @@ -175,8 +176,12 @@ } if (GetType() == ui::kMimeTypePng) { - mojo_base::BigBuffer png_data = - system_clipboard_->ReadPng(mojom::blink::ClipboardBuffer::kStandard); + mojom::blink::ClipboardBuffer buffer = + RuntimeEnabledFeatures::ClipboardPasteImageRespectBufferEnabled() && + system_clipboard_->IsSelectionMode() + ? mojom::blink::ClipboardBuffer::kSelection + : mojom::blink::ClipboardBuffer::kStandard; + mojo_base::BigBuffer png_data = system_clipboard_->ReadPng(buffer); auto data = std::make_unique<BlobData>(); data->SetContentType(ui::kMimeTypePng); 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 598bfea1..927d8b7 100644 --- a/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc +++ b/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc @@ -8,13 +8,17 @@ #include "base/test/bind.h" #include "base/test/scoped_feature_list.h" +#include "build/build_config.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/renderer/core/clipboard/data_object_item.h" +#include "third_party/blink/renderer/core/frame/local_dom_window.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/platform_event_controller.h" +#include "third_party/blink/renderer/core/frame/settings.h" #include "third_party/blink/renderer/core/testing/dummy_page_holder.h" #include "third_party/blink/renderer/core/testing/page_test_base.h" +#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" #include "third_party/blink/renderer/platform/testing/task_environment.h" #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" #include "third_party/skia/include/core/SkBitmap.h" @@ -729,4 +733,48 @@ EXPECT_TRUE(data.empty()); } +#if BUILDFLAG(IS_OZONE) +TEST_F(SystemClipboardTest, DataObjectItemGetAsFileRespectsSelectionMode) { + ScopedClipboardPasteImageRespectBufferForTest scoped_feature(true); + dom_window()->GetFrame()->GetSettings()->SetSelectionClipboardBufferAvailable( + true); + + // 1. Initial clipboard state: Write an image. + SkBitmap bitmap; + ASSERT_TRUE(bitmap.tryAllocPixelsFlags( + SkImageInfo::Make(4, 3, kN32_SkColorType, kOpaque_SkAlphaType), 0)); + clipboard_host()->WriteImage(bitmap); + clipboard_host()->CommitWrite(); + + auto sequence_number = system_clipboard().SequenceNumber(); + + // 2. Create DataObjectItem from this clipboard state. + DataObjectItem* item = DataObjectItem::CreateFromClipboard( + &system_clipboard(), ui::kMimeTypePng, sequence_number); + + // 3. Enable selection mode. + system_clipboard().SetSelectionMode(true); + + // 4. Retrieve the file from the item. + File* file = item->GetAsFile(); + EXPECT_NE(file, nullptr); + + // 5. Verify that the PNG read was made against the kSelection buffer. + EXPECT_EQ(mock_clipboard_host()->LastReadPngBuffer(), + mojom::blink::ClipboardBuffer::kSelection); + + // 6. Disable selection mode and retrieve the file again (forcing sequence + // number to match for simplicity of the test, though in real life + // SequenceNumber updates; but since we don't commit a new write, it stays + // the same). + system_clipboard().SetSelectionMode(false); + file = item->GetAsFile(); + EXPECT_NE(file, nullptr); + + // 7. Verify that the PNG read was made against the kStandard buffer. + EXPECT_EQ(mock_clipboard_host()->LastReadPngBuffer(), + mojom::blink::ClipboardBuffer::kStandard); +} +#endif + } // namespace blink diff --git a/third_party/blink/renderer/core/testing/mock_clipboard_host.cc b/third_party/blink/renderer/core/testing/mock_clipboard_host.cc index 81a4e24d..bc2b978 100644 --- a/third_party/blink/renderer/core/testing/mock_clipboard_host.cc +++ b/third_party/blink/renderer/core/testing/mock_clipboard_host.cc @@ -160,6 +160,7 @@ void MockClipboardHost::ReadPng(mojom::ClipboardBuffer clipboard_buffer, ReadPngCallback callback) { + last_read_png_buffer_ = clipboard_buffer; std::move(callback).Run(mojo_base::BigBuffer(png_)); } diff --git a/third_party/blink/renderer/core/testing/mock_clipboard_host.h b/third_party/blink/renderer/core/testing/mock_clipboard_host.h index d70e9eb..89dbd7d 100644 --- a/third_party/blink/renderer/core/testing/mock_clipboard_host.h +++ b/third_party/blink/renderer/core/testing/mock_clipboard_host.h @@ -72,6 +72,9 @@ int ReadAvailableFormatsCallCountForTesting() const { return read_available_formats_call_count_for_testing_; } + mojom::ClipboardBuffer LastReadPngBuffer() const { + return last_read_png_buffer_; + } // Test helpers used to simulate a slow OS clipboard read so callers can // verify that renderer-side Async Clipboard read paths are truly @@ -177,6 +180,9 @@ // Deferred-callback machinery for the truly-async-read regression test. bool defer_read_text_callback_for_testing_ = false; ReadTextCallback deferred_read_text_callback_for_testing_; + + mojom::ClipboardBuffer last_read_png_buffer_ = + mojom::ClipboardBuffer::kStandard; }; } // namespace blink
Regression Test / PoC
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 598bfea1..927d8b7 100644
--- a/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
+++ b/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
@@ -8,13 +8,17 @@
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
+#include "build/build_config.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/clipboard/data_object_item.h"
+#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/platform_event_controller.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/task_environment.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/skia/include/core/SkBitmap.h"
@@ -729,4 +733,48 @@
EXPECT_TRUE(data.empty());
}
+#if BUILDFLAG(IS_OZONE)
+TEST_F(SystemClipboardTest, DataObjectItemGetAsFileRespectsSelectionMode) {
+ ScopedClipboardPasteImageRespectBufferForTest scoped_feature(true);
+ dom_window()->GetFrame()->GetSettings()->SetSelectionClipboardBufferAvailable(
+ true);
+
+ // 1. Initial clipboard state: Write an image.
+ SkBitmap bitmap;
+ ASSERT_TRUE(bitmap.tryAllocPixelsFlags(
+ SkImageInfo::Make(4, 3, kN32_SkColorType, kOpaque_SkAlphaType), 0));
+ clipboard_host()->WriteImage(bitmap);
+ clipboard_host()->CommitWrite();
+
+ auto sequence_number = system_clipboard().SequenceNumber();
+
+ // 2. Create DataObjectItem from this clipboard state.
+ DataObjectItem* item = DataObjectItem::CreateFromClipboard(
+ &system_clipboard(), ui::kMimeTypePng, sequence_number);
+
+ // 3. Enable selection mode.
+ system_clipboard().SetSelectionMode(true);
+
+ // 4. Retrieve the file from the item.
+ File* file = item->GetAsFile();
+ EXPECT_NE(file, nullptr);
+
+ // 5. Verify that the PNG read was made against the kSelection buffer.
+ EXPECT_EQ(mock_clipboard_host()->LastReadPngBuffer(),
+ mojom::blink::ClipboardBuffer::kSelection);
+
+ // 6. Disable selection mode and retrieve the file again (forcing sequence
+ // number to match for simplicity of the test, though in real life
+ // SequenceNumber updates; but since we don't commit a new write, it stays
+ // the same).
+ system_clipboard().SetSelectionMode(false);
+ file = item->GetAsFile();
+ EXPECT_NE(file, nullptr);
+
+ // 7. Verify that the PNG read was made against the kStandard buffer.
+ EXPECT_EQ(mock_clipboard_host()->LastReadPngBuffer(),
+ mojom::blink::ClipboardBuffer::kStandard);
+}
+#endif
+
} // namespace blink
diff --git a/third_party/blink/renderer/core/testing/mock_clipboard_host.cc b/third_party/blink/renderer/core/testing/mock_clipboard_host.cc
index 81a4e24d..bc2b978 100644
--- a/third_party/blink/renderer/core/testing/mock_clipboard_host.cc
+++ b/third_party/blink/renderer/core/testing/mock_clipboard_host.cc
@@ -160,6 +160,7 @@
void MockClipboardHost::ReadPng(mojom::ClipboardBuffer clipboard_buffer,
ReadPngCallback callback) {
+ last_read_png_buffer_ = clipboard_buffer;
std::move(callback).Run(mojo_base::BigBuffer(png_));
}
diff --git a/third_party/blink/renderer/core/testing/mock_clipboard_host.h b/third_party/blink/renderer/core/testing/mock_clipboard_host.h
index d70e9eb..89dbd7d 100644
--- a/third_party/blink/renderer/core/testing/mock_clipboard_host.h
+++ b/third_party/blink/renderer/core/testing/mock_clipboard_host.h
@@ -72,6 +72,9 @@
int ReadAvailableFormatsCallCountForTesting() const {
return read_available_formats_call_count_for_testing_;
}
+ mojom::ClipboardBuffer LastReadPngBuffer() const {
+ return last_read_png_buffer_;
+ }
// Test helpers used to simulate a slow OS clipboard read so callers can
// verify that renderer-side Async Clipboard read paths are truly
@@ -177,6 +180,9 @@
// Deferred-callback machinery for the truly-async-read regression test.
bool defer_read_text_callback_for_testing_ = false;
ReadTextCallback deferred_read_text_callback_for_testing_;
+
+ mojom::ClipboardBuffer last_read_png_buffer_ =
+ mojom::ClipboardBuffer::kStandard;
};
} // namespace blink
Original Bug Report
Middle-click paste discloses global clipboard image to web page on Linux/ChromeOS
Steps to reproduce the problem
PRECONDITIONS (all default config, no flags):
- A web page with a focused contenteditable/textarea and a “paste” event listener.
- The global CLIPBOARD (kStandard, Ctrl-C buffer) holds a victim PNG the user previously copied (e.g. a screenshot, or an image copied from another origin/app).
- The PRIMARY selection (kSelection, middle-click buffer) advertises an image/png target (e.g. the user selected an image in an app that exports image/png to PRIMARY).
MANUAL STEPS:
- On a Linux desktop, copy an image to the CLIPBOARD with Ctrl-C (the “secret”).
- In another app, select a DIFFERENT image so the PRIMARY selection advertises image/png.
- In Chrome, open a page whose editable region has this paste handler: editor.addEventListener(‘paste’, async (e) => { for (const it of e.clipboardData.items) { if (it.kind === ‘file’ && it.type === ‘image/png’) { const f = it.getAsFile(); const buf = await f.arrayBuffer(); // bytes of the CLIPBOARD image, NOT PRIMARY exfiltrate(buf); } } });
- MIDDLE-CLICK into the editable region to perform a PRIMARY-selection paste.
- getAsFile() returns a File whose bytes are the CLIPBOARD (Ctrl-C) image — the image the user never authorized this page to read. The user only performed a PRIMARY paste.
DETERMINISTIC REPRODUCTION (containerized, no physical display): Reproduced on Chromium 149.0.7827.114, Debian 12 arm64 container, Xvfb virtual X server.
- Seed buffers with distinct solid-color PNGs via xclip: xclip -selection clipboard -t image/png -i victim_red.png # kStandard xclip -selection primary -t image/png -i decoy_green.png # kSelection
- Launch Chromium with –ozone-platform=x11 (NOT –headless; headless uses an in-memory clipboard stub and will not exercise the real PRIMARY path).
- Load a page with the paste handler above, focus the editor, and issue a real X11 middle-click (xdotool click 2).
- Compare the SHA-256 of the returned File against the two seeded images.
OBSERVED RESULT (2 of 2 runs, deterministic): getAsFile() -> image.png, image/png, 80 bytes SHA-256 = 42cb5164… == victim_red (the CLIPBOARD image) (decoy_green PRIMARY image SHA-256 = b4966ee9… was NOT returned) => the page received the CLIPBOARD image during a PRIMARY-selection paste.
NOTE: kPasteGlobalSelection is SupportedFromMenuOrKeyBinding only; it is NOT reachable via execCommand(‘paste’). A genuine middle-click user gesture is required.
Problem Description
On Linux/ChromeOS, Chrome exposes two OS clipboard buffers: the global CLIPBOARD (kStandard, the Ctrl-C/Ctrl-V buffer) and the PRIMARY selection (kSelection, the middle-click buffer). These are distinct trust surfaces: a middle-click PRIMARY paste must deliver only what is on PRIMARY and must not surrender the global CLIPBOARD.
SystemClipboard tracks the active buffer in buffer_. The editor-paste read helpers (ReadPlainText/ReadHTML/ReadRTF) all follow buffer_. During a middle-click paste, ClipboardCommands::ExecutePasteGlobalSelection calls SetSelectionMode(true) (sets buffer_ = kSelection), runs Paste(), then restores the buffer. Critically, Paste() dispatches the JS “paste” event SYNCHRONOUSLY, while buffer_ is still kSelection.
The defect: DataObjectItem::GetAsFile() — reached from page JS as clipboardData.items[i].getAsFile() inside the paste handler — reads the clipboard PNG with a HARDCODED mojom::blink::ClipboardBuffer::kStandard, ignoring buffer_. So during a PRIMARY-selection middle-click paste, getAsFile() returns a File containing the CLIPBOARD (kStandard) image bytes — an image the user copied from another origin or another application — even though the user performed only a PRIMARY paste and never authorized disclosure of the CLIPBOARD.
The preceding sequence-number guard does NOT prevent this: SequenceNumber() also follows buffer_, so it compares the kSelection sequence (which matches) and then reads kStandard regardless. The guard never consults which buffer the bytes come from.
This is the exact defect class fixed for the MARKUP paste path in the commit titled “Fix middle-click paste leaking kStandard image cross-buffer” (bug 497251066), which added a buffer-following no-arg ReadImageAsImageMarkup() overload. That fix did NOT touch the JS-facing GetAsFile() path, which embeds the identical hardcoded-kStandard defect and remains unfixed at tip. The sibling method in the same file, GetAsString(), correctly follows buffer_ via the no-arg ReadPlainText/ReadHTML/ReadRTF overloads — only the PNG/file path hardcodes a buffer. That asymmetry is the bug.
IMPACT: Cross-clipboard-buffer information disclosure to page JavaScript, in default configuration, from an ordinary “paste” event handler with NO clipboard-read permission prompt — sidestepping the async Clipboard API (navigator.clipboard.read) permission gate for the kStandard image. The page obtains raw PNG bytes the user copied elsewhere (screenshots, photos, images from other origins/apps). Gated by a real user middle-click and by PRIMARY advertising image/png while a different image sits on CLIPBOARD.
SUGGESTED FIX: Route the PNG read through the active buffer. Add a no-arg SystemClipboard::ReadPng() returning ReadPng(buffer_) (mirroring ReadPlainText() and the seed CL’s ReadImageAsImageMarkup()), and call it from GetAsFile(); keep the explicit ReadPng(buffer) overload for legitimate async Clipboard API callers. Gate behind the existing ClipboardPasteImageRespectBuffer feature so both the markup and getAsFile() paths follow buffer_.
Summary
Middle-click paste discloses global clipboard image to web page on Linux/ChromeOS
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A \