Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in DataTransfer
DescriptionMissing authorization in DataTransfer
ComponentDataTransfer
Sub-componentBlink>DataTransfer
Bug ClassAuthorization Error
Tracker550512266
Fix commita101cab547f8 (chromium/src) +90/-2
CISA KEVNot listed
Creditedleolee
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
ClipboardHostImplWriteTest
content/browser/renderer_host/clipboard_host_impl_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/clipboard_host_impl.cc
  • third_party/blink/web_tests/external/wpt/clipboard-apis/async-clipboard-cached-getType-permission-revocation.tentative.https.html
  • content/browser/renderer_host/clipboard_host_impl_unittest.cc
From a101cab547f85919d7503a9b17cf6a37f4018a2d Mon Sep 17 00:00:00 2001
From: Leo Lee <[email protected]>
Date: Wed, 09 Sep 2026 19:03:03 -0700
Subject: [PATCH] Require paste authorization for clipboard format metadata

Renderer-accessible ClipboardHost methods ReadAvailableTypes() and
IsFormatAvailable() exposed OS clipboard format metadata without
browser-side paste authorization.

Gate these methods with IsRendererPasteAllowed() and return neutral
values when access is denied. Keep GetSequenceNumber() available to
active frames because Blink uses its storage-key-partitioned sequence to
validate cached ClipboardItem representations after authorization
changes. Add unit and web-platform regression coverage.

