Medium watch CVSS 6.3 chrome OOB 🔧 Commit mapped

Overview

Medium
Severity
6.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in DataTransfer
DescriptionOut of bounds read in DataTransfer
ComponentDataTransfer
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker520521197
Fix commit847020c23229 (chromium/src) +112/-8
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-06-05 (per vendor advisory)
Fix landed2026-07-02 (public git)
Disclosed2026-09-17
Patch gap77 days — fix public in source before the advisory

Background

`IDataObject` / `STGMEDIUM`
the Windows COM interface (and its medium struct) through which clipboard and drag-and-drop payloads are handed to Chrome; the data source may be an untrusted external application.
`HGLOBAL` / `base::win::ScopedHGlobal`
a movable global memory handle holding the payload bytes; ScopedHGlobal locks it and exposes data.data() (a raw pointer to the bytes) and data.size() (the allocation size from GlobalSize).
NUL-scanning string constructors
helpers like base::WideToUTF16, base::UTF8ToUTF16, base::as_u16cstr, and base::SysNativeMBToWide accept a bare pointer and read forward until they hit a NUL terminator, so their length is dictated by buffer contents rather than by the known allocation size.
`CF_HTML` hardened path
the text/html reader was already length-bounded, and this patch generalizes that same discipline to the remaining text/URL/filename readers.

Root Cause Analysis

The Windows clipboard/drag readers in clipboard_util_win.cc locked the payload HGLOBAL and passed the raw data.data() pointer directly into NUL-scanning string constructors (base::WideToUTF16(data.data()), base::UTF8ToUTF16(data.data()), base::as_u16cstr(data.data()), base::SysNativeMBToWide(data.data()), and a direct std::wstring push). These constructors determine length by scanning for a NUL terminator, but the size of the payload is known independently as data.size() (the GlobalSize of the allocation). A hostile source implementing IDataObject::GetData can return an HGLOBAL whose bytes contain no NUL terminator within the allocation, so the scan runs past the end of the buffer, reading adjacent browser-process heap.

The fix treats data.size() as authoritative: two new helpers, HGlobalAsWideString and HGlobalAsString, construct the string from exactly data.size() bytes and then truncate at the first embedded NUL, and the four filename sites do the same bounding inline via data.size() / sizeof(wchar_t) and find before resizing. Because the constructed string can never exceed the allocation, the out-of-bounds scan is eliminated.

Key insight
The single mistake was trusting the payload to be NUL-terminated and letting a scanning constructor decide the string length, when the true length was already known from GlobalSize. The fix inverts the priority — read exactly data.size() bytes and only then cut at the first NUL — so buffer contents can no longer drive the read past the allocation.

Attack Path

  1. Craft a malicious data source An attacker provides an IDataObject (via clipboard paste or a drag-and-drop into the page) whose GetData returns an HGLOBAL allocated to hold text/URL/filename bytes with no NUL terminator inside the allocation, as the added MockUnterminatedTextDataObject test demonstrates.
  2. Chrome reads the payload When the page triggers a DataTransfer read, a reader such as clipboard_util::GetPlainText locks the HGLOBAL and calls a NUL-scanning constructor on data.data().
  3. Scan runs off the end With no NUL in the allocation, the constructor reads past data.size() into adjacent browser-process heap until it happens upon a NUL byte, producing an out-of-bounds read (CWE-125).
  4. Overread contents surface to the page The over-long string is returned as the pasted/dropped text, URL, or filename and becomes readable by the web page’s JavaScript, disclosing adjacent heap memory (inferred as the disclosure channel from the advisory’s “leaking adjacent browser-process heap to a web page”).

Impact Assessment

An attacker who can place a hostile IDataObject on the clipboard or into a drag-and-drop operation gains an out-of-bounds read of adjacent browser-process heap, whose contents can be reflected back to a web page through DataTransfer text/URL/filename fields. The precondition is a user paste or drop originating from attacker-controlled data. The advisory classifies this as an out-of-bounds read in DataTransfer; any use of the leaked bytes to defeat ASLR or chain into a stronger primitive is not shown by the patch and is (inferred) at most, and there is no exploited-in-the-wild claim in the provided material.

