Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in DataTransfer
DescriptionInappropriate implementation in DataTransfer
ComponentDataTransfer
Bug ClassLogic Error
Tracker498411773
Fix commit320aa27dd978 (chromium/src) +126/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/testing/mock_clipboard_host.cc
modified

Files Changed

  • third_party/blink/renderer/core/testing/mock_clipboard_host.cc
  • third_party/blink/renderer/core/testing/mock_clipboard_host.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 320aa27dd978b64fcd9aed762f3e2a44a020403a Mon Sep 17 00:00:00 2001
From: Rohan Raja <roraja@microsoft.com>
Date: Thu, 28 May 2026 22:47:41 -0700
Subject: [PATCH] Fix TOCTOU race in ClipboardItem lazy-read

The clipboard lazy-read feature
(`ReadClipboardDataOnClipboardItemGetType`) captured the OS clipboard
sequence number via a separate sync IPC after the async
`ReadAvailableCustomAndStandardFormats` callback returned. If the OS
clipboard changed during that async gap, the sequence number reflected
the new clipboard state while the format names reflected the original
state. This defeated the `HasClipboardChangedSinceClipboardRead()`
change-detection defense in `ClipboardItem::getType()` and allowed a
page with clipboard-read permission to read newly copied data that was
never intended for it.

Capture the sequence number in `HandleReadWithPermission()` before
dispatching the format-enumeration IPC, cache it on the
`ClipboardPromise`, and pass that cached value into the lazy
`ClipboardItem` constructor in `ResolveRead()`. Any OS clipboard
mutation during or after enumeration now leaves the cached baseline
stale relative to the live sequence number, so `getType()` correctly
rejects with `DataError` (fail-closed). Capture is gated by the same
feature flag as its consumer, so the eager-read path is unchanged and
incurs no extra Mojo round-trip.

Bug: 498411773
Change-Id: I1e949fc889f6f884da91afd6cb8a99ce88bdd61e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7854519
Reviewed-by: Dan Clark <daniec@microsoft.com>
Reviewed-by: Shweta Bindal <shwetabindal@microsoft.com>
Commit-Queue: Rohan Raja <roraja@microsoft.com>
Cr-Commit-Position: refs/heads/main@{#1638241}
---

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 2d9f96be..90b0b16 100644
--- a/third_party/blink/renderer/core/testing/mock_clipboard_host.cc
+++ b/third_party/blink/renderer/core/testing/mock_clipboard_host.cc
@@ -230,6 +230,11 @@
   Vector<String> format_names = ReadStandardFormatNames();
   for (const auto& item : unsanitized_custom_data_map_)
     format_names.emplace_back(item.key);
+  // TOCTOU race hook: lets tests mutate clipboard state inside the async
+  // gap. See crbug.com/498411773.
+  if (read_available_formats_hook_for_testing_) {
+    read_available_formats_hook_for_testing_.Run(this);
+  }
   std::move(callback).Run(std::move(format_names));
 }
 
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 2b62d13..6de0cffd 100644
--- a/third_party/blink/renderer/core/testing/mock_clipboard_host.h
+++ b/third_party/blink/renderer/core/testing/mock_clipboard_host.h
@@ -5,6 +5,8 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_TESTING_MOCK_CLIPBOARD_HOST_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_TESTING_MOCK_CLIPBOARD_HOST_H_
 
+#include "base/functional/callback.h"
+#include "base/gtest_prod_util.h"
 #include "build/build_config.h"
 #include "mojo/public/cpp/bindings/receiver_set.h"
 #include "mojo/public/cpp/bindings/remote.h"
@@ -52,6 +54,14 @@
   }
 #endif
 
+  // Installs a hook fired during ReadAvailableCustomAndStandardFormats(),
+  // after format enumeration but before the Mojo reply, so tests can simulate
+  // a clipboard change inside the async race window. See crbug.com/498411773.
+  void SetReadAvailableFormatsHookForTesting(
+      base::RepeatingCallback<void(MockClipboardHost*)> hook) {
+    read_available_formats_hook_for_testing_ = std::move(hook);
+  }
+
   // Method call tracking for testing
   int ReadTextCallCount() const { return read_text_call_count_; }
   int ReadHtmlCallCount() const { return read_html_call_count_; }
@@ -74,6 +84,9 @@
   void RunDeferredReadTextCallback();
 
  private:
