Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in DataTransfer
DescriptionInappropriate implementation in DataTransfer
ComponentDataTransfer
Bug ClassLogic Error
Tracker513762372
Fix commit92a7afe698ce (chromium/src) +222/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
CORE_EXPORT
third_party/blink/renderer/core/editing/commands/clipboard_commands.h
modified
permission_service_
third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
modified

Files Changed

  • third_party/blink/renderer/core/editing/commands/clipboard_commands.cc
  • third_party/blink/renderer/core/editing/commands/clipboard_commands.h
  • third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
  • third_party/blink/renderer/modules/clipboard/clipboard_promise.h
  • third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
From 92a7afe698cee4629c44660c83881df31779f796 Mon Sep 17 00:00:00 2001
From: Jack Miller <jackmil@google.com>
Date: Mon, 15 Jun 2026 12:01:20 -0700
Subject: [PATCH] Invalidate paste event when clipboard content changes

A website can initiate a synchronous API (e.g. `alert`) during a `paste`
event, causing the `paste` event to run longer than typical. During this
synchronous event, the user may copy something new. In this event, the
new contents are pasted rather than the contents of the clipboard at the
start of the paste event.

This change uses the `SystemClipboard` sequence number to check if the
contents of the clipboard has changed since the start of the `paste`
event and invalidates the event if it has changed.