Changed Functions

FunctionChangeNotes
MockUnterminatedTextDataObject
ui/base/clipboard/clipboard_util_win_unittest.cc
modified
if
ui/base/clipboard/clipboard_util_win_unittest.cc
modified
TEST_F
ui/base/clipboard/clipboard_util_win_unittest.cc
modified

Files Changed

  • ui/base/clipboard/clipboard_util_win.cc
  • ui/base/clipboard/clipboard_util_win_unittest.cc

Audit Directions

  • Pointer-only reads of sized buffers
    Flag any site that passes a bare data.data() (or other raw pointer into an externally sized allocation) into a NUL-scanning constructor while data.size()/GlobalSize is available but unused.
  • Untrusted `IDataObject`/OS payload parsing
    Review every clipboard, drag-and-drop, and shell-integration reader that assumes external HGLOBAL/STGMEDIUM contents are NUL-terminated, well-formed, or self-describing in length.
  • Inconsistent hardening across sibling paths
    When one format handler (here CF_HTML) is bounded but its neighbors are not, audit the whole family; the safe pattern should be centralized in a shared helper so no branch is left reading past its allocation.
From 847020c23229e1e26859ad337e146fea868d9c45 Mon Sep 17 00:00:00 2001
From: Rohan Raja <[email protected]>
Date: Wed, 01 Jul 2026 17:05:32 -0700
Subject: [PATCH] Bound Windows clipboard/drag text reads by HGLOBAL GlobalSize

The Windows clipboard/drag readers in clipboard_util_win.cc built
strings directly from a locked HGLOBAL pointer (data.data()) and passed
it to string / UTF constructors that NUL-scan to find the end. An
untrusted IDataObject payload that omits a NUL terminator therefore
drives the scan past the allocation, leaking adjacent browser-process
heap to a web page (CWE-125- out-of-bounds read).

Treat GlobalSize() as authoritative: bound every text/URL/filename read
by data.size() and truncate at the first embedded NUL, mirroring the
already hardened CF_HTML path. Two centralized helpers,
HGlobalAsWideString and HGlobalAsString, cover the six text/URL sites
(handling the byte->char width conversion and the null-lock check); the
four filename sites are bounded inline.

Bug: 520521197
Change-Id: I3a21d4428296572451c2efcecfdc35ee62d44308
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8016986
Commit-Queue: Rohan Raja <[email protected]>
Reviewed-by: Jesse McKenna <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1655827}
---
diff --git a/ui/base/clipboard/clipboard_util_win.cc b/ui/base/clipboard/clipboard_util_win.cc
index e553c8b4..63858ba0 100644
--- a/ui/base/clipboard/clipboard_util_win.cc
+++ b/ui/base/clipboard/clipboard_util_win.cc
@@ -50,6 +50,37 @@
 constexpr STGMEDIUM kNullStorageMedium = {.tymed = TYMED_NULL,
                                           .pUnkForRelease = nullptr};
 
+// Returns the locked HGLOBAL contents as UTF-16, bounded by size() and
+// truncated at the first embedded NUL. Untrusted external sources may omit a
+// NUL terminator, so size() is authoritative; reading via data() alone would
+// scan past the allocation.
+std::u16string HGlobalAsWideString(base::win::ScopedHGlobal<wchar_t*>& data) {
+  if (!data.data()) {
+    return std::u16string();
+  }
+  std::u16string out = base::WideToUTF16(
+      std::wstring_view(data.data(), data.size() / sizeof(wchar_t)));
+  if (size_t pos = out.find(u'\0'); pos != std::u16string::npos) {
+    out.resize(pos);
+  }
+  return out;
+}
+
+// Returns the locked HGLOBAL contents as UTF-16, decoding the bytes as UTF-8
+// and bounded by size(), truncated at the first embedded NUL. Untrusted
+// external sources may omit a NUL terminator, so size() is authoritative;
+// reading via data() alone would scan past the allocation.
+std::u16string HGlobalAsString(base::win::ScopedHGlobal<char*>& data) {
+  if (!data.data()) {
+    return std::u16string();
+  }
+  std::string out(data.data(), data.size());
+  if (size_t pos = out.find('\0'); pos != std::string::npos) {
+    out.resize(pos);
+  }
+  return base::UTF8ToUTF16(out);
+}
+
 // Result type for virtual file extraction: pairs of (temp_file_path,
 // display_name).
 using VirtualFileResults =