+  FRIEND_TEST_ALL_PREFIXES(ClipboardTest,
+                           ClipboardChangeDuringReadRejectsGetType);
+
   // mojom::ClipboardHost
   void GetSequenceNumber(mojom::ClipboardBuffer clipboard_buffer,
                          GetSequenceNumberCallback callback) override;
@@ -99,6 +112,7 @@
       const String& type,
       ReadDataTransferCustomDataCallback callback) override;
   void WriteText(const String& text) override;
+  void CommitWrite() override;
   void WriteHtml(const String& markup, const KURL& url) override;
   void WriteSvg(const String& markup) override;
   void WriteSmartPasteMarker() override;
@@ -106,7 +120,6 @@
       const HashMap<String, String>& data) override;
   void WriteBookmark(const String& url, const String& title) override;
   void WriteImage(const SkBitmap& bitmap) override;
-  void CommitWrite() override;
   void ReadAvailableCustomAndStandardFormats(
       ReadAvailableCustomAndStandardFormatsCallback callback) override;
   void ReadUnsanitizedCustomFormat(
@@ -150,6 +163,11 @@
   int read_html_call_count_ = 0;
   int read_available_formats_call_count_ = 0;
 
+  // Hook fired during ReadAvailableCustomAndStandardFormats() for TOCTOU
+  // race regression tests.
+  base::RepeatingCallback<void(MockClipboardHost*)>
+      read_available_formats_hook_for_testing_;
+
   // Deferred-callback machinery for the truly-async-read regression test.
   bool defer_read_text_callback_ = false;
   ReadTextCallback deferred_read_text_callback_;
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc b/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
index 9e35587..2c0c5d87 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
@@ -308,6 +308,14 @@
     return;
   }
 
+  // Snapshot the sequence number before format enumeration so a clipboard
+  // change during the async IPC will be detected by getType() (fail-closed).
+  // See crbug.com/498411773.
+  if (RuntimeEnabledFeatures::
+          ReadClipboardDataOnClipboardItemGetTypeEnabled()) {
+    sequence_number_at_read_start_ = GetSystemClipboard()->SequenceNumber();
+  }
+
 #if BUILDFLAG(IS_MAC)
   // Check macOS platform permission state if the runtime flag is enabled
   if (RuntimeEnabledFeatures::MacSystemClipboardPermissionCheckEnabled()) {
@@ -336,8 +344,7 @@
   if (RuntimeEnabledFeatures::
           ReadClipboardDataOnClipboardItemGetTypeEnabled()) {
     clipboard_items = {MakeGarbageCollected<ClipboardItem>(
-        item_mime_types_, GetSystemClipboard()->SequenceNumber(),
-        GetExecutionContext(),
+        item_mime_types_, sequence_number_at_read_start_, GetExecutionContext(),
         /*sanitize_html_for_lazy_read=*/!will_read_unprocessed_html_,
         ClipboardItem::AccessMode::kLazy)};
   } else {
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_promise.h b/third_party/blink/renderer/modules/clipboard/clipboard_promise.h
index 820cba4d..85fee98 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_promise.h
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_promise.h
@@ -221,6 +221,10 @@
   // Uses uint64_t to match Blob::size() return type and avoid truncation on
   // 32-bit platforms.
   uint64_t total_eager_read_blob_size_ = 0;
+  // Sequence number snapshotted before format enumeration so the lazy-read
+  // path can detect a clipboard change during the async IPC.
+  // See crbug.com/498411773.
+  std::optional<absl::uint128> sequence_number_at_read_start_;
   SEQUENCE_CHECKER(sequence_checker_);
 };
 
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc b/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
index 02c3e47..e6795bc2 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
@@ -15,6 +15,7 @@
 #include "third_party/blink/renderer/bindings/core/v8/v8_blob.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_clipboard_read_options.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/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"
@@ -67,16 +68,35 @@
   ScriptPromise<Blob> React(ScriptState* script_state,
                             HeapVector<Member<ClipboardItem>> clipboard_items) {
     if (clipboard_items.empty()) {
-      return ScriptPromise<Blob>();
+      return ScriptPromise<Blob>::RejectWithDOMException(
+          script_state,
+          MakeGarbageCollected<DOMException>(DOMExceptionCode::kNotFoundError,
+                                             "No clipboard items"));
     }
 
     auto& clipboard_item = clipboard_items[0];
 
-    ExceptionState exception_state(script_state->GetIsolate());
+    // Use DummyExceptionStateForTesting to avoid the ExceptionState destructor
+    // DCHECK that requires a pending V8 exception. We handle the exception
+    // ourselves by converting to a rejected promise.
+    DummyExceptionStateForTesting exception_state;
 
     // Call getType to trigger the underlying clipboard read
-    return clipboard_item->getType(script_state, expected_type_,
-                                   exception_state);
+    ScriptPromise<Blob> result =
+        clipboard_item->getType(script_state, expected_type_, exception_state);
+
+    // If getType() threw (e.g., DataError due to clipboard change detection),
+    // it returns an empty ScriptPromise. We must return a properly rejected
Loading diff…

Regression Test / PoC

shipped with the fix
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 2d9f96be..90b0b16 100644
--- a/third_party/blink/renderer/core/testing/mock_clipboard_host.cc
+++ b/third_party/blink/renderer/core/testing/mock_clipboard_host.cc
@@ -230,6 +230,11 @@
   Vector<String> format_names = ReadStandardFormatNames();
   for (const auto& item : unsanitized_custom_data_map_)
     format_names.emplace_back(item.key);
+  // TOCTOU race hook: lets tests mutate clipboard state inside the async
+  // gap. See crbug.com/498411773.
+  if (read_available_formats_hook_for_testing_) {
+    read_available_formats_hook_for_testing_.Run(this);
+  }
   std::move(callback).Run(std::move(format_names));
 }
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 2b62d13..6de0cffd 100644
--- a/third_party/blink/renderer/core/testing/mock_clipboard_host.h
+++ b/third_party/blink/renderer/core/testing/mock_clipboard_host.h
@@ -5,6 +5,8 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_TESTING_MOCK_CLIPBOARD_HOST_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_TESTING_MOCK_CLIPBOARD_HOST_H_
 
+#include "base/functional/callback.h"
+#include "base/gtest_prod_util.h"
 #include "build/build_config.h"
 #include "mojo/public/cpp/bindings/receiver_set.h"
 #include "mojo/public/cpp/bindings/remote.h"
@@ -52,6 +54,14 @@
   }
 #endif
 
