Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactConfused deputy in DataTransfer
DescriptionConfused deputy in DataTransfer
ComponentDataTransfer
Bug ClassLogic Error
Tracker500094528
Fix commit4939275abb61 (chromium/src) +29/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

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

Files Changed

  • content/browser/renderer_host/clipboard_host_impl.cc
  • content/browser/renderer_host/clipboard_host_impl.h
  • content/browser/renderer_host/clipboard_host_impl_unittest.cc
From 4939275abb618e309bdbcf4d9931f671eed71825 Mon Sep 17 00:00:00 2001
From: Avi Drissman <[email protected]>
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 <[email protected]>
Reviewed-by: Bryan Oltman <[email protected]>
Commit-Queue: Bryan Oltman <[email protected]>
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);
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 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);
Loading diff…

Original Bug Report

reported by [email protected]

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.mm
  • content/browser/renderer_host/clipboard_host_impl.cc
  • ui/base/clipboard/clipboard_mac.mm
  • ui/base/clipboard/clipboard_util.cc
  • content/browser/file_system/browser_file_system_helper.cc
  • content/browser/file_system_access/file_system_access_manager_impl.cc
  • content/browser/child_process_security_policy_impl.cc
  • third_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.WriteBookmark Mojo IPC method with a file:// 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 to clipboard_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 an NSPasteboardTypeFileURL on 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 on WebContents::HasRecentInteraction()).
  • The renderer sends the ClipboardHost.ReadFiles(kStandard) IPC call.
  • The browser reads the NSPasteboardTypeFileURL from the system pasteboard. Believing the user genuinely copied a file via the OS UI, the browser blindly grants the renderer process read access via ChildProcessSecurityPolicyImpl::GrantReadFile.
  • A File System Access DataTransferToken is generated and returned to the renderer.
  • The renderer redeems this token (which bypasses sensitive directory checks in ResolveDataTransferTokenWithFileType because 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.

  1. From a compromised renderer process on macOS, initiate a Mojo IPC call: ClipboardHost.WriteBookmark("file:///Users/<user>/.ssh/id_rsa", "title").
  2. Send a ClipboardHost.CommitWrite() IPC message to flush the URL to the OS clipboard.
  3. Pause execution and wait for any user interaction (e.g., an arbitrary click on the page surface) to satisfy the 5-second HasRecentInteraction() window.
  4. Send a ClipboardHost.ReadFiles(kStandard) IPC call.
  5. Receive the Mojo response containing a DataTransferFile entry with a valid file_system_access_token.
  6. 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

  1. Input Validation: In ClipboardHostImpl::WriteBookmark, explicitly validate the URL scheme. If the URL is a file:// scheme, the browser must check if the renderer process already has read access to that specific file path via ChildProcessSecurityPolicyImpl. If it does not, the request should be rejected or the process terminated.
  2. Policy Enforcement: Consider routing WriteBookmark through IsClipboardCopyAllowedByPolicy to align it with WriteText, WriteHtml, and WriteImage.
  3. macOS Clipboard Hardening: In clipboard_util::PasteboardItemsFromUrls, avoid promoting file:// URLs to NSPasteboardTypeFileURL when 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.

View on issue tracker