@@ -732,7 +763,7 @@
   if (GetData(data_object, ClipboardFormatType::BookmarkListType(), &store)) {
     {
       base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
-      ParseBookmarkListData(base::WideToUTF16(data.data()), url_infos);
+      ParseBookmarkListData(HGlobalAsWideString(data), url_infos);
     }
     ReleaseStgMedium(&store);
     return !url_infos.empty();
@@ -745,7 +776,7 @@
       GetData(data_object, ClipboardFormatType::UrlType(), &store)) {
     {
       base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
-      SplitUrlAndTitle(base::WideToUTF16(data.data()), &url, &title);
+      SplitUrlAndTitle(HGlobalAsWideString(data), &url, &title);
       url_infos.emplace_back(url, title);
     }
     ReleaseStgMedium(&store);
@@ -756,7 +787,7 @@
   if (GetData(data_object, ClipboardFormatType::UrlAType(), &store)) {
     {
       base::win::ScopedHGlobal<char*> data(store.hGlobal);
-      SplitUrlAndTitle(base::UTF8ToUTF16(data.data()), &url, &title);
+      SplitUrlAndTitle(HGlobalAsString(data), &url, &title);
       url_infos.emplace_back(url, title);
     }
     ReleaseStgMedium(&store);
@@ -822,7 +853,9 @@
       // filename using Unicode
       base::win::ScopedHGlobal<wchar_t*> data(medium.hGlobal);
       if (data.data() && data.data()[0]) {
-        filenames->push_back(data.data());
+        std::wstring filename(data.data(), data.size() / sizeof(wchar_t));
+        filename.resize(std::min(filename.size(), filename.find(L'\0')));
+        filenames->push_back(std::move(filename));
       }
     }
     ReleaseStgMedium(&medium);
@@ -834,7 +867,9 @@
       // filename using ASCII
       base::win::ScopedHGlobal<char*> data(medium.hGlobal);
       if (data.data() && data.data()[0]) {
-        filenames->push_back(base::SysNativeMBToWide(data.data()));
+        std::string mb(data.data(), data.size());
+        mb.resize(std::min(mb.size(), mb.find('\0')));
+        filenames->push_back(base::SysNativeMBToWide(mb));
       }
     }
     ReleaseStgMedium(&medium);
@@ -1041,7 +1076,7 @@
     {
       // Unicode text
       base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
-      plain_text->assign(base::as_u16cstr(data.data()));
+      plain_text->assign(HGlobalAsWideString(data));
     }
     ReleaseStgMedium(&store);
     return true;
@@ -1051,7 +1086,7 @@
     {
       // ASCII text
       base::win::ScopedHGlobal<char*> data(store.hGlobal);
-      plain_text->assign(base::UTF8ToUTF16(data.data()));
+      plain_text->assign(HGlobalAsString(data));
     }
     ReleaseStgMedium(&store);
     return true;
@@ -1097,7 +1132,7 @@
   {
     // text/html
     base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
-    html->assign(base::as_u16cstr(data.data()));
+    html->assign(HGlobalAsWideString(data));
   }
   ReleaseStgMedium(&store);
   return true;
diff --git a/ui/base/clipboard/clipboard_util_win_unittest.cc b/ui/base/clipboard/clipboard_util_win_unittest.cc
index ecb3ea63..b921b6a 100644
--- a/ui/base/clipboard/clipboard_util_win_unittest.cc
+++ b/ui/base/clipboard/clipboard_util_win_unittest.cc
@@ -11,6 +11,7 @@
 #include "base/containers/span.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