+  // Installs a hook fired during ReadAvailableCustomAndStandardFormats(),
+  // after format enumeration but before the Mojo reply, so tests can simulate
+  // a clipboard change inside the async race window. See crbug.com/498411773.
+  void SetReadAvailableFormatsHookForTesting(
+      base::RepeatingCallback<void(MockClipboardHost*)> hook) {
+    read_available_formats_hook_for_testing_ = std::move(hook);
+  }
+
   // Method call tracking for testing
   int ReadTextCallCount() const { return read_text_call_count_; }
   int ReadHtmlCallCount() const { return read_html_call_count_; }
@@ -74,6 +84,9 @@
   void RunDeferredReadTextCallback();
 
  private:
+  FRIEND_TEST_ALL_PREFIXES(ClipboardTest,
+                           ClipboardChangeDuringReadRejectsGetType);
+
   // mojom::ClipboardHost
   void GetSequenceNumber(mojom::ClipboardBuffer clipboard_buffer,
                          GetSequenceNumberCallback callback) override;
@@ -99,6 +112,7 @@
       const String& type,
       ReadDataTransferCustomDataCallback callback) override;
   void WriteText(const String& text) override;
+  void CommitWrite() override;
   void WriteHtml(const String& markup, const KURL& url) override;
   void WriteSvg(const String& markup) override;
   void WriteSmartPasteMarker() override;
@@ -106,7 +120,6 @@
       const HashMap<String, String>& data) override;
   void WriteBookmark(const String& url, const String& title) override;
   void WriteImage(const SkBitmap& bitmap) override;