Bug: 513762372
Change-Id: I064a0dbe1e5684399de8cf44a692cd53a3e069d2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7896735
Reviewed-by: Dan Clark <daniec@microsoft.com>
Reviewed-by: Jesse McKenna <jessemckenna@google.com>
Commit-Queue: Jack Miller <jackmil@google.com>
Cr-Commit-Position: refs/heads/main@{#1646979}
---

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 e61f652a9..463c4d9 100644
--- a/third_party/blink/renderer/core/editing/commands/clipboard_commands.cc
+++ b/third_party/blink/renderer/core/editing/commands/clipboard_commands.cc
@@ -95,13 +95,17 @@
   struct State {
     const AtomicString* event_type = nullptr;
     std::optional<EditorCommandSource> source;
+    std::optional<absl::uint128> sequence_number = 0;
   };
 
-  base::AutoReset<State> SetState(const AtomicString& event_type,
-                                  EditorCommandSource source) {
+  base::AutoReset<State> SetState(
+      const AtomicString& event_type,
+      EditorCommandSource source,
+      std::optional<absl::uint128> sequence_number) {
     State new_state;
     new_state.event_type = &event_type;
     new_state.source = source;
+    new_state.sequence_number = sequence_number;
     return base::AutoReset<State>(&state_, new_state);
   }
 
@@ -154,6 +158,14 @@
          event_state.source == EditorCommandSource::kMenuOrKeyBinding;
 }
 
+std::optional<absl::uint128>
+ClipboardCommands::GetSequenceNumberForExecutingPaste(
+    ExecutionContext& context) {
+  const ExecutionContextClipboardEventState::State& event_state =
+      ExecutionContextClipboardEventState::From(context).GetState();
+  return event_state.sequence_number;
+}
+
 bool ClipboardCommands::CanSmartReplaceInClipboard(LocalFrame& frame) {
   return frame.GetEditor().SmartInsertDeleteEnabled() &&
          frame.GetSystemClipboard()->IsFormatAvailable(
@@ -222,6 +234,10 @@
     return true;
 
   SystemClipboard* system_clipboard = frame.GetSystemClipboard();
+  std::optional<absl::uint128> sequence_number =
+      event_type == event_type_names::kPaste
+          ? std::make_optional(system_clipboard->SequenceNumber())
+          : std::nullopt;
   DataTransfer* const data_transfer = DataTransfer::Create(
       DataTransfer::kCopyAndPaste, policy,
       policy == DataTransferAccessPolicy::kWritable
@@ -234,7 +250,7 @@
     base::AutoReset<ExecutionContextClipboardEventState::State> reset =
         ExecutionContextClipboardEventState::From(
             *target->GetExecutionContext())
-            .SetState(event_type, source);
+            .SetState(event_type, source, sequence_number);
     Event* const evt = ClipboardEvent::Create(event_type, data_transfer);
     target->DispatchEvent(*evt);
     no_default_processing = evt->defaultPrevented();
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 6a6bd12b..74613a6 100644
--- a/third_party/blink/renderer/core/editing/commands/clipboard_commands.h
+++ b/third_party/blink/renderer/core/editing/commands/clipboard_commands.h
@@ -32,6 +32,8 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_CLIPBOARD_COMMANDS_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_EDITING_COMMANDS_CLIPBOARD_COMMANDS_H_
 
+#include "base/gtest_prod_util.h"
+#include "third_party/abseil-cpp/absl/numeric/int128.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/editing/forward.h"
 #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
@@ -53,6 +55,10 @@
 // This class provides static functions about commands related to clipboard.
 class CORE_EXPORT ClipboardCommands {
   STATIC_ONLY(ClipboardCommands);
+  FRIEND_TEST_ALL_PREFIXES(ClipboardTest, PasteEventUninterruptedReadText);
+  FRIEND_TEST_ALL_PREFIXES(ClipboardTest,
+                           PasteEventInterruptedReadTextRejected);
+  FRIEND_TEST_ALL_PREFIXES(ClipboardTest, PasteEventInterruptedReadRejected);
 
  public:
   static bool EnabledCopy(LocalFrame&, Event*, EditorCommandSource);
@@ -95,6 +101,9 @@
   static bool IsExecutingCutOrCopy(ExecutionContext&);
   // As above, but for the "paste" event.
   static bool IsExecutingPaste(ExecutionContext&);
+  // Returns the clipboard sequence number at the start of executing paste.
+  static std::optional<absl::uint128> GetSequenceNumberForExecutingPaste(
+      ExecutionContext&);
 
  private:
   static bool CanSmartReplaceInClipboard(LocalFrame&);
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc b/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
index 2c0c5d87..cf97d08 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
@@ -135,7 +135,12 @@
                                    ExceptionState& exception_state)
     : ExecutionContextLifecycleObserver(context),
       script_promise_resolver_(resolver),
-      permission_service_(context) {}
+      permission_service_(context) {
+  if (context && ClipboardCommands::IsExecutingPaste(*context)) {
+    sequence_number_at_paste_start_ =
+        ClipboardCommands::GetSequenceNumberForExecutingPaste(*context);
+  }
+}
 
 ClipboardPromise::~ClipboardPromise() = default;
 
@@ -711,6 +716,20 @@
        ClipboardCommands::IsExecutingCutOrCopy(*context)) ||
       (permission == mojom::blink::PermissionName::CLIPBOARD_READ &&
        ClipboardCommands::IsExecutingPaste(*context))) {
+    // Validate the contents of the user's clipboard have not changed since the
+    // start of the paste event and fail if it has. This prevents an attacker
+    // from initiating a synchronous javascript command (e.g. alert) during the
+    // paste event and the user unknowingly copies something new before the
+    // paste event resolves.
+    if (permission == mojom::blink::PermissionName::CLIPBOARD_READ &&
+        sequence_number_at_paste_start_.has_value() &&
+        GetSystemClipboard()->SequenceNumber() !=
+            *sequence_number_at_paste_start_) {
+      script_promise_resolver_->RejectWithDOMException(
+          DOMExceptionCode::kDataError,
+          "Clipboard contents changed since paste event started.");
+      return;
+    }
     GetClipboardTaskRunner()->PostTask(
         FROM_HERE,
         blink::BindOnce(std::move(callback),
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_promise.h b/third_party/blink/renderer/modules/clipboard/clipboard_promise.h
index 85fee98..9af54696 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_promise.h
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_promise.h
@@ -201,6 +201,8 @@
   bool will_read_unprocessed_html_ = false;
   // Plain text data to be written to the clipboard.
   String plain_text_;
+  // Clipboard sequence number captured at the start of paste event.
+  std::optional<absl::uint128> sequence_number_at_paste_start_;
   // The list of formats read from the clipboard.
   HeapVector<std::pair<String, Member<V8UnionBlobOrString>>>
       clipboard_item_data_;
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc b/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
index 395fd61..8c2ba41 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
@@ -4,8 +4,10 @@
 
 #include "third_party/blink/renderer/modules/clipboard/clipboard.h"
 
+#include "base/functional/bind.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/run_until.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/mojom/clipboard/clipboard.mojom-blink.h"
 #include "third_party/blink/public/mojom/permissions/permission.mojom-blink.h"
@@ -13,12 +15,18 @@
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_blob.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_clipboard_read_options.h"
+#include "third_party/blink/renderer/core/clipboard/paste_mode.h"
 #include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
 #include "third_party/blink/renderer/core/dom/dom_exception.h"
+#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
+#include "third_party/blink/renderer/core/editing/commands/clipboard_commands.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.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/html/html_element.h"
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc b/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
index 395fd61..8c2ba41 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
@@ -4,8 +4,10 @@
 
 #include "third_party/blink/renderer/modules/clipboard/clipboard.h"
 
+#include "base/functional/bind.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/run_until.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/mojom/clipboard/clipboard.mojom-blink.h"
 #include "third_party/blink/public/mojom/permissions/permission.mojom-blink.h"
@@ -13,12 +15,18 @@
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_blob.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_clipboard_read_options.h"
+#include "third_party/blink/renderer/core/clipboard/paste_mode.h"
 #include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
 #include "third_party/blink/renderer/core/dom/dom_exception.h"
+#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
+#include "third_party/blink/renderer/core/editing/commands/clipboard_commands.h"
+#include "third_party/blink/renderer/core/editing/editor.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.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/html/html_element.h"
 #include "third_party/blink/renderer/core/page/focus_controller.h"
 #include "third_party/blink/renderer/core/testing/page_test_base.h"
 #include "third_party/blink/renderer/modules/clipboard/clipboard_item.h"
@@ -880,4 +888,168 @@
       mojom::blink::PermissionService::Name_, {});
 }
 
+class ClipboardPasteTestListener final : public NativeEventListener {
+ public:
+  explicit ClipboardPasteTestListener(base::OnceCallback<void(Event*)> callback)
+      : callback_(std::move(callback)) {}
+
+  void Invoke(ExecutionContext*, Event* event) override {
+    std::move(callback_).Run(event);
+  }
+
+  bool Matches(const EventListener& other) const override {
+    return this == &other;
+  }
+
+ private:
+  base::OnceCallback<void(Event*)> callback_;
+};
+
+TEST_F(ClipboardTest, PasteEventUninterruptedReadText) {
+  V8TestingScope scope;
+  ExecutionContext* executionContext = GetFrame().DomWindow();
+  String initial_string = "InitialStringForClipboardTesting";
+  WritePlainTextToClipboard(initial_string);
+  GetFrame().GetSystemClipboard()->CommitWrite();
+
+  SetSecureOrigin(executionContext);
+  SetPageFocus(true);
+
+  bool listener_called = false;
+  auto* listener =
+      MakeGarbageCollected<ClipboardPasteTestListener>(base::BindOnce(
+          [](ExecutionContext* executionContext, ScriptState* script_state,
+             bool* listener_called, Event* event) {
+            *listener_called = true;
+            DummyExceptionStateForTesting exception_state;
+            ScriptPromise<IDLString> promise =
+                ClipboardPromise::CreateForReadText(
+                    executionContext, script_state, exception_state);
+            ScriptPromiseTester promise_tester(script_state, promise);
+            promise_tester.WaitUntilSettled();
+            EXPECT_TRUE(promise_tester.IsFulfilled());
+            String promise_returned_string;
+            promise_tester.Value().ToString(promise_returned_string);
+            EXPECT_EQ(promise_returned_string,
+                      "InitialStringForClipboardTesting");
+          },
+          WrapPersistent(executionContext),
+          WrapPersistent(scope.GetScriptState()),
+          Unretained(&listener_called)));
+
+  GetFrame().GetDocument()->body()->addEventListener(event_type_names::kPaste,
+                                                     listener);
+
+  ClipboardCommands::DispatchPasteEvent(GetFrame(), PasteMode::kAllMimeTypes,
+                                        EditorCommandSource::kMenuOrKeyBinding);
+
+  EXPECT_TRUE(listener_called);
+  GetFrame().GetDocument()->body()->removeEventListener(
+      event_type_names::kPaste, listener, /*use_capture=*/false);
+}
+
+TEST_F(ClipboardTest, PasteEventInterruptedReadTextRejected) {
+  V8TestingScope scope;
+  ExecutionContext* executionContext = GetFrame().DomWindow();
+  String initial_string = "InitialStringForClipboardTesting";
+  WritePlainTextToClipboard(initial_string);
+  GetFrame().GetSystemClipboard()->CommitWrite();
+
+  SetSecureOrigin(executionContext);
+  SetPageFocus(true);
+
+  bool listener_called = false;
+  auto* listener =
+      MakeGarbageCollected<ClipboardPasteTestListener>(base::BindOnce(
+          [](ExecutionContext* executionContext, ScriptState* script_state,
+             SystemClipboard* system_clipboard, bool* listener_called,
+             Event* event) {
+            *listener_called = true;
+            absl::uint128 initial_sequence = system_clipboard->SequenceNumber();
+            system_clipboard->WritePlainText("SecretExploitString");
+            system_clipboard->CommitWrite();
+            EXPECT_TRUE(base::test::RunUntil([&]() {
+              return system_clipboard->SequenceNumber() != initial_sequence;
+            }));
+
+            DummyExceptionStateForTesting exception_state;
+            ScriptPromise<IDLString> promise =
+                ClipboardPromise::CreateForReadText(
+                    executionContext, script_state, exception_state);
+            ScriptPromiseTester promise_tester(script_state, promise);
+            promise_tester.WaitUntilSettled();
+            EXPECT_TRUE(promise_tester.IsRejected());
+
+            EXPECT_EQ(promise_tester.ValueAsString(),
+                      "DataError: Clipboard contents changed since paste "
+                      "event started.");
+          },
+          WrapPersistent(executionContext),
+          WrapPersistent(scope.GetScriptState()),
+          WrapPersistent(GetFrame().GetSystemClipboard()),
+          Unretained(&listener_called)));
+
+  GetFrame().GetDocument()->body()->addEventListener(event_type_names::kPaste,
+                                                     listener);
+
+  ClipboardCommands::DispatchPasteEvent(GetFrame(), PasteMode::kAllMimeTypes,
+                                        EditorCommandSource::kMenuOrKeyBinding);
+
+  EXPECT_TRUE(listener_called);
+  GetFrame().GetDocument()->body()->removeEventListener(
+      event_type_names::kPaste, listener, /*use_capture=*/false);
+}
+
+TEST_F(ClipboardTest, PasteEventInterruptedReadRejected) {
+  V8TestingScope scope;
+  ExecutionContext* executionContext = GetFrame().DomWindow();
+  String initial_string = "InitialStringForClipboardTesting";
+  WritePlainTextToClipboard(initial_string);
+  GetFrame().GetSystemClipboard()->CommitWrite();
+
+  SetSecureOrigin(executionContext);
+  SetPageFocus(true);
+
+  bool listener_called = false;
+  auto* listener =
+      MakeGarbageCollected<ClipboardPasteTestListener>(base::BindOnce(
+          [](ExecutionContext* executionContext, ScriptState* script_state,
+             SystemClipboard* system_clipboard, bool* listener_called,
+             Event* event) {
+            *listener_called = true;
+            absl::uint128 initial_sequence = system_clipboard->SequenceNumber();
+            system_clipboard->WritePlainText("SecretExploitString");
+            system_clipboard->CommitWrite();
+            EXPECT_TRUE(base::test::RunUntil([&]() {
+              return system_clipboard->SequenceNumber() != initial_sequence;
+            }));
+
+            DummyExceptionStateForTesting exception_state;
+            ScriptPromise<IDLSequence<ClipboardItem>> promise =
+                ClipboardPromise::CreateForRead(executionContext, script_state,
+                                                nullptr, exception_state);
+            ScriptPromiseTester promise_tester(script_state, promise);
+            promise_tester.WaitUntilSettled();
+            EXPECT_TRUE(promise_tester.IsRejected());
+
+            EXPECT_EQ(promise_tester.ValueAsString(),
+                      "DataError: Clipboard contents changed since paste "
+                      "event started.");
+          },
+          WrapPersistent(executionContext),
+          WrapPersistent(scope.GetScriptState()),
+          WrapPersistent(GetFrame().GetSystemClipboard()),
+          Unretained(&listener_called)));
+
+  GetFrame().GetDocument()->body()->addEventListener(event_type_names::kPaste,
+                                                     listener);
+
+  ClipboardCommands::DispatchPasteEvent(GetFrame(), PasteMode::kAllMimeTypes,
+                                        EditorCommandSource::kMenuOrKeyBinding);
+
+  EXPECT_TRUE(listener_called);
+  GetFrame().GetDocument()->body()->removeEventListener(
+      event_type_names::kPaste, listener, /*use_capture=*/false);
+}
+
 }  // namespace blink
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.