CVE-2026-78967
Overview
Files Changed
content/browser/renderer_host/clipboard_host_impl.cc
Patch
From d5674ed78a8c68ca800217c5a3eda5ddfbbc8a2c Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@google.com>
Date: Wed, 08 Jul 2026 00:51:12 -0700
Subject: [PATCH] ClipboardHostImpl: ignore requests from inactive documents
ClipboardHostImpl is a DocumentService whose Mojo receiver stays bound
while its RenderFrameHost is inactive (e.g. stored in the back/forward
cache). The IPC handlers previously did not check the document
lifecycle, so a renderer could keep reading or writing the system
clipboard, register a clipboardchange listener, or piggy-back on the
tab-wide recent-interaction state of the foreground page after its
document had become inactive.
Add render_frame_host().IsActive() guards to every mojom::ClipboardHost
entry point (including the macOS find- pasteboard write) and to
OnClipboardDataChanged(). Read methods now reply with empty results,
write methods are dropped, and CommitWrite() discards any staged writer
state when the document is not active.
Bug: 514529599
Change-Id: Iad5186e86b5d5e9fb2342614700aac2178b275b0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8030502
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Commit-Queue: Eriko Kurimoto <elkurin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1658557}
---
diff --git a/content/browser/renderer_host/clipboard_host_impl.cc b/content/browser/renderer_host/clipboard_host_impl.cc
index eecfccb8..dc1dd4f 100644
--- a/content/browser/renderer_host/clipboard_host_impl.cc
+++ b/content/browser/renderer_host/clipboard_host_impl.cc
@@ -117,12 +117,22 @@
void ClipboardHostImpl::GetSequenceNumber(ui::ClipboardBuffer clipboard_buffer,
GetSequenceNumberCallback callback) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard)) {
+ std::move(callback).Run(absl::uint128());
+ return;
+ }
std::move(callback).Run(GetSequenceNumberImpl(clipboard_buffer));
}
void ClipboardHostImpl::ReadAvailableTypes(
ui::ClipboardBuffer clipboard_buffer,
ReadAvailableTypesCallback callback) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard)) {
+ std::move(callback).Run({});
+ return;
+ }
auto* clipboard = ui::Clipboard::GetForCurrentThread();
auto data_endpoint = CreateDataEndpoint(render_frame_host());
@@ -190,6 +200,11 @@
void ClipboardHostImpl::IsFormatAvailable(blink::mojom::ClipboardFormat format,
ui::ClipboardBuffer clipboard_buffer,
IsFormatAvailableCallback callback) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard)) {
+ std::move(callback).Run(false);
+ return;
+ }
ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread();
auto data_endpoint = CreateDataEndpoint(render_frame_host());
clipboard->GetAllAvailableFormats(
@@ -230,7 +245,9 @@
void ClipboardHostImpl::ReadText(ui::ClipboardBuffer clipboard_buffer,
ReadTextCallback callback) {
- if (!IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard) ||
+ !IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
std::move(callback).Run(std::u16string());
return;
}
@@ -264,7 +281,9 @@
void ClipboardHostImpl::ReadHtml(ui::ClipboardBuffer clipboard_buffer,
ReadHtmlCallback callback) {
- if (!IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard) ||
+ !IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
std::move(callback).Run(std::u16string(), GURL(), 0, 0);
return;
}
@@ -305,7 +324,9 @@
void ClipboardHostImpl::ReadSvg(ui::ClipboardBuffer clipboard_buffer,
ReadSvgCallback callback) {
- if (!IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard) ||
+ !IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
std::move(callback).Run(std::u16string());
return;
}
@@ -340,7 +361,9 @@
void ClipboardHostImpl::ReadRtf(ui::ClipboardBuffer clipboard_buffer,
ReadRtfCallback callback) {
- if (!IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard) ||
+ !IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
std::move(callback).Run(std::string());
return;
}
@@ -376,7 +399,9 @@
void ClipboardHostImpl::ReadPng(ui::ClipboardBuffer clipboard_buffer,
ReadPngCallback callback) {
- if (!IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard) ||
+ !IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
std::move(callback).Run(mojo_base::BigBuffer());
return;
}
@@ -425,7 +450,9 @@
void ClipboardHostImpl::ReadFiles(ui::ClipboardBuffer clipboard_buffer,
ReadFilesCallback callback) {
- if (!IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard) ||
+ !IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
std::move(callback).Run(blink::mojom::ClipboardFiles::New());
return;
}
@@ -511,7 +538,9 @@
ui::ClipboardBuffer clipboard_buffer,
const std::u16string& type,
ReadDataTransferCustomDataCallback callback) {
- if (!IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard) ||
+ !IsRendererPasteAllowed(clipboard_buffer, render_frame_host())) {
std::move(callback).Run(std::u16string());
return;
}
@@ -549,6 +578,10 @@
}
void ClipboardHostImpl::WriteText(const std::u16string& text) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard)) {
+ return;
+ }
ClipboardPasteData data;
data.text = text;
++pending_writes_;
@@ -565,6 +598,10 @@
void ClipboardHostImpl::WriteHtml(const std::u16string& markup,
const GURL& url) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard)) {
+ return;
+ }
ClipboardPasteData data;
data.html = markup;
++pending_writes_;
@@ -580,6 +617,10 @@
}
void ClipboardHostImpl::WriteSvg(const std::u16string& markup) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard)) {
+ return;
+ }
ClipboardPasteData data;
data.svg = markup;
++pending_writes_;
@@ -595,11 +636,19 @@
}
void ClipboardHostImpl::WriteSmartPasteMarker() {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard)) {
+ return;
+ }
clipboard_writer_->WriteWebSmartPaste();
}
void ClipboardHostImpl::WriteDataTransferCustomData(
const base::flat_map<std::u16string, std::u16string>& data) {
+ if (render_frame_host().IsInactiveAndDisallowActivation(
+ DisallowActivationReasonId::kClipboard)) {
+ return;
+ }
ClipboardPasteData clipboard_paste_data;
clipboard_paste_data.custom_data = data;
@@ -624,11 +673,19 @@
void ClipboardHostImpl::WriteBookmark(const std::string& url,
const std::u16string& title) {
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 285c297f..9c8cb86c 100644
--- a/content/browser/renderer_host/clipboard_host_impl_unittest.cc
+++ b/content/browser/renderer_host/clipboard_host_impl_unittest.cc
@@ -168,6 +168,48 @@
&unused_sequence_number);
}
+TEST_F(ClipboardHostImplTest, WriteFromInactiveDocumentIsIgnored) {
+ const std::u16string kInitial = u"initial";
+ {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteText(kInitial);
+ }
+
+ static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+ ASSERT_FALSE(web_contents()->GetPrimaryMainFrame()->IsActive());
+
+ mojo_clipboard()->WriteText(u"from-inactive-document");
+ mojo_clipboard()->CommitWrite();
+ mojo_clipboard().FlushForTesting();
+
+ base::test::TestFuture<std::u16string> future;
+ system_clipboard()->ReadText(ui::ClipboardBuffer::kCopyPaste,
+ /*data_dst=*/std::nullopt, future.GetCallback());
+ EXPECT_EQ(kInitial, future.Take());
+}
+
+TEST_F(ClipboardHostImplTest, ReadFromInactiveDocumentIsIgnored) {
+ {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteText(u"clipboard-text");
+ }
+
+ static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+ ASSERT_FALSE(web_contents()->GetPrimaryMainFrame()->IsActive());
+
+ std::u16string result = u"non-empty";
+ mojo_clipboard()->ReadText(ui::ClipboardBuffer::kCopyPaste, &result);
+ EXPECT_TRUE(result.empty());
+
+ std::vector<std::u16string> types = {u"non-empty"};
+ mojo_clipboard()->ReadAvailableTypes(ui::ClipboardBuffer::kCopyPaste, &types);
+ EXPECT_TRUE(types.empty());
+}
+
TEST_F(ClipboardHostImplTest, ReadAvailableTypes_TextUriList) {
std::vector<std::u16string> types;
@@ -1076,8 +1118,10 @@
return fake_clipboard_host_impl_;
}
- private:
+ protected:
mojo::Remote<blink::mojom::ClipboardHost> remote_;
+
+ private:
// `ClipboardHostImpl` is a `DocumentService` and manages its own
// lifetime.
raw_ptr<ClipboardHostImpl> fake_clipboard_host_impl_;
@@ -1131,6 +1175,22 @@
base::RunLoop().RunUntilIdle();
}
+TEST_F(ClipboardHostImplChangeTest, NoNotificationToInactiveDocument) {
+ auto mock_listener = std::make_unique<MockClipboardListener>();
+ EXPECT_CALL(*mock_listener, OnClipboardDataChanged).Times(0);
+
+ clipboard_host_impl()->RegisterClipboardListener(mock_listener->GetRemote());
+ EXPECT_TRUE(clipboard_host_impl()->listening_to_clipboard_);
+
+ static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame())
+ ->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+ ASSERT_FALSE(web_contents()->GetPrimaryMainFrame()->IsActive());
+
+ ui::ClipboardMonitor::GetInstance()->NotifyClipboardDataChanged();
+ remote_.FlushForTesting();
+}
+
TEST_F(ClipboardHostImplChangeTest, ClipboardListenerDisconnect) {
// Initially, the clipboard host should not be listening to clipboard changes
EXPECT_FALSE(clipboard_host_impl()->listening_to_clipboard_);
Original Bug Report
Clipboard access by background (BFCached) pages via ClipboardHostImpl
Flapjack, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A compromised renderer in the Back/Forward Cache (BFCache) can potentially read, write, or monitor the system clipboard. The ClipboardHostImpl Mojo interface lacks active document checks, and background pages can exploit tab-wide user interaction states to bypass clipboard read permissions.
Affected files:
content/browser/renderer_host/clipboard_host_impl.cc
Estimated timestamp from git blame: 2021-02-12
Description
The ClipboardHostImpl class (content/browser/renderer_host/clipboard_host_impl.cc) handles clipboard-related Mojo IPCs from renderer processes. It inherits from DocumentService, which ties the Mojo connection to a RenderFrameHost. However, when a document enters the Back/Forward Cache (BFCache), the RenderFrameHost is preserved, and the Mojo connection is not severed or paused.
Because ClipboardHostImpl does not verify if the calling document is actively displayed (e.g., using render_frame_host().IsActive()), a compromised renderer can continue to invoke clipboard methods from the background.
This lack of lifecycle checking exposes three distinct potential attack vectors:
- Bypassing Paste Restrictions (Read): Operations like
ReadTextrequire a user gesture. The browser checks this viaIsRendererPasteAllowed(), which delegates toChromeContentBrowserClient::IsClipboardPasteAllowed(). This function checksWebContents::HasRecentInteraction(). Crucially, interaction state is tracked at the tab (WebContents) level. If the user interacts with the newly loaded, active page in the same tab, the BFCached document inherits this interaction state and can silently read the clipboard. - Silent Clipboard Writes: Methods like
WriteTextandCommitWritelack both user interaction checks and active document checks in the browser process. A background page can silently overwrite the user’s clipboard. - Silent Clipboard Monitoring: A background page can call
RegisterClipboardListener(). Because there are no active checks during registration or when events fire (OnClipboardDataChanged), the BFCached renderer receives continuous, asynchronous notifications whenever the user copies new data across the OS.
Suggested Attacker Steps
Note: These are potential steps based on code analysis; a working proof of concept has not been executed.
- An attacker lures the victim to a malicious website.
- The attacker exploits a separate memory corruption vulnerability to gain Remote Code Execution (RCE) within the sandboxed renderer process.
- The victim navigates away to a legitimate site in the same tab. Chrome places the malicious page into the BFCache.
- The attacker uses their RCE to bypass the renderer’s application-level freeze, keeping their threads alive.
- To steal clipboard data (Read): The user clicks or types on the new, legitimate page. Within 5 seconds, the attacker’s BFCached renderer sends a
ReadTextMojo IPC. The browser process checks the tab’s interaction state, sees the recent interaction, and returns the clipboard contents to the attacker. - To monitor the clipboard: The attacker sends a
RegisterClipboardListenerIPC from the BFCache and silently receives all subsequent clipboard changes.
Suggested Fix
The most direct fix is to ensure ClipboardHostImpl does not process IPCs for inactive documents. Add checks at the beginning of all IPC entry points in content/browser/renderer_host/clipboard_host_impl.cc:
if (!render_frame_host().IsActive()) {
// Run callback with empty/default data or report a bad message.
return;
}
Alternatively, consider evaluating if the Mojo Binder Policy for ClipboardHost should be updated to defer or cancel messages when a document enters the BFCache, similar to how it handles Prerendering.
Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.