-  void CommitWrite() override;
   void ReadAvailableCustomAndStandardFormats(
       ReadAvailableCustomAndStandardFormatsCallback callback) override;
   void ReadUnsanitizedCustomFormat(
@@ -150,6 +163,11 @@
   int read_html_call_count_ = 0;
   int read_available_formats_call_count_ = 0;
 
+  // Hook fired during ReadAvailableCustomAndStandardFormats() for TOCTOU
+  // race regression tests.
+  base::RepeatingCallback<void(MockClipboardHost*)>
+      read_available_formats_hook_for_testing_;
+
   // Deferred-callback machinery for the truly-async-read regression test.
   bool defer_read_text_callback_ = false;
   ReadTextCallback deferred_read_text_callback_;
diff --git a/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc b/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
index 02c3e47..e6795bc2 100644
--- a/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
+++ b/third_party/blink/renderer/modules/clipboard/clipboard_unittest.cc
@@ -15,6 +15,7 @@
 #include "third_party/blink/renderer/bindings/core/v8/v8_blob.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_clipboard_read_options.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/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"
@@ -67,16 +68,35 @@
   ScriptPromise<Blob> React(ScriptState* script_state,
                             HeapVector<Member<ClipboardItem>> clipboard_items) {
     if (clipboard_items.empty()) {
-      return ScriptPromise<Blob>();
+      return ScriptPromise<Blob>::RejectWithDOMException(
+          script_state,
+          MakeGarbageCollected<DOMException>(DOMExceptionCode::kNotFoundError,
+                                             "No clipboard items"));
     }
 
     auto& clipboard_item = clipboard_items[0];
 
-    ExceptionState exception_state(script_state->GetIsolate());
+    // Use DummyExceptionStateForTesting to avoid the ExceptionState destructor
+    // DCHECK that requires a pending V8 exception. We handle the exception
+    // ourselves by converting to a rejected promise.
+    DummyExceptionStateForTesting exception_state;
 
     // Call getType to trigger the underlying clipboard read
-    return clipboard_item->getType(script_state, expected_type_,
-                                   exception_state);
+    ScriptPromise<Blob> result =
+        clipboard_item->getType(script_state, expected_type_, exception_state);
+
+    // If getType() threw (e.g., DataError due to clipboard change detection),
+    // it returns an empty ScriptPromise. We must return a properly rejected
+    // promise instead, because ThenCallable's ToV8Traits will DCHECK on an
+    // empty promise.
+    if (exception_state.HadException()) {
+      return ScriptPromise<Blob>::RejectWithDOMException(
+          script_state, MakeGarbageCollected<DOMException>(
+                            exception_state.CodeAs<DOMExceptionCode>(),
+                            exception_state.Message()));
+    }
+
+    return result;
   }
 
  private:
@@ -795,4 +815,69 @@
       mojom::blink::PermissionService::Name_, {});
 }
 
+// Verifies TOCTOU protection: clipboard change during format enumeration
+// causes getType() to reject with DataError.
+TEST_F(ClipboardTest, ClipboardChangeDuringReadRejectsGetType) {
+  V8TestingScope scope;
+  ExecutionContext* executionContext = GetFrame().DomWindow();
+
+  mock_clipboard_host()->WriteText("OriginalText");
+  mock_clipboard_host()->CommitWrite();
+
+  // Simulate clipboard change during ReadAvailableFormats.
+  mock_clipboard_host()->SetReadAvailableFormatsHookForTesting(
+      base::BindRepeating([](MockClipboardHost* host) {
+        host->WriteText("OverwrittenText");
+        host->CommitWrite();
+      }));
+
+  EXPECT_CALL(permission_service_, RequestPermission)
+      .WillOnce(WithArg<1>(
+          [](mojom::blink::PermissionService::RequestPermissionCallback
+                 callback) {
+            std::move(callback).Run(
+                mojom::blink::PermissionStatusWithDetails::New(
+                    mojom::blink::PermissionStatus::GRANTED, nullptr));
+          }));
+  BindMockPermissionService(executionContext);
+  SetSecureOrigin(executionContext);
+  SetPageFocus(true);
+
+  ScriptPromise<IDLSequence<ClipboardItem>> promise =
+      ClipboardPromise::CreateForRead(executionContext, scope.GetScriptState(),
+                                      nullptr, scope.GetExceptionState());
+
+  ScriptPromiseTester read_tester(scope.GetScriptState(), promise);
+  read_tester.WaitUntilSettled();
+  EXPECT_TRUE(read_tester.IsFulfilled());
+
+  mock_clipboard_host()->SetReadAvailableFormatsHookForTesting({});
+
+  // getType() should detect the clipboard change and reject.
+  auto* get_type_helper =
+      MakeGarbageCollected<ClipboardItemGetType>("text/plain");
+  auto chained_promise = promise.Then(scope.GetScriptState(), get_type_helper);
+
+  ScriptPromiseTester promise_tester(scope.GetScriptState(), chained_promise);
+  promise_tester.WaitUntilSettled();
+
+  if (promise_tester.IsFulfilled()) {
+    ScriptValue value = promise_tester.Value();
+    v8::Local<v8::Value> v8_value = value.V8Value();
+    if (v8_value->IsPromise()) {
+      ScriptPromise<Blob> inner_promise =
+          ScriptPromise<Blob>::FromV8Value(scope.GetScriptState(), v8_value);
+      ScriptPromiseTester inner_tester(scope.GetScriptState(), inner_promise);
+      inner_tester.WaitUntilSettled();
+      EXPECT_TRUE(inner_tester.IsRejected())
+          << "getType() should reject when clipboard changed during read";
+    } else {
+      ADD_FAILURE() << "Expected rejection but got a non-promise value";
+    }
+  }
+
+  executionContext->GetBrowserInterfaceBroker().SetBinderForTesting(
+      mojom::blink::PermissionService::Name_, {});
+}
+
 }  // namespace blink
