CVE-2026-95360
Overview
Background
- `SystemClipboard`
- Blink’s renderer-side interface to the browser-process
ClipboardHost, exposing reads likeReadPlainTextplus a monotonically increasingSequenceNumberthat changes whenever the clipboard contents change. - Default paste behavior
- the built-in editing action that runs after a
pasteevent handler does not consume the clipboard, injecting the clipboard content into the editable region. - `ScopedSystemClipboardSnapshot`
- a scope guard that caches
ClipboardHostresults so multipleSystemClipboardreads within the paste coalesce into at most one IPC per data type. - Lazy clipboard read
- the pattern where a synchronous paste defers reading actual clipboard bytes until after the
paste/beforeinputevents, leaving a time gap during which the clipboard can be replaced.
Root Cause Analysis
During a synchronous paste, ClipboardCommands::Paste dispatched the paste and beforeinput events and only afterward read the real clipboard bytes in PasteFromClipboard / PasteAsPlainTextFromClipboard to perform the default paste. Because a malicious page could stall its paste handler with a busy loop, the clipboard could be overwritten between the moment the paste was invoked and the moment the default behavior read it, and unlike ClipboardPromise and DataObjectItem, the default paste path performed no SequenceNumber validation. The invariant violated is that a paste must inject exactly the content present on the clipboard when the paste action was invoked, not whatever content is present later.
The fix records paste_sequence_number from system_clipboard->SequenceNumber() at the start of Paste and re-checks it before dispatching the plain-text event, before performing the fragment paste, and before building the beforeinput DataTransfer via the new CreateBeforeInputDataTransfer, aborting or substituting an empty DataObject when the numbers diverge. It also adds a ScopedSystemClipboardSnapshot to ExecutePasteAndMatchStyle so that path gains the same coalescing and sequence checking.
SequenceNumber at paste start and refuses to inject (and refuses to expose data to beforeinput) whenever that number no longer matches.Attack Path
- Set up an editable target
The attacker page presents a contenteditable or input element and registers a
pasteevent handler that deliberately does not read the clipboard, so the default paste behavior will run. - Stall the paste handler When the user (or script) invokes paste, the handler runs a busy loop, holding the synchronous paste open and creating a window before the default behavior lazily reads the clipboard.
- Swap the clipboard content
During that window the page replaces the clipboard contents, so
SystemClipboard’sSequenceNumberadvances past the value implied at paste invocation. (inferred) - Leak the substituted content
The default paste path reads the now-changed clipboard and either injects it into the page or exposes it through the
beforeinputDataTransfer, disclosing or pasting content the user never intended. (inferred)
Impact Assessment
beforeinput context, which could leak or substitute sensitive clipboard data. The preconditions are an editable target, a paste handler that defers to default behavior, and the page’s ability to stall that handler and mutate the clipboard during the gap. The advisory classifies this as a race condition in Editing and states no exploited-in-the-wild status; any escalation beyond content substitution/disclosure is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/editing/commands/clipboard_commands.cc |
modified | |
CORE_EXPORTthird_party/blink/renderer/core/editing/commands/clipboard_commands.cc |
modified | |
ChangeClipboardEventListenerthird_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc |
modified | |
EventCountingListenerthird_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc |
modified | |
ifthird_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc |
modified | |
Samplethird_party/blink/web_tests/editing/assert_selection.js |
modified |
Files Changed
third_party/blink/renderer/core/editing/commands/clipboard_commands.ccthird_party/blink/renderer/core/editing/commands/clipboard_commands_test.ccthird_party/blink/web_tests/external/wpt/editing/other/paste-clipboard-change.tentative.htmlthird_party/blink/web_tests/editing/assert_selection.jsthird_party/blink/renderer/core/editing/commands/clipboard_commands.hui/base/clipboard/test/test_clipboard.cc
Audit Directions
- Lazy resource reads across event dispatchLook for editing/clipboard paths that dispatch page-controllable events (
paste,beforeinput,input) and only read the underlying resource afterward, where the page can stall a handler and mutate state in between. - Missing sequence-number validationAudit any consumer of
SystemClipboardthat does not compareSequenceNumberagainst a value captured at operation start, sinceClipboardPromiseandDataObjectItemvalidate but ad hoc default paths may not. - Inconsistent snapshot scopingSearch for clipboard operations that omit
ScopedSystemClipboardSnapshot(asExecutePasteAndMatchStyleoriginally did), which both multiplies IPCs and skips the coalesced, consistent view that enables sequence checking.
Patch
From 4f9d868279b8169a4e1f6617629bb093c04abef4 Mon Sep 17 00:00:00 2001
From: Daniel Clark <daniec@microsoft.com>
Date: Mon, 14 Sep 2026 12:09:28 -0700
Subject: [PATCH] Ensure default paste behavior doesn't leak changed clipboard content
A synchronous paste reads the clipboard lazily, so if the paste event
handler doesn't read the clipboard, the clipboard read only happens
after the paste event when the default paste behavior runs.
The page could potentially slow down the paste event handler with a busy
loop, creating a gap where the clipboard contents could change. The
result is that the default paste behavior could paste the new content
rather than the content on the clipboard at the time the paste action
was invoked. ClipboardPromise or DataObjectItem each have their own
sequence number checks, but the default paste behavior doesn't go
through either.
In this CL, fix this by having ClipboardCommand record the sequence
number at the start of the paste. If these diverge, don't perform
the default paste action that would inject the content into the
page. Also, don't pass actual clipboard data to the beforeinput event
if we detect that the clipboard has changed by that point.
ClipboardCommands::ExecutePasteAndMatchStyle was missing a
ScopedSystemClipboardSnapshot, so add one so that it gets the same
sequence number checking and IPC coalescing as Paste.
Fixed: 517584808
Change-Id: I29fc34d6e84b74914287f819bb3f66e44459ab7d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8217390
Reviewed-by: Kent Tamura <tkent@chromium.org>
Commit-Queue: Dan Clark <daniec@microsoft.com>
Cr-Commit-Position: refs/heads/main@{#1697410}
---
diff --git a/third_party/blink/renderer/core/editing/commands/clipboard_commands.cc b/third_party/blink/renderer/core/editing/commands/clipboard_commands.cc
index bc94b30..039fb1d7 100644
--- a/third_party/blink/renderer/core/editing/commands/clipboard_commands.cc
+++ b/third_party/blink/renderer/core/editing/commands/clipboard_commands.cc
@@ -117,6 +117,25 @@
State state_;
};
+DataTransfer* CreateBeforeInputDataTransfer(
+ LocalFrame& frame,
+ Element* target,
+ PasteMode paste_mode,
+ absl::uint128 paste_sequence_number) {
+ SystemClipboard* const system_clipboard = frame.GetSystemClipboard();
+ DataObject* data_object = DataObject::Create();
+ if (system_clipboard->SequenceNumber() == paste_sequence_number) {
+ data_object = DataObject::CreateFromClipboard(
+ target ? target->GetExecutionContext() : nullptr, system_clipboard,
+ paste_mode);
+ if (system_clipboard->SequenceNumber() != paste_sequence_number) {
+ data_object = DataObject::Create();
+ }
+ }
+ return DataTransfer::Create(DataTransfer::kCopyAndPaste,
+ DataTransferAccessPolicy::kReadable, data_object);
+}
+
} // namespace
bool ClipboardCommands::CanReadClipboard(LocalFrame& frame,
@@ -500,13 +519,18 @@
void ClipboardCommands::PasteAsPlainTextFromClipboard(
LocalFrame& frame,
- EditorCommandSource source) {
+ EditorCommandSource source,
+ absl::uint128 paste_sequence_number) {
Element* const target = FindEventTargetForClipboardEvent(frame, source);
if (!target)
return;
+ const String text = frame.GetSystemClipboard()->ReadPlainText();
+ const bool smart_replace = CanSmartReplaceInClipboard(frame);
+ if (frame.GetSystemClipboard()->SequenceNumber() != paste_sequence_number) {
+ return;
+ }
target->DispatchEvent(*TextEvent::CreateForPlainTextPaste(
- frame.DomWindow(), frame.GetSystemClipboard()->ReadPlainText(),
- CanSmartReplaceInClipboard(frame)));
+ frame.DomWindow(), text, smart_replace));
}
ClipboardCommands::FragmentAndPlainText
@@ -553,16 +577,21 @@
return std::make_pair(fragment, true);
}
-void ClipboardCommands::PasteFromClipboard(LocalFrame& frame,
- EditorCommandSource source,
- DataTransfer* data_transfer) {
+void ClipboardCommands::PasteFromClipboard(
+ LocalFrame& frame,
+ EditorCommandSource source,
+ DataTransfer* data_transfer,
+ absl::uint128 paste_sequence_number) {
const ClipboardCommands::FragmentAndPlainText fragment_and_plain_text =
GetFragmentFromClipboard(frame);
+ const bool smart_replace = CanSmartReplaceInClipboard(frame);
+ if (frame.GetSystemClipboard()->SequenceNumber() != paste_sequence_number) {
+ return;
+ }
if (!fragment_and_plain_text.first)
return;
- PasteAsFragment(frame, fragment_and_plain_text.first,
- CanSmartReplaceInClipboard(frame),
+ PasteAsFragment(frame, fragment_and_plain_text.first, smart_replace,
fragment_and_plain_text.second, source, data_transfer);
}
@@ -576,10 +605,13 @@
}
// The code below makes multiple calls to SystemClipboard methods which
- // implies multiple IPC calls to the ClipboardHost in the browaser process.
+ // implies multiple IPC calls to the ClipboardHost in the browser process.
// SystemClipboard snapshotting tells SystemClipboard to cache results from
// the ClipboardHost so that at most one IPC is made for each type.
- ScopedSystemClipboardSnapshot snapshot(*frame.GetSystemClipboard());
+ SystemClipboard* const system_clipboard = frame.GetSystemClipboard();
+ ScopedSystemClipboardSnapshot snapshot(*system_clipboard);
+ const absl::uint128 paste_sequence_number =
+ system_clipboard->SequenceNumber();
if (!DispatchPasteEvent(frame, PasteMode::kAllMimeTypes, source))
return;
@@ -611,11 +643,8 @@
if (source == EditorCommandSource::kMenuOrKeyBinding) {
Element* const target = FindEventTargetForClipboardEvent(frame, source);
- data_transfer = DataTransfer::Create(
- DataTransfer::kCopyAndPaste, DataTransferAccessPolicy::kReadable,
- DataObject::CreateFromClipboard(
- target ? target->GetExecutionContext() : nullptr,
- frame.GetSystemClipboard(), paste_mode));
+ data_transfer = CreateBeforeInputDataTransfer(frame, target, paste_mode,
+ paste_sequence_number);
if (DispatchBeforeInputDataTransfer(
target, InputEvent::InputType::kInsertFromPaste, data_transfer) !=
@@ -631,11 +660,15 @@
return;
}
- if (paste_mode == PasteMode::kAllMimeTypes) {
- PasteFromClipboard(frame, source, data_transfer);
+ if (system_clipboard->SequenceNumber() != paste_sequence_number) {
return;
}
- PasteAsPlainTextFromClipboard(frame, source);
+
+ if (paste_mode == PasteMode::kAllMimeTypes) {
+ PasteFromClipboard(frame, source, data_transfer, paste_sequence_number);
+ return;
+ }
+ PasteAsPlainTextFromClipboard(frame, source, paste_sequence_number);
}
class CORE_EXPORT PasteImageResourceObserver final
@@ -843,6 +876,15 @@
Event*,
EditorCommandSource source,
const String&) {
+ // The code below makes multiple calls to SystemClipboard methods which
+ // implies multiple IPC calls to the ClipboardHost in the browser process.
+ // SystemClipboard snapshotting tells SystemClipboard to cache results from
+ // the ClipboardHost so that at most one IPC is made for each type.
+ SystemClipboard* const system_clipboard = frame.GetSystemClipboard();
+ ScopedSystemClipboardSnapshot snapshot(*system_clipboard);
+ const absl::uint128 paste_sequence_number =
+ system_clipboard->SequenceNumber();
+
if (!DispatchPasteEvent(frame, PasteMode::kPlainTextOnly, source))
return false;
// A 'paste' event handler may destroy target frame.
@@ -864,11 +906,8 @@
Element* const target = FindEventTargetForClipboardEvent(frame, source);
- DataTransfer* data_transfer = DataTransfer::Create(
- DataTransfer::kCopyAndPaste, DataTransferAccessPolicy::kReadable,
- DataObject::CreateFromClipboard(
- target ? target->GetExecutionContext() : nullptr,
- frame.GetSystemClipboard(), PasteMode::kPlainTextOnly));
+ DataTransfer* data_transfer = CreateBeforeInputDataTransfer(
+ frame, target, PasteMode::kPlainTextOnly, paste_sequence_number);
if (DispatchBeforeInputDataTransfer(
target, InputEvent::InputType::kInsertFromPaste, data_transfer) !=
DispatchEventResult::kNotCanceled) {
@@ -883,7 +922,11 @@
return true;
}
- PasteAsPlainTextFromClipboard(frame, source);
+ if (system_clipboard->SequenceNumber() != paste_sequence_number) {
+ return false;
+ }
+
+ PasteAsPlainTextFromClipboard(frame, source, paste_sequence_number);
return true;
}
diff --git a/third_party/blink/renderer/core/editing/commands/clipboard_commands.h b/third_party/blink/renderer/core/editing/commands/clipboard_commands.h
index cedf743..11a31e80f1 100644
--- a/third_party/blink/renderer/core/editing/commands/clipboard_commands.h
+++ b/third_party/blink/renderer/core/editing/commands/clipboard_commands.h
@@ -138,10 +138,13 @@
bool match_style,
EditorCommandSource,
DataTransfer* = nullptr);
- static void PasteAsPlainTextFromClipboard(LocalFrame&, EditorCommandSource);
+ static void PasteAsPlainTextFromClipboard(LocalFrame&,
+ EditorCommandSource,
+ absl::uint128);
static void PasteFromClipboard(LocalFrame&,
EditorCommandSource,
- DataTransfer*);
+ DataTransfer*,
+ absl::uint128);
static void PasteFromImageUrl(LocalFrame&, EditorCommandSource, String);
using FragmentAndPlainText = std::pair<DocumentFragment*, const bool>;
diff --git a/third_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc b/third_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc
index 6f26ee0..6548b6b 100644
--- a/third_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc
+++ b/third_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc
@@ -4,14 +4,17 @@
#include <memory>
+#include "base/memory/raw_ptr.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
+#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
#include "third_party/blink/renderer/core/editing/editor.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/set_selection_options.h"
#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/event_type_names.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html_names.h"
@@ -28,6 +31,73 @@
namespace blink {
+namespace {
+
+class ChangeClipboardEventListener final : public NativeEventListener {
+ public:
+ explicit ChangeClipboardEventListener(
+ mojom::blink::ClipboardHost* clipboard_host)
+ : clipboard_host_(clipboard_host) {}
+
+ void Invoke(ExecutionContext*, Event*) override {
+ clipboard_host_->WriteText("changed");
+ clipboard_host_->CommitWrite();
+ }
+
+ private:
+ raw_ptr<mojom::blink::ClipboardHost> clipboard_host_;
+};
+
+class EventCountingListener final : public NativeEventListener {
+ public:
+ void Invoke(ExecutionContext*, Event*) override { ++event_count_; }
+ int EventCount() const { return event_count_; }
+
+ private:
+ int event_count_ = 0;
+};
+
+void ExpectDefaultPasteResult(const AtomicString* clipboard_change_event_type,
+ const String& expected_html) {
+ test::TaskEnvironment task_environment;
+ auto page_holder = std::make_unique<DummyPageHolder>(gfx::Size(1, 1));
+ LocalFrame& frame = page_holder->GetFrame();
+
+ PageTestBase::MockClipboardHostProvider mock_clipboard_host_provider(
+ frame.GetBrowserInterfaceBroker());
+ mojom::blink::ClipboardHost* clipboard_host =
+ mock_clipboard_host_provider.clipboard_host();
+
+ HTMLElement* body = page_holder->GetDocument().body();
+ body->setAttribute(html_names::kContenteditableAttr, keywords::kTrue);
+ body->Focus();
+ frame.GetDocument()->UpdateStyleAndLayout(DocumentUpdateReason::kTest);
+ frame.Selection().SetSelection(
+ SelectionInDomTree::Builder().SelectAllChildren(*body).Build(),
+ SetSelectionOptions());
+ ASSERT_TRUE(
+ frame.Selection().ComputeVisibleSelectionInDomTree().IsContentEditable());
+
+ clipboard_host->WriteText("original");
+ clipboard_host->CommitWrite();
+ test::RunPendingTasks();
+
+ if (clipboard_change_event_type) {
+ body->addEventListener(
+ *clipboard_change_event_type,
+ MakeGarbageCollected<ChangeClipboardEventListener>(clipboard_host));
+ }
+ auto* before_input_listener = MakeGarbageCollected<EventCountingListener>();
+ body->addEventListener(event_type_names::kBeforeinput, before_input_listener);
+
+ frame.GetEditor().ExecuteCommand("Paste");
+
+ EXPECT_EQ(before_input_listener->EventCount(), 1);
+ EXPECT_EQ(body->GetInnerHTMLString(), expected_html);
+}
+
+} // namespace
+
// Paste in kSelection mode (the state ExecutePasteGlobalSelection sets on
// middle-click) must not leak an image planted on the kStandard buffer
// through GetFragmentFromClipboard's image fallback.
@@ -69,4 +139,19 @@
<< html.Utf8();
}
+TEST(ClipboardCommandsPasteTest,
+ DefaultPasteWithoutClipboardChangePastesClipboardContent) {
+ ExpectDefaultPasteResult(nullptr, "original");
+}
+
+TEST(ClipboardCommandsPasteTest,
+ ForeignClipboardChangeDuringPastePreventsDefaultPaste) {
+ ExpectDefaultPasteResult(&event_type_names::kPaste, "");
+}
+
+TEST(ClipboardCommandsPasteTest,
+ ForeignClipboardChangeDuringBeforeInputPreventsDefaultPaste) {
+ ExpectDefaultPasteResult(&event_type_names::kBeforeinput, "");
+}
+
} // namespace blink
diff --git a/third_party/blink/web_tests/editing/assert_selection.js b/third_party/blink/web_tests/editing/assert_selection.js
index 640f10cd..28b0999 100644
--- a/third_party/blink/web_tests/editing/assert_selection.js
+++ b/third_party/blink/web_tests/editing/assert_selection.js
@@ -714,10 +714,18 @@
function setClipboardData(html, opt_text) {
assert_not_equals(window.internals, undefined,
'This test requests clipboard access from JavaScript.');
+ // Run the copy in the same document (the sample IFRAME) that will receive the
+ // paste. If the copy runs in a different frame than the paste, the two use
+ // different ClipboardHost mojo pipes, which are not mutually ordered. The
+ // paste creates a SystemClipboard snapshot that records the clipboard
+ // sequence number; if the copy's asynchronous CommitWrite has not yet been
+ // processed on its own pipe, the snapshot baselines a stale sequence number
+ // and the paste is spuriously treated as reading changed clipboard content.
+ const doc = this.document;
function computeTextData() {
if (opt_text !== undefined)
return opt_text;
- const element = document.createElement('div');
+ const element = doc.createElement('div');
element.innerHTML = html;
return element.textContent;
}
@@ -727,9 +735,9 @@
clipboardData.setData('text/html', html);
event.preventDefault();
}
- document.addEventListener('copy', copyHandler);
- document.execCommand('copy');
- document.removeEventListener('copy', copyHandler);
+ doc.addEventListener('copy', copyHandler);
+ doc.execCommand('copy');
+ doc.removeEventListener('copy', copyHandler);
}
class Sample {
diff --git a/ui/base/clipboard/test/test_clipboard.cc b/ui/base/clipboard/test/test_clipboard.cc
index f705374..3312245 100644
--- a/ui/base/clipboard/test/test_clipboard.cc
+++ b/ui/base/clipboard/test/test_clipboard.cc
@@ -453,6 +453,7 @@
url_title = other.url_title;
html_src_url = other.html_src_url;
png = other.png;
+ filenames = other.filenames;
data_src = other.data_src;
}
@@ -463,6 +464,7 @@
url_title = other.url_title;
html_src_url = other.html_src_url;
png = other.png;
+ filenames = other.filenames;
data_src = other.data_src;
return *this;
}
diff --git a/third_party/blink/web_tests/external/wpt/editing/other/paste-clipboard-change.tentative.html b/third_party/blink/web_tests/external/wpt/editing/other/paste-clipboard-change.tentative.html
index b029f84..b71ccd8 100644
... (file diff truncated)
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc b/third_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc
index 6f26ee0..6548b6b 100644
--- a/third_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc
+++ b/third_party/blink/renderer/core/editing/commands/clipboard_commands_test.cc
@@ -4,14 +4,17 @@
#include <memory>
+#include "base/memory/raw_ptr.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
+#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
#include "third_party/blink/renderer/core/editing/editor.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/set_selection_options.h"
#include "third_party/blink/renderer/core/editing/visible_selection.h"
+#include "third_party/blink/renderer/core/event_type_names.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html_names.h"
@@ -28,6 +31,73 @@
namespace blink {
+namespace {
+
+class ChangeClipboardEventListener final : public NativeEventListener {
+ public:
+ explicit ChangeClipboardEventListener(
+ mojom::blink::ClipboardHost* clipboard_host)
+ : clipboard_host_(clipboard_host) {}
+
+ void Invoke(ExecutionContext*, Event*) override {
+ clipboard_host_->WriteText("changed");
+ clipboard_host_->CommitWrite();
+ }
+
+ private:
+ raw_ptr<mojom::blink::ClipboardHost> clipboard_host_;
+};
+
+class EventCountingListener final : public NativeEventListener {
+ public:
+ void Invoke(ExecutionContext*, Event*) override { ++event_count_; }
+ int EventCount() const { return event_count_; }
+
+ private:
+ int event_count_ = 0;
+};
+
+void ExpectDefaultPasteResult(const AtomicString* clipboard_change_event_type,
+ const String& expected_html) {
+ test::TaskEnvironment task_environment;
+ auto page_holder = std::make_unique<DummyPageHolder>(gfx::Size(1, 1));
+ LocalFrame& frame = page_holder->GetFrame();
+
+ PageTestBase::MockClipboardHostProvider mock_clipboard_host_provider(
+ frame.GetBrowserInterfaceBroker());
+ mojom::blink::ClipboardHost* clipboard_host =
+ mock_clipboard_host_provider.clipboard_host();
+
+ HTMLElement* body = page_holder->GetDocument().body();
+ body->setAttribute(html_names::kContenteditableAttr, keywords::kTrue);
+ body->Focus();
+ frame.GetDocument()->UpdateStyleAndLayout(DocumentUpdateReason::kTest);
+ frame.Selection().SetSelection(
+ SelectionInDomTree::Builder().SelectAllChildren(*body).Build(),
+ SetSelectionOptions());
+ ASSERT_TRUE(
+ frame.Selection().ComputeVisibleSelectionInDomTree().IsContentEditable());
+
+ clipboard_host->WriteText("original");
+ clipboard_host->CommitWrite();
+ test::RunPendingTasks();
+
+ if (clipboard_change_event_type) {
+ body->addEventListener(
+ *clipboard_change_event_type,
+ MakeGarbageCollected<ChangeClipboardEventListener>(clipboard_host));
+ }
+ auto* before_input_listener = MakeGarbageCollected<EventCountingListener>();
+ body->addEventListener(event_type_names::kBeforeinput, before_input_listener);
+
+ frame.GetEditor().ExecuteCommand("Paste");
+
+ EXPECT_EQ(before_input_listener->EventCount(), 1);
+ EXPECT_EQ(body->GetInnerHTMLString(), expected_html);
+}
+
+} // namespace
+
// Paste in kSelection mode (the state ExecutePasteGlobalSelection sets on
// middle-click) must not leak an image planted on the kStandard buffer
// through GetFragmentFromClipboard's image fallback.
@@ -69,4 +139,19 @@
<< html.Utf8();
}
+TEST(ClipboardCommandsPasteTest,
+ DefaultPasteWithoutClipboardChangePastesClipboardContent) {
+ ExpectDefaultPasteResult(nullptr, "original");
+}
+
+TEST(ClipboardCommandsPasteTest,
+ ForeignClipboardChangeDuringPastePreventsDefaultPaste) {
+ ExpectDefaultPasteResult(&event_type_names::kPaste, "");
+}
+
+TEST(ClipboardCommandsPasteTest,
+ ForeignClipboardChangeDuringBeforeInputPreventsDefaultPaste) {
+ ExpectDefaultPasteResult(&event_type_names::kBeforeinput, "");
+}
+
} // namespace blink
diff --git a/third_party/blink/web_tests/editing/assert_selection.js b/third_party/blink/web_tests/editing/assert_selection.js
index 640f10cd..28b0999 100644
--- a/third_party/blink/web_tests/editing/assert_selection.js
+++ b/third_party/blink/web_tests/editing/assert_selection.js
@@ -714,10 +714,18 @@
function setClipboardData(html, opt_text) {
assert_not_equals(window.internals, undefined,
'This test requests clipboard access from JavaScript.');
+ // Run the copy in the same document (the sample IFRAME) that will receive the
+ // paste. If the copy runs in a different frame than the paste, the two use
+ // different ClipboardHost mojo pipes, which are not mutually ordered. The
+ // paste creates a SystemClipboard snapshot that records the clipboard
+ // sequence number; if the copy's asynchronous CommitWrite has not yet been
+ // processed on its own pipe, the snapshot baselines a stale sequence number
+ // and the paste is spuriously treated as reading changed clipboard content.
+ const doc = this.document;
function computeTextData() {
if (opt_text !== undefined)
return opt_text;
- const element = document.createElement('div');
+ const element = doc.createElement('div');
element.innerHTML = html;
return element.textContent;
}
@@ -727,9 +735,9 @@
clipboardData.setData('text/html', html);
event.preventDefault();
}
- document.addEventListener('copy', copyHandler);
- document.execCommand('copy');
- document.removeEventListener('copy', copyHandler);
+ doc.addEventListener('copy', copyHandler);
+ doc.execCommand('copy');
+ doc.removeEventListener('copy', copyHandler);
}
class Sample {
diff --git a/third_party/blink/web_tests/external/wpt/editing/other/paste-clipboard-change.tentative.html b/third_party/blink/web_tests/external/wpt/editing/other/paste-clipboard-change.tentative.html
index b029f84..b71ccd8 100644
--- a/third_party/blink/web_tests/external/wpt/editing/other/paste-clipboard-change.tentative.html
+++ b/third_party/blink/web_tests/external/wpt/editing/other/paste-clipboard-change.tentative.html
@@ -21,7 +21,12 @@
const testElement = document.getElementById(testingId);
const utils = new EditorTestUtils(testElement);
-promise_test(async () => {
+async function pasteAfterChangingClipboard(eventType) {
+ if (testElement.isContentEditable)
+ testElement.textContent = "";
+ else
+ testElement.value = "";
+
// Copy the content to the clipboard.
const range = document.createRange();
const contentToCopy = document.getElementById("contentCopy");
@@ -36,14 +41,35 @@
e.preventDefault();
e.clipboardData.setData("text/plain", "New text");
}, { once: true });
- testElement.addEventListener("paste", (e) => {
+ let beforeInputFired = false;
+ testElement.addEventListener("beforeinput", () => {
+ beforeInputFired = true;
+ }, { once: true });
+ testElement.addEventListener(eventType, () => {
// Overwrite the clipboard content.
document.execCommand("copy");
}, { once: true });
await test_driver.click(testElement);
await utils.sendPasteShortcutKey();
- // Check content.
- assert_equals(testElement.isContentEditable ? testElement.textContent : testElement.value, "New text");
+ return {
+ beforeInputFired,
+ pastedContent: testElement.isContentEditable ?
+ testElement.textContent : testElement.value,
+ };
+}
+
+promise_test(async () => {
+ const result = await pasteAfterChangingClipboard("paste");
+ assert_true(result.beforeInputFired);
+ assert_equals(result.pastedContent, "",
+ "clipboard content written during the paste handler must not be pasted");
}, "clipboard change during paste event handler");
+
+promise_test(async () => {
+ const result = await pasteAfterChangingClipboard("beforeinput");
+ assert_true(result.beforeInputFired);
+ assert_equals(result.pastedContent, "",
+ "clipboard content written during beforeinput must not be pasted");
+}, "clipboard change during beforeinput event handler");
</script>
diff --git a/ui/base/clipboard/test/test_clipboard.cc b/ui/base/clipboard/test/test_clipboard.cc
index f705374..3312245 100644
--- a/ui/base/clipboard/test/test_clipboard.cc
+++ b/ui/base/clipboard/test/test_clipboard.cc
@@ -453,6 +453,7 @@
url_title = other.url_title;
html_src_url = other.html_src_url;
png = other.png;
+ filenames = other.filenames;
data_src = other.data_src;
}
@@ -463,6 +464,7 @@
url_title = other.url_title;
html_src_url = other.html_src_url;
png = other.png;
+ filenames = other.filenames;
data_src = other.data_src;
return *this;
}