CVE-2026-87582
Overview
Background
- `ClipboardHostImpl::WriteBookmark`
- the browser-process Mojo endpoint that accepts a free-form URL string and title from a renderer and stages them onto the system clipboard as a bookmark/URL entry.
- `ui::ClipboardMac::WriteURL`
- the macOS clipboard backend that, for
file://URLs, routes them throughPasteboardItemsFromUrlsand writes them to the native pasteboard asNSPasteboardTypeFileURL(a file entry, not a plain URL). - `ReadFiles()` / `clipboard.mojom` invariant
- the clipboard read path that is documented to only return file paths the user deliberately placed there, so a renderer must not be able to synthesize its own file entries.
- Confused deputy
- a lower-privileged caller (the renderer) tricks a higher-privileged component (the browser process) into performing an action on its behalf that the caller could not perform directly.
Root Cause Analysis
ClipboardHostImpl::WriteBookmark treated the renderer-supplied URL as opaque and, after only its existing activation/invalid-URL guards, forwarded it straight into clipboard_writer_->WriteURL(ui::ClipboardUrlInfo{.url = GURL(url), ...}). On macOS, ui::ClipboardMac::WriteURL interprets a file:// scheme specially and writes it to the pasteboard as an NSPasteboardTypeFileURL file entry rather than a plain URL, so a subsequent ReadFiles() returns that same local path back to the renderer. That breaks the invariant documented on ReadFiles in clipboard.mojom — that read-back file entries reflect user-chosen files, not arbitrary renderer-supplied paths — making the browser a confused deputy that launders an attacker-chosen path into the trusted clipboard file list.
The fix constructs GURL gurl(url) once and adds an early return when gurl.SchemeIsFile() is true, so file:// bookmarks never reach WriteURL; the branch runs before the sink, and legitimate non-file bookmarks proceed via std::move(gurl). This is a tightening: a new scheme check is added alongside the existing invalid-URL guard, closing the write path that fed the file-typed pasteboard entry.
file:// bookmark flow into ClipboardMac::WriteURL, which promotes it to a file entry readable via ReadFiles(); the fix rejects file:// schemes in the browser process before the value is ever written to the clipboard.Attack Path
- Malicious renderer issues WriteBookmark
A compromised or hostile renderer calls the
WriteBookmarkMojo method with afile://URL such asfile:///etc/passwdand any title. - Guard is bypassed pre-patch
The pre-patch
WriteBookmarkapplies only its activation and invalid-URL checks, then constructsGURL(url)and forwards it toclipboard_writer_->WriteURL, with no scheme filtering. - macOS backend promotes it to a file entry
ui::ClipboardMac::WriteURLsees thefile://scheme and, viaPasteboardItemsFromUrls, writes the path to the native pasteboard asNSPasteboardTypeFileURLrather than a plain URL string. - Renderer reads the path back
A subsequent
ReadFiles()returns the attacker-supplied local path to the renderer as if it were a user-selected file, violating theReadFilesinvariant inclipboard.mojom. - Confused-deputy authorization bypass The renderer has thereby caused the trusted browser process to place an arbitrary local filesystem path into the clipboard file list it can observe, potentially feeding downstream code that treats those paths as legitimately user-granted file access (inferred).
Impact Assessment
ReadFiles(), bypassing the authorization boundary that clipboard file entries reflect user intent (an AuthorizationError-class confused deputy in DataTransfer), on macOS specifically. This requires the renderer to already be able to call the WriteBookmark Mojo endpoint; any further escalation, such as using the leaked path to obtain the file’s contents or broader local file access, is not demonstrated by the patch (inferred). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3.Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/renderer_host/clipboard_host_impl_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/clipboard_host_impl.hcontent/browser/renderer_host/clipboard_host_impl.cccontent/browser/renderer_host/clipboard_host_impl_unittest.cc
Audit Directions
- Renderer-controlled URLs into privileged sinksAudit every browser-process Mojo endpoint that accepts a free-form URL/scheme from a renderer (clipboard, downloads, drag-and-drop, navigation helpers) and traces it into OS or platform APIs that treat
file://differently, ensuring scheme validation happens before the sink. - Write-then-read invariant mismatchesLook for cases where a renderer-writable channel (WriteBookmark, WriteURL, image/format writers) feeds a read path documented to only return user-chosen or trusted data, since a scheme or type coercion in the backend can silently violate that invariant.
- Platform-specific type promotionReview per-OS clipboard/pasteboard backends (e.g.
ClipboardMac, and the equivalent Windows/Linux paths) for logic that reclassifies a value into a more privileged type (URL → file entry), and confirm the cross-platform layer filters such inputs uniformly.
Patch
From 4939275abb618e309bdbcf4d9931f671eed71825 Mon Sep 17 00:00:00 2001
From: Avi Drissman <avi@chromium.org>
Date: Mon, 03 Aug 2026 08:56:46 -0700
Subject: [PATCH] Drop file:// URLs in ClipboardHostImpl::WriteBookmark
WriteBookmark accepts a free-form URL string from the renderer. On
macOS, ClipboardMac::WriteURL forwards file:// URLs to
PasteboardItemsFromUrls, which writes them to the system pasteboard as
NSPasteboardTypeFileURL. A subsequent ReadFiles() then returns the same
path back to the renderer, contrary to the invariant documented on
ReadFiles in clipboard.mojom.
Renderers have no need to write file:// bookmarks, so drop them in
ClipboardHostImpl alongside the existing invalid-URL guard.
Fixed: 500094528
Change-Id: Iabbef8189dd88c955bcc1cf83874ab8b6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8177579
Auto-Submit: Avi Drissman <avi@chromium.org>
Reviewed-by: Bryan Oltman <bryanoltman@google.com>
Commit-Queue: Bryan Oltman <bryanoltman@google.com>
Cr-Commit-Position: refs/heads/main@{#1672699}
---
diff --git a/content/browser/renderer_host/clipboard_host_impl.cc b/content/browser/renderer_host/clipboard_host_impl.cc
index b48457b10d..5c2ca77c 100644
--- a/content/browser/renderer_host/clipboard_host_impl.cc
+++ b/content/browser/renderer_host/clipboard_host_impl.cc
@@ -706,8 +706,16 @@
DisallowActivationReasonId::kClipboard)) {
return;
}
+
+ GURL gurl(url);
+ // Drop file:// URLs so a renderer cannot place a local path on the clipboard
+ // via WriteBookmark and read it back via ReadFiles().
+ if (gurl.SchemeIsFile()) {
+ return;
+ }
+
clipboard_writer_->WriteURL(
- ui::ClipboardUrlInfo{.url = GURL(url), .title = title});
+ ui::ClipboardUrlInfo{.url = std::move(gurl), .title = title});
}
void ClipboardHostImpl::WriteImage(const SkBitmap& bitmap) {
diff --git a/content/browser/renderer_host/clipboard_host_impl.h b/content/browser/renderer_host/clipboard_host_impl.h
index ec953f6..5efad4d 100644
--- a/content/browser/renderer_host/clipboard_host_impl.h
+++ b/content/browser/renderer_host/clipboard_host_impl.h
@@ -85,6 +85,7 @@
FRIEND_TEST_ALL_PREFIXES(ClipboardHostImplWriteTest,
WriteBookmark_InvalidUrl_DoesNotCrash);
FRIEND_TEST_ALL_PREFIXES(ClipboardHostImplWriteTest, WriteBookmark_EmptyUrl);
+ FRIEND_TEST_ALL_PREFIXES(ClipboardHostImplWriteTest, WriteBookmark_FileUrl);
FRIEND_TEST_ALL_PREFIXES(ClipboardHostImplWriteTest, WriteBitmap);
FRIEND_TEST_ALL_PREFIXES(ClipboardHostImplWriteTest, WriteBitmap_Empty);
FRIEND_TEST_ALL_PREFIXES(ClipboardHostImplWriteTest,
diff --git a/content/browser/renderer_host/clipboard_host_impl_unittest.cc b/content/browser/renderer_host/clipboard_host_impl_unittest.cc
index cb2cf6f0..194a384 100644
--- a/content/browser/renderer_host/clipboard_host_impl_unittest.cc
+++ b/content/browser/renderer_host/clipboard_host_impl_unittest.cc
@@ -545,6 +545,25 @@
EXPECT_TRUE(title.empty());
}
+// file:// URLs are dropped so a renderer cannot place a local path on the
+// clipboard via WriteBookmark and read it back via ReadFiles.
+TEST_F(ClipboardHostImplWriteTest, WriteBookmark_FileUrl) {
+ clipboard_host_impl()->WriteBookmark("file:///etc/passwd", u"some title");
+ clipboard_host_impl()->CommitWrite();
+
+ std::u16string title;
+ std::string url;
+ ui::clipboard_test_util::ReadBookmark(system_clipboard(),
+ /*data_dst=*/nullptr, &title, &url);
+ EXPECT_TRUE(url.empty()) << "Got url='" << url << "'";
+ EXPECT_TRUE(title.empty()) << "Got title='" << title << "'";
+
+ std::vector<ui::FileInfo> files = ui::clipboard_test_util::ReadFilenames(
+ system_clipboard(), ui::ClipboardBuffer::kCopyPaste,
+ /*data_dst=*/nullptr);
+ EXPECT_TRUE(files.empty());
+}
+
TEST_F(ClipboardHostImplWriteTest, WriteBitmap) {
const SkBitmap kBitmap = gfx::test::CreateBitmap(3, 2);
clipboard_host_impl()->WriteImage(kBitmap);
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 cb2cf6f0..194a384 100644
--- a/content/browser/renderer_host/clipboard_host_impl_unittest.cc
+++ b/content/browser/renderer_host/clipboard_host_impl_unittest.cc
@@ -545,6 +545,25 @@
EXPECT_TRUE(title.empty());
}
+// file:// URLs are dropped so a renderer cannot place a local path on the
+// clipboard via WriteBookmark and read it back via ReadFiles.
+TEST_F(ClipboardHostImplWriteTest, WriteBookmark_FileUrl) {
+ clipboard_host_impl()->WriteBookmark("file:///etc/passwd", u"some title");
+ clipboard_host_impl()->CommitWrite();
+
+ std::u16string title;
+ std::string url;
+ ui::clipboard_test_util::ReadBookmark(system_clipboard(),
+ /*data_dst=*/nullptr, &title, &url);
+ EXPECT_TRUE(url.empty()) << "Got url='" << url << "'";
+ EXPECT_TRUE(title.empty()) << "Got title='" << title << "'";
+
+ std::vector<ui::FileInfo> files = ui::clipboard_test_util::ReadFilenames(
+ system_clipboard(), ui::ClipboardBuffer::kCopyPaste,
+ /*data_dst=*/nullptr);
+ EXPECT_TRUE(files.empty());
+}
+
TEST_F(ClipboardHostImplWriteTest, WriteBitmap) {
const SkBitmap kBitmap = gfx::test::CreateBitmap(3, 2);
clipboard_host_impl()->WriteImage(kBitmap);
Original Bug Report
Potential macOS sandbox escape / arbitrary file read via ClipboardHost
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 compromised renderer on macOS can potentially achieve an arbitrary file read and escape the sandbox by abusing the ClipboardHost Mojo interface. By sending a local file URL via WriteBookmark and then reading it back via ReadFiles after a user click, the renderer can trick the browser into granting it read permissions and a DataTransferToken to any file on the disk.
Affected files:
ui/base/clipboard/clipboard_util_mac.mmcontent/browser/renderer_host/clipboard_host_impl.ccui/base/clipboard/clipboard_mac.mmui/base/clipboard/clipboard_util.cccontent/browser/file_system/browser_file_system_helper.cccontent/browser/file_system_access/file_system_access_manager_impl.cccontent/browser/child_process_security_policy_impl.ccthird_party/blink/public/mojom/clipboard/clipboard.mojom
Estimated timestamp from git blame: 2026-03-22
Vulnerability Detail
There is a potential logic flaw in how Chromium handles clipboard operations on macOS, allowing a compromised renderer to bypass the sandbox and read arbitrary local files. The vulnerability stems from a lack of validation in ClipboardHost.WriteBookmark combined with automatic file promotion logic in the macOS clipboard implementation.
The attack leverages two distinct phases:
1. Write Phase (Staging the File)
- A compromised renderer calls the
ClipboardHost.WriteBookmarkMojo IPC method with afile://URL pointing to a sensitive file (e.g.,file:///Users/victim/.ssh/id_rsa). ClipboardHostImpl::WriteBookmark(content/browser/renderer_host/clipboard_host_impl.cc:623) receives this string and passes it directly toclipboard_writer_->WriteURL()without validating the scheme or verifying if the renderer has permission to access the file.- On macOS, the write operation eventually reaches
clipboard_util::PasteboardItemsFromUrls(ui/base/clipboard/clipboard_util_mac.mm:326). - Here, the unsandboxed browser process checks if the URL is a file and if it exists (
url.isFileURL && [url checkResourceIsReachableAndReturnError:nil]). Because the browser is unsandboxed, this check succeeds for any existing local file. The browser then promotes the string to anNSPasteboardTypeFileURLon the macOS system pasteboard.
2. Read Phase (Exfiltration)
- The compromised renderer waits for a minor user interaction (e.g., a single click anywhere on the page) to satisfy
IsRendererPasteAllowed(which relies onWebContents::HasRecentInteraction()). - The renderer sends the
ClipboardHost.ReadFiles(kStandard)IPC call. - The browser reads the
NSPasteboardTypeFileURLfrom the system pasteboard. Believing the user genuinely copied a file via the OS UI, the browser blindly grants the renderer process read access viaChildProcessSecurityPolicyImpl::GrantReadFile. - A File System Access
DataTransferTokenis generated and returned to the renderer. - The renderer redeems this token (which bypasses sensitive directory checks in
ResolveDataTransferTokenWithFileTypebecause it is a file handle, not a directory) and reads the file contents, completing the sandbox escape.
Potential Reproduction Steps
Note: These are suggested/potential steps to trigger the vulnerability. Our tooling agent does not currently have the ability to run code or execute a live proof-of-concept.
- From a compromised renderer process on macOS, initiate a Mojo IPC call:
ClipboardHost.WriteBookmark("file:///Users/<user>/.ssh/id_rsa", "title"). - Send a
ClipboardHost.CommitWrite()IPC message to flush the URL to the OS clipboard. - Pause execution and wait for any user interaction (e.g., an arbitrary click on the page surface) to satisfy the 5-second
HasRecentInteraction()window. - Send a
ClipboardHost.ReadFiles(kStandard)IPC call. - Receive the Mojo response containing a
DataTransferFileentry with a validfile_system_access_token. - Redeem the token via the File System Access API (e.g.,
getFile()) to read the raw contents of the user’s SSH key.
Suggested Fix
- Input Validation: In
ClipboardHostImpl::WriteBookmark, explicitly validate the URL scheme. If the URL is afile://scheme, the browser must check if the renderer process already has read access to that specific file path viaChildProcessSecurityPolicyImpl. If it does not, the request should be rejected or the process terminated. - Policy Enforcement: Consider routing
WriteBookmarkthroughIsClipboardCopyAllowedByPolicyto align it withWriteText,WriteHtml, andWriteImage. - macOS Clipboard Hardening: In
clipboard_util::PasteboardItemsFromUrls, avoid promotingfile://URLs toNSPasteboardTypeFileURLwhen the data originates from an untrusted web origin.
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
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.