CVE-2026-11161
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.cc
Patch
From e59c8afc12fa995b6d25e21357ed313cfd56f744 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Wed, 22 Apr 2026 11:31:10 -0700
Subject: [PATCH] Add sequence number validation to DataObjectItem::GetAsFile
This CL adds a sequence number validation check to
DataObjectItem::GetAsFile() for clipboard-sourced items, mirroring the
existing check in GetAsString(). This prevents a potential Time-of-Check
to Time-of-Use (TOCTOU) vulnerability where a malicious site could
bypass the clipboard snapshot cache and retrieve updated OS clipboard
content that the user did not intend to paste.
Regression tests are added to SystemClipboardTest to verify the fix for
GetAsFile() and to ensure coverage for the existing GetAsString() check.
Fixed: 501920294
Change-Id: I7f2196ce3d21ef03c430ebc8c95ffb6bbc594056
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7759841
Reviewed-by: Dan Clark <daniec@microsoft.com>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1618988}
---
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 67c951a..94d5a2f 100644
--- a/third_party/blink/renderer/core/clipboard/data_object_item.cc
+++ b/third_party/blink/renderer/core/clipboard/data_object_item.cc
@@ -168,6 +168,12 @@
}
DCHECK_EQ(source_, DataSource::kClipboardSource);
+ // Verify that the clipboard has not changed since the item was created.
+ // See crbug.com/501920294.
+ if (system_clipboard_->SequenceNumber() != sequence_number_) {
+ return nullptr;
+ }
+
if (GetType() == ui::kMimeTypePng) {
mojo_base::BigBuffer png_data =
system_clipboard_->ReadPng(mojom::blink::ClipboardBuffer::kStandard);
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 bed37e8f..e0b14dd 100644
--- a/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
+++ b/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
@@ -10,6 +10,7 @@
#include "base/test/scoped_feature_list.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_frame.h"
#include "third_party/blink/renderer/core/frame/platform_event_controller.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
@@ -18,6 +19,7 @@
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkImageInfo.h"
+#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/geometry/size.h"
@@ -670,4 +672,61 @@
}
#endif // BUILDFLAG(IS_MAC)
+// Regression test for crbug.com/501920294
+TEST_F(SystemClipboardTest, DataObjectItemGetAsFileSequenceNumberValidation) {
+ // 1. Initial clipboard state: "image 1"
+ SkBitmap bitmap1;
+ ASSERT_TRUE(bitmap1.tryAllocPixelsFlags(
+ SkImageInfo::Make(4, 3, kN32_SkColorType, kOpaque_SkAlphaType), 0));
+ clipboard_host()->WriteImage(bitmap1);
+ clipboard_host()->CommitWrite();
+
+ auto sequence_number1 = system_clipboard().SequenceNumber();
+
+ // 2. Create DataObjectItem from this clipboard state.
+ DataObjectItem* item = DataObjectItem::CreateFromClipboard(
+ &system_clipboard(), ui::kMimeTypePng, sequence_number1);
+
+ // 3. Update clipboard state: "image 2".
+ // This increments the sequence number on the clipboard host.
+ SkBitmap bitmap2;
+ ASSERT_TRUE(bitmap2.tryAllocPixelsFlags(
+ SkImageInfo::Make(40, 30, kN32_SkColorType, kOpaque_SkAlphaType), 0));
+ clipboard_host()->WriteImage(bitmap2);
+ clipboard_host()->CommitWrite();
+
+ auto sequence_number2 = system_clipboard().SequenceNumber();
+ ASSERT_NE(sequence_number1, sequence_number2);
+
+ // 4. Try to read from the OLD DataObjectItem.
+ // This must return nullptr because the sequence number has changed,
+ // preventing a TOCTOU vulnerability.
+ File* file = item->GetAsFile();
+ EXPECT_EQ(file, nullptr);
+}
+
+TEST_F(SystemClipboardTest, DataObjectItemGetAsStringSequenceNumberValidation) {
+ // 1. Initial clipboard state: "text 1"
+ clipboard_host()->WriteText("text 1");
+ clipboard_host()->CommitWrite();
+
+ auto sequence_number1 = system_clipboard().SequenceNumber();
+
+ // 2. Create DataObjectItem from this clipboard state.
+ DataObjectItem* item = DataObjectItem::CreateFromClipboard(
+ &system_clipboard(), ui::kMimeTypePlainText, sequence_number1);
+
+ // 3. Update clipboard state: "text 2".
+ clipboard_host()->WriteText("text 2");
+ clipboard_host()->CommitWrite();
+
+ auto sequence_number2 = system_clipboard().SequenceNumber();
+ ASSERT_NE(sequence_number1, sequence_number2);
+
+ // 4. Try to read from the OLD DataObjectItem.
+ // It should return an empty string because the sequence number has changed.
+ String data = item->GetAsString();
+ EXPECT_TRUE(data.empty());
+}
+
} // 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 bed37e8f..e0b14dd 100644
--- a/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
+++ b/third_party/blink/renderer/core/clipboard/system_clipboard_test.cc
@@ -10,6 +10,7 @@
#include "base/test/scoped_feature_list.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_frame.h"
#include "third_party/blink/renderer/core/frame/platform_event_controller.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
@@ -18,6 +19,7 @@
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkImageInfo.h"
+#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/geometry/size.h"
@@ -670,4 +672,61 @@
}
#endif // BUILDFLAG(IS_MAC)
+// Regression test for crbug.com/501920294
+TEST_F(SystemClipboardTest, DataObjectItemGetAsFileSequenceNumberValidation) {
+ // 1. Initial clipboard state: "image 1"
+ SkBitmap bitmap1;
+ ASSERT_TRUE(bitmap1.tryAllocPixelsFlags(
+ SkImageInfo::Make(4, 3, kN32_SkColorType, kOpaque_SkAlphaType), 0));
+ clipboard_host()->WriteImage(bitmap1);
+ clipboard_host()->CommitWrite();
+
+ auto sequence_number1 = system_clipboard().SequenceNumber();
+
+ // 2. Create DataObjectItem from this clipboard state.
+ DataObjectItem* item = DataObjectItem::CreateFromClipboard(
+ &system_clipboard(), ui::kMimeTypePng, sequence_number1);
+
+ // 3. Update clipboard state: "image 2".
+ // This increments the sequence number on the clipboard host.
+ SkBitmap bitmap2;
+ ASSERT_TRUE(bitmap2.tryAllocPixelsFlags(
+ SkImageInfo::Make(40, 30, kN32_SkColorType, kOpaque_SkAlphaType), 0));
+ clipboard_host()->WriteImage(bitmap2);
+ clipboard_host()->CommitWrite();
+
+ auto sequence_number2 = system_clipboard().SequenceNumber();
+ ASSERT_NE(sequence_number1, sequence_number2);
+
+ // 4. Try to read from the OLD DataObjectItem.
+ // This must return nullptr because the sequence number has changed,
+ // preventing a TOCTOU vulnerability.
+ File* file = item->GetAsFile();
+ EXPECT_EQ(file, nullptr);
+}
+
+TEST_F(SystemClipboardTest, DataObjectItemGetAsStringSequenceNumberValidation) {
+ // 1. Initial clipboard state: "text 1"
+ clipboard_host()->WriteText("text 1");
+ clipboard_host()->CommitWrite();
+
+ auto sequence_number1 = system_clipboard().SequenceNumber();
+
+ // 2. Create DataObjectItem from this clipboard state.
+ DataObjectItem* item = DataObjectItem::CreateFromClipboard(
+ &system_clipboard(), ui::kMimeTypePlainText, sequence_number1);
+
+ // 3. Update clipboard state: "text 2".
+ clipboard_host()->WriteText("text 2");
+ clipboard_host()->CommitWrite();
+
+ auto sequence_number2 = system_clipboard().SequenceNumber();
+ ASSERT_NE(sequence_number1, sequence_number2);
+
+ // 4. Try to read from the OLD DataObjectItem.
+ // It should return an empty string because the sequence number has changed.
+ String data = item->GetAsString();
+ EXPECT_TRUE(data.empty());
+}
+
} // namespace blink
Original Bug Report
Info Leak: Missing clipboard sequence check in DataObjectItem::GetAsFile
Project Fortify, 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 without the Chrome Security team.
Overview: A potential Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists in blink’s clipboard handling. By bypassing the clipboard snapshot cache using a nested copy command, a malicious site could trick DataObjectItem::GetAsFile() into lazily reading and returning updated OS clipboard content (like a user’s recent screenshot) without validation.
Affected files:
third_party/blink/renderer/core/clipboard/data_object_item.ccthird_party/blink/renderer/core/clipboard/system_clipboard.cccontent/browser/renderer_host/clipboard_host_impl.cc
Estimated timestamp from git blame: 2025-04-03
Description
There is a potential Time-of-Check to Time-of-Use (TOCTOU) vulnerability in how Chromium handles lazy reads of PNG data from the clipboard within the renderer process.
When a paste event is dispatched, a DataTransfer object is populated with DataObjectItems representing the clipboard content. Each item captures the clipboard’s sequence_number_ at the time of creation. To prevent multiple synchronous IPCs and ensure consistency, ClipboardCommands::Paste uses a ScopedSystemClipboardSnapshot to cache clipboard reads during the event.
However, a malicious site can bypass this caching mechanism. If the site executes document.execCommand('copy') from within the paste event handler (which is allowed because the initial paste provides transient user activation), it triggers a write to the clipboard. The functions SystemClipboard::WriteDataObject() and CommitWrite() unconditionally call SystemClipboard::ResetSnapshot(), wiping out the protective cache.
If the script then stalls the main thread (e.g., using alert() or a busy loop) and the OS clipboard is updated externally during this delay (e.g., the user takes a screenshot), a subsequent call to DataTransferItem.getAsFile() will force a cache miss. DataObjectItem::GetAsFile() handles the PNG fetch by making a synchronous IPC to the browser process (system_clipboard_->ReadPng()). The browser process returns the current OS clipboard content.
Crucially, unlike DataObjectItem::GetAsString() which explicitly verifies that system_clipboard_->SequenceNumber() == sequence_number_ before returning data, DataObjectItem::GetAsFile() fails to perform any sequence number validation. Consequently, it potentially returns the newly copied data (like a sensitive screenshot) to the page, data the user never intended to paste.
Potential Attack Scenario
Note: These are potential steps as an automated agent has not run a working Proof of Concept.
- A user copies an innocuous image and pastes it into a malicious page (e.g., pressing Ctrl+V).
- The malicious page’s
pasteevent listener executes and stores a reference to theimage/pngDataTransferItem. - The script registers a one-shot
copyevent listener that callsevent.preventDefault(). - The script executes
document.execCommand('copy'). This succeeds due to the transient user activation from the paste action, triggering the one-shot listener which prevents the default copy action but causesSystemClipboardto reset its snapshot cache. - The script stalls the renderer’s main thread (e.g., by calling
alert('...')). - During the stall, the user takes a screenshot (updating the OS clipboard).
- The user dismisses the alert. The script resumes and calls
getAsFile()on the stored PNG item. - Due to the missing sequence number check and the wiped cache,
DataObjectItem::GetAsFile()performs a synchronous IPC, retrieves the new screenshot from the browser process, and returns it to the script.
Suggested Fix
Add a sequence number validation check in DataObjectItem::GetAsFile() for the kClipboardSource PNG path, mirroring the logic found in GetAsString():
// in third_party/blink/renderer/core/clipboard/data_object_item.cc
DCHECK_EQ(source_, DataSource::kClipboardSource);
+ if (system_clipboard_->SequenceNumber() != sequence_number_) {
+ return nullptr;
+ }
if (GetType() == ui::kMimeTypePng) {
mojo_base::BigBuffer png_data =
system_clipboard_->ReadPng(mojom::blink::ClipboardBuffer::kStandard);
// ...
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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. And please feel free to reach out to me directly if you have concerns or feedback on the project.