+#include "base/strings/utf_string_conversions.h"
 #include "base/win/scoped_hglobal.h"
 #include "base/win/shlwapi.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -270,6 +271,64 @@
   const unsigned int claimed_items_;
 };
 
+// Serves CF_UNICODETEXT from an HGLOBAL holding the supplied wide text with
+// NO trailing NUL, mimicking an untrusted external source.
+class MockUnterminatedTextDataObject
+    : public Microsoft::WRL::RuntimeClass<
+          Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
+          IDataObject> {
+ public:
+  explicit MockUnterminatedTextDataObject(std::wstring text)
+      : text_(std::move(text)) {}
+
+  IFACEMETHODIMP GetData(FORMATETC* format_etc, STGMEDIUM* medium) override {
+    if (!format_etc || !medium) {
+      return E_INVALIDARG;
+    }
+    if (format_etc->cfFormat == CF_UNICODETEXT) {
+      HGLOBAL hGlobal = ::GlobalAlloc(GHND, text_.size() * sizeof(wchar_t));
+      auto* dst = static_cast<wchar_t*>(::GlobalLock(hGlobal));
+      // SAFETY: `hGlobal` holds exactly `text_.size()` wchar_t elements.
+      UNSAFE_BUFFERS(base::span(dst, text_.size()))
+          .copy_from(base::span(text_));
+      ::GlobalUnlock(hGlobal);
+      medium->tymed = TYMED_HGLOBAL;
+      medium->hGlobal = hGlobal;
+      medium->pUnkForRelease = nullptr;
+      return S_OK;
+    }
+    return DV_E_FORMATETC;
+  }
+
+  IFACEMETHODIMP QueryGetData(FORMATETC* format_etc) override {
+    if (!format_etc) {
+      return E_INVALIDARG;
+    }
+    return format_etc->cfFormat == CF_UNICODETEXT ? S_OK : DV_E_FORMATETC;
+  }
+
+  IFACEMETHODIMP GetDataHere(FORMATETC*, STGMEDIUM*) override {
+    return E_NOTIMPL;
+  }
+  IFACEMETHODIMP GetCanonicalFormatEtc(FORMATETC*, FORMATETC*) override {
+    return E_NOTIMPL;
+  }
+  IFACEMETHODIMP SetData(FORMATETC*, STGMEDIUM*, BOOL) override {
+    return E_NOTIMPL;
+  }
+  IFACEMETHODIMP EnumFormatEtc(DWORD, IEnumFORMATETC**) override {
+    return E_NOTIMPL;
+  }
+  IFACEMETHODIMP DAdvise(FORMATETC*, DWORD, IAdviseSink*, DWORD*) override {
+    return E_NOTIMPL;
+  }
+  IFACEMETHODIMP DUnadvise(DWORD) override { return E_NOTIMPL; }
+  IFACEMETHODIMP EnumDAdvise(IEnumSTATDATA**) override { return E_NOTIMPL; }
+
+ private:
+  std::wstring text_;
+};
+
 using ClipboardUtilWinTest = PlatformTest;
 
 TEST_F(ClipboardUtilWinTest, EmptyHtmlToCFHtml) {
@@ -399,5 +458,15 @@
   EXPECT_FALSE(filenames.has_value());
 }
 
+TEST_F(ClipboardUtilWinTest, GetPlainTextBoundsToHGlobalSize) {
+  const std::wstring text(64, L'a');
+  Microsoft::WRL::ComPtr<IDataObject> data_object =
+      Microsoft::WRL::Make<MockUnterminatedTextDataObject>(text);
+
+  std::u16string out;
+  ASSERT_TRUE(clipboard_util::GetPlainText(data_object.Get(), &out));
+  EXPECT_EQ(out, base::WideToUTF16(text));
+}
+
 }  // namespace
 }  // namespace ui
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/base/clipboard/clipboard_util_win_unittest.cc b/ui/base/clipboard/clipboard_util_win_unittest.cc
index ecb3ea63..b921b6a 100644
--- a/ui/base/clipboard/clipboard_util_win_unittest.cc
+++ b/ui/base/clipboard/clipboard_util_win_unittest.cc
@@ -11,6 +11,7 @@
 #include "base/containers/span.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
