CVE-2026-3938
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ClipboardPasteAllowedBrowserClientcontent/browser/renderer_host/clipboard_host_impl_unittest.cc |
modified | |
ClipboardHostImplTestcontent/browser/renderer_host/clipboard_host_impl_unittest.cc |
modified | |
ClipboardHostImplTestcontent/browser/renderer_host/clipboard_host_impl_unittest.cc |
modified | |
TEST_Fcontent/browser/renderer_host/clipboard_host_impl_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/clipboard_host_impl.cccontent/browser/renderer_host/clipboard_host_impl_unittest.cc
Patch
From 67ed1f75de8df62e337bef6e7725a085ad045e36 Mon Sep 17 00:00:00 2001 From: Rohan Raja <[email protected]> Date: Sat, 24 Jan 2026 09:30:40 -0800 Subject: [PATCH] Add IsRendererPasteAllowed checks to clipboard custom format methods in ClipboardHostImpl This CL fixes a security vulnerability where ReadAvailableCustomAndStandardFormats() and ReadUnsanitizedCustomFormat() bypass permission checks, allowing compromised renderers to read web custom clipboard formats without user activation. Changes Made: - Added IsRendererPasteAllowed() check to ReadAvailableCustomAndStandardFormats() Returns empty vector if permission denied - Added IsRendererPasteAllowed() check to ReadUnsanitizedCustomFormat() Returns empty BigBuffer if permission denied - Fixed all early return paths in ReadUnsanitizedCustomFormat() to properly invoke callbacks, preventing callback leaks Risk Assessment: Low - Follows established pattern from existing clipboard methods. - No functional changes to legitimate use cases requiring user activation Bug: 474763968 Change-Id: I555360a1d3f2309f2a90b8da9709b014480606ca Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7509483 Reviewed-by: Sambamurthy Bandaru <[email protected]> Reviewed-by: Dan Clark <[email protected]> Reviewed-by: Shweta Bindal <[email protected]> Commit-Queue: Rohan Raja <[email protected]> Cr-Commit-Position: refs/heads/main@{#1574177} --- diff --git a/content/browser/renderer_host/clipboard_host_impl.cc b/content/browser/renderer_host/clipboard_host_impl.cc index be6eb7b5..24f0fddc 100644 --- a/content/browser/renderer_host/clipboard_host_impl.cc +++ b/content/browser/renderer_host/clipboard_host_impl.cc @@ -605,6 +605,12 @@ void ClipboardHostImpl::ReadAvailableCustomAndStandardFormats( ReadAvailableCustomAndStandardFormatsCallback callback) { + if (!IsRendererPasteAllowed(ui::ClipboardBuffer::kCopyPaste, + render_frame_host())) { + std::move(callback).Run(std::vector<std::u16string>()); + return; + } + std::vector<std::u16string> format_types = ui::Clipboard::GetForCurrentThread() ->ReadAvailableStandardAndCustomFormatNames( @@ -615,10 +621,18 @@ void ClipboardHostImpl::ReadUnsanitizedCustomFormat( const std::u16string& format, ReadUnsanitizedCustomFormatCallback callback) { + if (!IsRendererPasteAllowed(ui::ClipboardBuffer::kCopyPaste, + render_frame_host())) { + std::move(callback).Run(mojo_base::BigBuffer()); + return; + } + // `kMaxFormatSize` includes the null terminator as well so we check if // the `format` size is strictly less than `kMaxFormatSize` or not. - if (format.length() >= blink::mojom::ClipboardHost::kMaxFormatSize) + if (format.length() >= blink::mojom::ClipboardHost::kMaxFormatSize) { + std::move(callback).Run(mojo_base::BigBuffer()); return; + } // Extract the custom format names and then query the web custom format // corresponding to the MIME type. @@ -630,15 +644,19 @@ std::string web_custom_format_string; if (custom_format_names.find(format_name) != custom_format_names.end()) web_custom_format_string = custom_format_names[format_name]; - if (web_custom_format_string.empty()) + if (web_custom_format_string.empty()) { + std::move(callback).Run(mojo_base::BigBuffer()); return; + } std::string result; ui::Clipboard::GetForCurrentThread()->ReadData( ui::ClipboardFormatType::CustomPlatformType(web_custom_format_string), data_endpoint.get(), &result); - if (result.size() >= blink::mojom::ClipboardHost::kMaxDataSize) + if (result.size() >= blink::mojom::ClipboardHost::kMaxDataSize) { + std::move(callback).Run(mojo_base::BigBuffer()); return; + } base::span<const uint8_t> span = base::as_byte_span(result); mojo_base::BigBuffer buffer = mojo_base::BigBuffer(span); std::move(callback).Run(std::move(buffer)); diff --git a/content/browser/renderer_host/clipboard_host_impl_unittest.cc b/content/browser/renderer_host/clipboard_host_impl_unittest.cc index f6eda8e..5b12e63 100644 --- a/content/browser/renderer_host/clipboard_host_impl_unittest.cc +++ b/content/browser/renderer_host/clipboard_host_impl_unittest.cc @@ -24,6 +24,8 @@ #include "content/public/test/browser_task_environment.h" #include "content/public/test/navigation_simulator.h" #include "content/public/test/test_renderer_host.h" +#include "content/public/test/test_utils.h" +#include "content/test/test_content_browser_client.h" #include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/system/message_pipe.h" #include "skia/ext/skia_utils_base.h" @@ -51,6 +53,26 @@ } namespace content { +// Custom ContentBrowserClient for testing clipboard paste permissions. +class ClipboardPasteAllowedBrowserClient : public TestContentBrowserClient { + public: + ClipboardPasteAllowedBrowserClient() = default; + ~ClipboardPasteAllowedBrowserClient() override = default; + + void set_is_clipboard_paste_allowed(bool allowed) { + is_clipboard_paste_allowed_ = allowed; + } + + // ContentBrowserClient: + bool IsClipboardPasteAllowed( + content::RenderFrameHost* render_frame_host) override { + return is_clipboard_paste_allowed_; + } + + private: + bool is_clipboard_paste_allowed_ = true; +}; + class ClipboardHostImplTest : public RenderViewHostTestHarness { protected: ClipboardHostImplTest() { ui::TestClipboard::CreateForCurrentThread(); } @@ -920,4 +942,102 @@ base::RunLoop().RunUntilIdle(); } +TEST_F(ClipboardHostImplTest, + ReadUnsanitizedCustomFormat_WithoutUserActivation) { + // Setup: Custom browser client that denies clipboard paste + ClipboardPasteAllowedBrowserClient browser_client; + browser_client.set_is_clipboard_paste_allowed(false); + ScopedContentBrowserClientSetting browser_client_setting(&browser_client); + + // Write custom format to clipboard + std::string test_data = "confidential_custom_data"; + { + ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste); + writer.WriteData(u"web text/custom", + mojo_base::BigBuffer(base::as_byte_span(test_data))); + } + + // Test: Try to read custom format without user activation + base::test::TestFuture<mojo_base::BigBuffer> future; + mojo_clipboard()->ReadUnsanitizedCustomFormat(u"web text/custom", + future.GetCallback()); + + // Verify: Should return empty buffer due to permission check failure + EXPECT_EQ(0u, future.Get().size()); +} + +TEST_F(ClipboardHostImplTest, + ReadAvailableCustomAndStandardFormats_WithUserActivation) { + // Setup: Custom browser client that allows clipboard paste + ClipboardPasteAllowedBrowserClient browser_client; + browser_client.set_is_clipboard_paste_allowed(true); + ScopedContentBrowserClientSetting browser_client_setting(&browser_client); + + // Write some standard format data that TestClipboard can handle + mojo_clipboard()->WriteText(u"test text"); + mojo_clipboard()->CommitWrite(); + base::RunLoop().RunUntilIdle(); + + // Test: Read available formats with permission allowed + base::test::TestFuture<const std::vector<std::u16string>&> future; + mojo_clipboard()->ReadAvailableCustomAndStandardFormats(future.GetCallback()); + + // Verify: With permission allowed, the call completes successfully. + // TestClipboard should return standard formats like "text/plain". + const auto& formats = future.Get(); + EXPECT_TRUE(std::ranges::contains(formats, u"text/plain")); +} + +TEST_F(ClipboardHostImplTest, ReadUnsanitizedCustomFormat_WithUserActivation) { + // Setup: Custom browser client that allows clipboard paste + ClipboardPasteAllowedBrowserClient browser_client; + browser_client.set_is_clipboard_paste_allowed(true); + ScopedContentBrowserClientSetting browser_client_setting(&browser_client); + + // Write custom format data using ScopedClipboardWriter which properly + // handles web custom format metadata + std::string test_data = "test_custom_data"; + { + ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste); + writer.WriteData(u"text/custom", + mojo_base::BigBuffer(base::as_byte_span(test_data))); + }
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 f6eda8e..5b12e63 100644
--- a/content/browser/renderer_host/clipboard_host_impl_unittest.cc
+++ b/content/browser/renderer_host/clipboard_host_impl_unittest.cc
@@ -24,6 +24,8 @@
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/test_utils.h"
+#include "content/test/test_content_browser_client.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "skia/ext/skia_utils_base.h"
@@ -51,6 +53,26 @@
}
namespace content {
+// Custom ContentBrowserClient for testing clipboard paste permissions.
+class ClipboardPasteAllowedBrowserClient : public TestContentBrowserClient {
+ public:
+ ClipboardPasteAllowedBrowserClient() = default;
+ ~ClipboardPasteAllowedBrowserClient() override = default;
+
+ void set_is_clipboard_paste_allowed(bool allowed) {
+ is_clipboard_paste_allowed_ = allowed;
+ }
+
+ // ContentBrowserClient:
+ bool IsClipboardPasteAllowed(
+ content::RenderFrameHost* render_frame_host) override {
+ return is_clipboard_paste_allowed_;
+ }
+
+ private:
+ bool is_clipboard_paste_allowed_ = true;
+};
+
class ClipboardHostImplTest : public RenderViewHostTestHarness {
protected:
ClipboardHostImplTest() { ui::TestClipboard::CreateForCurrentThread(); }
@@ -920,4 +942,102 @@
base::RunLoop().RunUntilIdle();
}
+TEST_F(ClipboardHostImplTest,
+ ReadUnsanitizedCustomFormat_WithoutUserActivation) {
+ // Setup: Custom browser client that denies clipboard paste
+ ClipboardPasteAllowedBrowserClient browser_client;
+ browser_client.set_is_clipboard_paste_allowed(false);
+ ScopedContentBrowserClientSetting browser_client_setting(&browser_client);
+
+ // Write custom format to clipboard
+ std::string test_data = "confidential_custom_data";
+ {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteData(u"web text/custom",
+ mojo_base::BigBuffer(base::as_byte_span(test_data)));
+ }
+
+ // Test: Try to read custom format without user activation
+ base::test::TestFuture<mojo_base::BigBuffer> future;
+ mojo_clipboard()->ReadUnsanitizedCustomFormat(u"web text/custom",
+ future.GetCallback());
+
+ // Verify: Should return empty buffer due to permission check failure
+ EXPECT_EQ(0u, future.Get().size());
+}
+
+TEST_F(ClipboardHostImplTest,
+ ReadAvailableCustomAndStandardFormats_WithUserActivation) {
+ // Setup: Custom browser client that allows clipboard paste
+ ClipboardPasteAllowedBrowserClient browser_client;
+ browser_client.set_is_clipboard_paste_allowed(true);
+ ScopedContentBrowserClientSetting browser_client_setting(&browser_client);
+
+ // Write some standard format data that TestClipboard can handle
+ mojo_clipboard()->WriteText(u"test text");
+ mojo_clipboard()->CommitWrite();
+ base::RunLoop().RunUntilIdle();
+
+ // Test: Read available formats with permission allowed
+ base::test::TestFuture<const std::vector<std::u16string>&> future;
+ mojo_clipboard()->ReadAvailableCustomAndStandardFormats(future.GetCallback());
+
+ // Verify: With permission allowed, the call completes successfully.
+ // TestClipboard should return standard formats like "text/plain".
+ const auto& formats = future.Get();
+ EXPECT_TRUE(std::ranges::contains(formats, u"text/plain"));
+}
+
+TEST_F(ClipboardHostImplTest, ReadUnsanitizedCustomFormat_WithUserActivation) {
+ // Setup: Custom browser client that allows clipboard paste
+ ClipboardPasteAllowedBrowserClient browser_client;
+ browser_client.set_is_clipboard_paste_allowed(true);
+ ScopedContentBrowserClientSetting browser_client_setting(&browser_client);
+
+ // Write custom format data using ScopedClipboardWriter which properly
+ // handles web custom format metadata
+ std::string test_data = "test_custom_data";
+ {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteData(u"text/custom",
+ mojo_base::BigBuffer(base::as_byte_span(test_data)));
+ }
+
+ // Test: Read custom format with permission allowed
+ // Note: Need to prepend "web " prefix to match how ExtractCustomPlatformNames
+ // works
+ base::test::TestFuture<mojo_base::BigBuffer> future;
+ mojo_clipboard()->ReadUnsanitizedCustomFormat(u"web text/custom",
+ future.GetCallback());
+
+ // Verify: With permission allowed, the data should be successfully retrieved
+ const auto& result = future.Get();
+ EXPECT_GT(result.size(), 0u);
+
+ // Verify the content matches what was written
+ std::string retrieved_data(result.begin(), result.end());
+ EXPECT_EQ(retrieved_data, test_data);
+}
+
+TEST_F(ClipboardHostImplTest,
+ ReadAvailableCustomAndStandardFormats_TextWithoutUserActivation) {
+ // Setup: Custom browser client that denies clipboard paste
+ ClipboardPasteAllowedBrowserClient browser_client;
+ browser_client.set_is_clipboard_paste_allowed(false);
+ ScopedContentBrowserClientSetting browser_client_setting(&browser_client);
+
+ // Write standard text format to clipboard
+ {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteText(u"test text");
+ }
+
+ // Test: Try to read available formats without permission
+ base::test::TestFuture<const std::vector<std::u16string>&> future;
+ mojo_clipboard()->ReadAvailableCustomAndStandardFormats(future.GetCallback());
+
+ // Verify: Should return empty vector due to permission check failure
+ EXPECT_EQ(0u, future.Get().size());
+}
+
} // namespace content
Original Bug Report
ClipboardHost custom format methods bypass IsClipboardPasteAllowed (Issue 40051481 incomplete)
Steps to reproduce the problem
STEPS TO REPRODUCE:
-
Build Chromium content_shell
-
Start HTTP server from the Release directory: cd [chromium_src]/out/Release python -m http.server 8888
-
Copy the attached PoC files (poc.html, victim.html) to the Release directory
-
Open victim.html in NORMAL Chrome browser: http://localhost:8888/victim.html Click “Copy Sensitive Data” to put test data on clipboard
-
Launch content_shell with MojoJS (simulates compromised renderer): .\content_shell.exe –enable-blink-features=MojoJS,MojoJSTest http://localhost:8888/poc.html
VULNERABILITY: In content/browser/renderer_host/clipboard_host_impl.cc:
- ReadAvailableCustomAndStandardFormats() (line 607)
- ReadUnsanitizedCustomFormat() (line 616)
These methods do not call IsRendererPasteAllowed() before accessing clipboard.
Other Read methods (ReadText, ReadHtml, ReadImage, etc.) do have this check.
Related: Issue 40051481 addressed similar clipboard permission bypass.
Problem Description
In content/browser/renderer_host/clipboard_host_impl.cc, two methods access clipboard data without calling IsRendererPasteAllowed():
ReadAvailableCustomAndStandardFormats() (line 607) ReadUnsanitizedCustomFormat() (line 616) Other Read methods in the same file do call IsRendererPasteAllowed() before accessing clipboard data:
ReadText() ReadHtml() ReadSvg() ReadRtf() ReadPng() ReadFiles() ReadDataTransferCustomData() Historical Context:
The custom format methods were added in September 2021 (commit 8aa9dc97 - “[Clipboard API] Clipboard Custom Formats implementation Part 7”). Issue 40051481 was fixed in October 2022, which added IsRendererPasteAllowed() checks to clipboard read methods. However, ReadAvailableCustomAndStandardFormats() and ReadUnsanitizedCustomFormat() were not updated as part of that fix.
Affected Code: void ClipboardHostImpl::ReadAvailableCustomAndStandardFormats( ReadAvailableCustomAndStandardFormatsCallback callback) { // No IsRendererPasteAllowed() check std::vectorstd::u16string format_types = ui::Clipboard::GetForCurrentThread() ->ReadAvailableStandardAndCustomFormatNames(…); std::move(callback).Run(std::move(format_types)); }
void ClipboardHostImpl::ReadUnsanitizedCustomFormat( const std::u16string& format, ReadUnsanitizedCustomFormatCallback callback) { // No IsRendererPasteAllowed() check // … reads clipboard data directly }
Additional Comments
These methods were added in September 2021 (commit 8aa9dc97) without IsRendererPasteAllowed() checks. When Issue 40051481 was fixed in October 2022, the permission check was added to other clipboard read methods but these two custom format methods were not included. This is an incomplete fix of Issue 40051481 rather than a regression.
The W3C Clipboard API spec Section 7.3.1 requires read() to run “check clipboard read permission” (Section 9.1.1) before accessing clipboard data. If the check fails, the operation must reject with NotAllowedError. The vulnerable methods bypass this requirement.
Reference: https://w3c.github.io/clipboard-apis/#dom-clipboard-read
Summary
ClipboardHost custom format methods bypass IsClipboardPasteAllowed (Issue 40051481 incomplete)
Custom Questions
Reporter credit:
vicevirus
Additional Data
Category: Security
Chrome Channel: Not sure
Regression: No \