Bug: 550512266
Change-Id: If701e398bdbd1fd9f9fd5d5ba94e1cd81c84eb4a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8283983
Reviewed-by: Dan Clark <[email protected]>
Commit-Queue: Leo Lee <[email protected]>
Reviewed-by: Ana Sollano Kim <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1695102}
---
diff --git a/content/browser/renderer_host/clipboard_host_impl.cc b/content/browser/renderer_host/clipboard_host_impl.cc
index 2878dea..e6290a6d 100644
--- a/content/browser/renderer_host/clipboard_host_impl.cc
+++ b/content/browser/renderer_host/clipboard_host_impl.cc
@@ -125,7 +125,7 @@
 void ClipboardHostImpl::ReadAvailableTypes(
     ui::ClipboardBuffer clipboard_buffer,
     ReadAvailableTypesCallback callback) {
-  if (!IsContextActive()) {
+  if (!IsPasteAllowed(clipboard_buffer)) {
     std::move(callback).Run({});
     return;
   }
@@ -196,7 +196,7 @@
 void ClipboardHostImpl::IsFormatAvailable(blink::mojom::ClipboardFormat format,
                                           ui::ClipboardBuffer clipboard_buffer,
                                           IsFormatAvailableCallback callback) {
-  if (!IsContextActive()) {
+  if (!IsPasteAllowed(clipboard_buffer)) {
     std::move(callback).Run(false);
     return;
   }
diff --git a/content/browser/renderer_host/clipboard_host_impl_unittest.cc b/content/browser/renderer_host/clipboard_host_impl_unittest.cc
index b8596cd..d2d82b18 100644
--- a/content/browser/renderer_host/clipboard_host_impl_unittest.cc
+++ b/content/browser/renderer_host/clipboard_host_impl_unittest.cc
@@ -279,6 +279,52 @@
   EXPECT_NE(id3, id4);
 }
 
+TEST_F(ClipboardHostImplTest,
+       ClipboardSequenceNumberDoesNotRequirePasteAuthorization) {
+  ClipboardPasteAllowedBrowserClient browser_client;
+  ScopedContentBrowserClientSetting browser_client_setting(&browser_client);
+
+  browser_client.set_is_clipboard_paste_allowed(false);
+
+  absl::uint128 sequence_number;
+  mojo_clipboard()->GetSequenceNumber(ui::ClipboardBuffer::kCopyPaste,
+                                      &sequence_number);
+  EXPECT_NE(absl::uint128(), sequence_number);
+}
+
+TEST_F(ClipboardHostImplTest,
+       ClipboardFormatMetadataRequiresPasteAuthorization) {
+  ClipboardPasteAllowedBrowserClient browser_client;
+  ScopedContentBrowserClientSetting browser_client_setting(&browser_client);
+
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+    writer.WriteText(u"clipboard-text");
+  }
+
+  browser_client.set_is_clipboard_paste_allowed(false);
+
+  std::vector<std::u16string> types = {u"non-empty"};
+  mojo_clipboard()->ReadAvailableTypes(ui::ClipboardBuffer::kCopyPaste, &types);
+  EXPECT_TRUE(types.empty());
+
+  bool format_available = true;
+  mojo_clipboard()->IsFormatAvailable(blink::mojom::ClipboardFormat::kPlaintext,
+                                      ui::ClipboardBuffer::kCopyPaste,
+                                      &format_available);
+  EXPECT_FALSE(format_available);
+
+  browser_client.set_is_clipboard_paste_allowed(true);
+
+  mojo_clipboard()->ReadAvailableTypes(ui::ClipboardBuffer::kCopyPaste, &types);
+  EXPECT_TRUE(std::ranges::contains(types, u"text/plain"));
+
+  mojo_clipboard()->IsFormatAvailable(blink::mojom::ClipboardFormat::kPlaintext,
+                                      ui::ClipboardBuffer::kCopyPaste,
+                                      &format_available);
+  EXPECT_TRUE(format_available);
+}
+
 class ClipboardHostImplWriteTest : public RenderViewHostTestHarness {
  protected:
   ClipboardHostImplWriteTest()
diff --git a/third_party/blink/web_tests/external/wpt/clipboard-apis/async-clipboard-cached-getType-permission-revocation.tentative.https.html b/third_party/blink/web_tests/external/wpt/clipboard-apis/async-clipboard-cached-getType-permission-revocation.tentative.https.html
new file mode 100644
index 0000000..c2f33a8
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/clipboard-apis/async-clipboard-cached-getType-permission-revocation.tentative.https.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>
+  Cached ClipboardItem data survives clipboard-read permission revocation
+</title>
+<link rel="help" href="https://w3c.github.io/clipboard-apis/#dom-clipboarditem-gettype">
+<body>Body needed for clipboard access</body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<script>
+promise_test(async t => {
+  const readPermission = {name: 'clipboard-read'};
+  const writePermission = {name: 'clipboard-write'};
+
+  await test_driver.set_permission(readPermission, 'granted');
+  await test_driver.set_permission(writePermission, 'granted');
+
+  t.add_cleanup(async () => {
+    await test_driver.set_permission(readPermission, 'prompt');
+    await test_driver.set_permission(writePermission, 'prompt');
+  });
+
+  await navigator.clipboard.writeText('cached clipboard data');
+
+  const [item] = await navigator.clipboard.read();
+
+  // Materialize and cache the representation while access is authorized.
+  const firstBlob = await item.getType('text/plain');
+  assert_equals(await firstBlob.text(), 'cached clipboard data');
+
+  // Authorization changes, but neither the clipboard nor the cached
+  // representation changes.
+  await test_driver.set_permission(readPermission, 'denied');
+
+  const secondBlob = await item.getType('text/plain');
+  assert_equals(await secondBlob.text(), 'cached clipboard data');
+}, 'Revoking clipboard-read does not invalidate an already-resolved ' +
+   'ClipboardItem representation');
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/clipboard_host_impl_unittest.cc b/content/browser/renderer_host/clipboard_host_impl_unittest.cc
index b8596cd..d2d82b18 100644
--- a/content/browser/renderer_host/clipboard_host_impl_unittest.cc
+++ b/content/browser/renderer_host/clipboard_host_impl_unittest.cc
@@ -279,6 +279,52 @@
   EXPECT_NE(id3, id4);
 }
 
+TEST_F(ClipboardHostImplTest,
+       ClipboardSequenceNumberDoesNotRequirePasteAuthorization) {
+  ClipboardPasteAllowedBrowserClient browser_client;
+  ScopedContentBrowserClientSetting browser_client_setting(&browser_client);
+
+  browser_client.set_is_clipboard_paste_allowed(false);
+
+  absl::uint128 sequence_number;
+  mojo_clipboard()->GetSequenceNumber(ui::ClipboardBuffer::kCopyPaste,
+                                      &sequence_number);
+  EXPECT_NE(absl::uint128(), sequence_number);
+}
+
+TEST_F(ClipboardHostImplTest,
+       ClipboardFormatMetadataRequiresPasteAuthorization) {
+  ClipboardPasteAllowedBrowserClient browser_client;
+  ScopedContentBrowserClientSetting browser_client_setting(&browser_client);
+
+  {
+    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+    writer.WriteText(u"clipboard-text");
+  }
+
+  browser_client.set_is_clipboard_paste_allowed(false);
+
+  std::vector<std::u16string> types = {u"non-empty"};
+  mojo_clipboard()->ReadAvailableTypes(ui::ClipboardBuffer::kCopyPaste, &types);
+  EXPECT_TRUE(types.empty());
+
+  bool format_available = true;
+  mojo_clipboard()->IsFormatAvailable(blink::mojom::ClipboardFormat::kPlaintext,
+                                      ui::ClipboardBuffer::kCopyPaste,
+                                      &format_available);
+  EXPECT_FALSE(format_available);
+
+  browser_client.set_is_clipboard_paste_allowed(true);
+
+  mojo_clipboard()->ReadAvailableTypes(ui::ClipboardBuffer::kCopyPaste, &types);
+  EXPECT_TRUE(std::ranges::contains(types, u"text/plain"));
+
+  mojo_clipboard()->IsFormatAvailable(blink::mojom::ClipboardFormat::kPlaintext,
+                                      ui::ClipboardBuffer::kCopyPaste,
+                                      &format_available);
+  EXPECT_TRUE(format_available);
+}
+
 class ClipboardHostImplWriteTest : public RenderViewHostTestHarness {
  protected:
   ClipboardHostImplWriteTest()
diff --git a/third_party/blink/web_tests/external/wpt/clipboard-apis/async-clipboard-cached-getType-permission-revocation.tentative.https.html b/third_party/blink/web_tests/external/wpt/clipboard-apis/async-clipboard-cached-getType-permission-revocation.tentative.https.html
new file mode 100644
index 0000000..c2f33a8
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/clipboard-apis/async-clipboard-cached-getType-permission-revocation.tentative.https.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>
+  Cached ClipboardItem data survives clipboard-read permission revocation
+</title>
+<link rel="help" href="https://w3c.github.io/clipboard-apis/#dom-clipboarditem-gettype">
+<body>Body needed for clipboard access</body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<script>
+promise_test(async t => {
+  const readPermission = {name: 'clipboard-read'};
+  const writePermission = {name: 'clipboard-write'};
+
+  await test_driver.set_permission(readPermission, 'granted');
+  await test_driver.set_permission(writePermission, 'granted');
+
+  t.add_cleanup(async () => {
+    await test_driver.set_permission(readPermission, 'prompt');
+    await test_driver.set_permission(writePermission, 'prompt');
+  });
+
+  await navigator.clipboard.writeText('cached clipboard data');
+
+  const [item] = await navigator.clipboard.read();
+
+  // Materialize and cache the representation while access is authorized.
+  const firstBlob = await item.getType('text/plain');
+  assert_equals(await firstBlob.text(), 'cached clipboard data');
+
+  // Authorization changes, but neither the clipboard nor the cached
+  // representation changes.
+  await test_driver.set_permission(readPermission, 'denied');
+
+  const secondBlob = await item.getType('text/plain');
+  assert_equals(await secondBlob.text(), 'cached clipboard data');
+}, 'Revoking clipboard-read does not invalidate an already-resolved ' +
+   'ClipboardItem representation');
+</script>
Loading diff…

Original Bug Report

reported by [email protected]

ClipboardHost metadata methods expose system clipboard state without paste authorization

Three renderer-accessible blink.mojom.ClipboardHost methods return OS clipboard metadata without the browser-side authorization check used by clipboard payload reads:

  • GetSequenceNumber
  • ReadAvailableTypes
  • IsFormatAvailable

A compromised renderer can call these methods directly without clipboard-read permission, user activation, or recent paste interaction. It can poll the salted sequence number to detect clipboard changes and then enumerate formats to infer whether the user copied text, HTML, images, files, or distinctive custom data. Clipboard payloads remain gated, so the impact is metadata information disclosure.

Affected code:

content/browser/renderer_host/clipboard_host_impl.cc

These methods check whether the document is inactive, but do not call:

IsRendererPasteAllowed(clipboard_buffer, render_frame_host())

Payload-reading methods such as ReadText, ReadHtml, ReadFiles, and ReadAvailableCustomAndStandardFormats do perform that check.

Reproduction

From a compromised renderer holding the document-scoped ClipboardHost remote:

  1. Poll GetSequenceNumber(kCopyPaste).
  2. When its value changes, call ReadAvailableTypes(kCopyPaste).
  3. Copy text, rich content, an image, or files in another application.
  4. Observe the sequence change and corresponding format list without a permission prompt or user activation in the attacking document.

Expected

An unauthorized renderer cannot monitor clipboard changes or enumerate clipboard formats.

Actual

The browser returns the sequence value, available type list, and individual format-presence results without checking IsRendererPasteAllowed().

Suggested remediation

Require browser-side authorization before returning this metadata. A direct use of IsRendererPasteAllowed() may need compatibility analysis because the methods support legacy paste/DataTransfer paths; access could instead be bound to browser-maintained authorized-paste state.

Chromium issue 474763968 / CL 7509483 added IsRendererPasteAllowed() to ReadAvailableCustomAndStandardFormats and ReadUnsanitizedCustomFormat, but did not cover these three methods.

CL 8161110 corrected when Async Clipboard payload access receives an implicit grant during selection paste. This report is distinct: these browser-side metadata methods do not consult that authorization state.

View on issue tracker