+#include "base/strings/utf_string_conversions.h"
 #include "base/win/scoped_hglobal.h"
 #include "base/win/shlwapi.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -270,6 +271,64 @@
   const unsigned int claimed_items_;
 };
 
+// Serves CF_UNICODETEXT from an HGLOBAL holding the supplied wide text with
+// NO trailing NUL, mimicking an untrusted external source.
+class MockUnterminatedTextDataObject
+    : public Microsoft::WRL::RuntimeClass<
+          Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
+          IDataObject> {
+ public:
+  explicit MockUnterminatedTextDataObject(std::wstring text)
+      : text_(std::move(text)) {}
+
+  IFACEMETHODIMP GetData(FORMATETC* format_etc, STGMEDIUM* medium) override {
+    if (!format_etc || !medium) {
+      return E_INVALIDARG;
+    }
+    if (format_etc->cfFormat == CF_UNICODETEXT) {
+      HGLOBAL hGlobal = ::GlobalAlloc(GHND, text_.size() * sizeof(wchar_t));
+      auto* dst = static_cast<wchar_t*>(::GlobalLock(hGlobal));
+      // SAFETY: `hGlobal` holds exactly `text_.size()` wchar_t elements.
+      UNSAFE_BUFFERS(base::span(dst, text_.size()))
+          .copy_from(base::span(text_));
+      ::GlobalUnlock(hGlobal);
+      medium->tymed = TYMED_HGLOBAL;
+      medium->hGlobal = hGlobal;
+      medium->pUnkForRelease = nullptr;
+      return S_OK;
+    }
+    return DV_E_FORMATETC;
+  }
+
+  IFACEMETHODIMP QueryGetData(FORMATETC* format_etc) override {
+    if (!format_etc) {
+      return E_INVALIDARG;
+    }
+    return format_etc->cfFormat == CF_UNICODETEXT ? S_OK : DV_E_FORMATETC;
+  }
+
+  IFACEMETHODIMP GetDataHere(FORMATETC*, STGMEDIUM*) override {
+    return E_NOTIMPL;
+  }
+  IFACEMETHODIMP GetCanonicalFormatEtc(FORMATETC*, FORMATETC*) override {
+    return E_NOTIMPL;
+  }
+  IFACEMETHODIMP SetData(FORMATETC*, STGMEDIUM*, BOOL) override {
+    return E_NOTIMPL;
+  }
+  IFACEMETHODIMP EnumFormatEtc(DWORD, IEnumFORMATETC**) override {
+    return E_NOTIMPL;
+  }
+  IFACEMETHODIMP DAdvise(FORMATETC*, DWORD, IAdviseSink*, DWORD*) override {
+    return E_NOTIMPL;
+  }
+  IFACEMETHODIMP DUnadvise(DWORD) override { return E_NOTIMPL; }
+  IFACEMETHODIMP EnumDAdvise(IEnumSTATDATA**) override { return E_NOTIMPL; }
+
+ private:
+  std::wstring text_;
+};
+
 using ClipboardUtilWinTest = PlatformTest;
 
 TEST_F(ClipboardUtilWinTest, EmptyHtmlToCFHtml) {
@@ -399,5 +458,15 @@
   EXPECT_FALSE(filenames.has_value());
 }
 
+TEST_F(ClipboardUtilWinTest, GetPlainTextBoundsToHGlobalSize) {
+  const std::wstring text(64, L'a');
+  Microsoft::WRL::ComPtr<IDataObject> data_object =
+      Microsoft::WRL::Make<MockUnterminatedTextDataObject>(text);
+
+  std::u16string out;
+  ASSERT_TRUE(clipboard_util::GetPlainText(data_object.Get(), &out));
+  EXPECT_EQ(out, base::WideToUTF16(text));
+}
+
 }  // namespace
 }  // namespace ui
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.