Loading diff…

Original Bug Report

reported by vm...@google.com

TOCTOU in ClipboardItem allows bypass of clipboard change detection

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 security team.

Overview: A race condition in the experimental clipboard lazy-reading feature (#defer-os-clipboard-read-call-to-get-type) potentially allows a website to read clipboard data copied by a user after the initial read() authorization. This bypasses the change-detection defense intended to prevent websites from accessing newly copied sensitive data. The vulnerability stems from capturing the clipboard sequence number synchronously after an asynchronous format enumeration step.

Affected files:

  • third_party/blink/renderer/modules/clipboard/clipboard_promise.cc
  • third_party/blink/renderer/modules/clipboard/clipboard_item.cc
  • third_party/blink/renderer/core/clipboard/system_clipboard.cc
  • content/browser/renderer_host/clipboard_host_impl.cc

Estimated timestamp from git blame: 2026-03-09

Description

A Time-of-Check Time-of-Use (TOCTOU) vulnerability potentially exists in the ReadClipboardDataOnClipboardItemGetType feature (controlled by the #defer-os-clipboard-read-call-to-get-type flag) of the Clipboard API. This feature introduces a lazy-reading mechanism where navigator.clipboard.read() returns a list of ClipboardItem objects, but the actual data for each format is only fetched when getType() is called.

To prevent a malicious site from reading sensitive data (like a password) that a user copies to the clipboard after authorizing a read operation, a change-detection defense was implemented. This defense snapshots the clipboard’s sequence number when the ClipboardItem is created and verifies it hasn’t changed before performing the actual read in getType().

However, the sequence number is captured at the wrong point in the read() flow:

  1. When navigator.clipboard.read() is called, the renderer initiates an asynchronous request to enumerate available formats via GetSystemClipboard()->ReadAvailableCustomAndStandardFormats().
  2. The browser-side implementation (ClipboardHostImpl) reads the OS clipboard format names and returns them via IPC. No sequence number is snapshotted yet.
  3. Upon receiving the format names, the renderer task runs ClipboardPromise::ResolveRead(), which then makes a synchronous Mojo call to GetSystemClipboard()->SequenceNumber() to establish the baseline for the ClipboardItem.
  4. Due to the asynchronous nature of steps 1 and 2, there is a race window between the browser reading the formats and the renderer requesting the sequence number. If the user copies new data to the clipboard during this window (e.g., when switching focus), the ClipboardItem will be initialized with the old format names (from state N) but the new sequence number (from state M).

When item.getType() is subsequently called, ClipboardItem::HasClipboardChangedSinceClipboardRead() compares the current system sequence number (M) with the stored sequence number (M). The check passes, and the browser proceeds to read the current clipboard content, which is state M. This defeats the change-detection defense and allows the site to read data the user never intended to share with that specific read() operation.

Potential Attacker Steps

While we don’t have a working Proof of Concept yet, an attacker could potentially exploit this by deliberately widening the race window:

  1. A malicious page obtains clipboard-read permission.
  2. The page initiates navigator.clipboard.read() while focused.
  3. The attacker’s JavaScript immediately blocks the renderer’s main thread (e.g., using a long-running synchronous while loop) to delay the processing of the asynchronous format enumeration callback.
  4. During this delay, the user switches focus to another application and copies sensitive data (e.g., a password).
  5. The renderer unblocks and processes the callback. The resulting ClipboardItem is constructed with the new sequence number but the old formats.
  6. The page calls getType() on the returned ClipboardItem. The change-detection check passes, and the newly copied sensitive data is successfully read.

Impact

A page with clipboard-read permission can read data copied to the clipboard by the user even after they have switched focus away from the page, provided the race is won. This allows for the theft of sensitive information copied from other applications.

Suggested Fix

The sequence number should be captured before or simultaneously with the format enumeration. One approach is to modify the browser-side implementation (ClipboardHostImpl::ReadAvailableCustomAndStandardFormats) to return both the format names and the sequence number in the same IPC response. This ensures the snapshot reflects the state of the clipboard at the exact moment the formats were read.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


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.

View on issue tracker