CVE-2026-106388
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ClipboardHostImplWriteTestcontent/browser/renderer_host/clipboard_host_impl_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/clipboard_host_impl.ccthird_party/blink/web_tests/external/wpt/clipboard-apis/async-clipboard-cached-getType-permission-revocation.tentative.https.htmlcontent/browser/renderer_host/clipboard_host_impl_unittest.cc
Patch
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>
Regression Test / PoC
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>
Original Bug Report
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:
GetSequenceNumberReadAvailableTypesIsFormatAvailable
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:
- Poll
GetSequenceNumber(kCopyPaste). - When its value changes, call
ReadAvailableTypes(kCopyPaste). - Copy text, rich content, an image, or files in another application.
- 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.
Related work
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.