CVE-2026-93376
Overview
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;
ScopedHGloballocks it and exposesdata.data()(a raw pointer to the bytes) anddata.size()(the allocation size fromGlobalSize). - NUL-scanning string constructors
- helpers like
base::WideToUTF16,base::UTF8ToUTF16,base::as_u16cstr, andbase::SysNativeMBToWideaccept 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/htmlreader 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.
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
- Craft a malicious data source
An attacker provides an
IDataObject(via clipboard paste or a drag-and-drop into the page) whoseGetDatareturns anHGLOBALallocated to hold text/URL/filename bytes with no NUL terminator inside the allocation, as the addedMockUnterminatedTextDataObjecttest demonstrates. - Chrome reads the payload
When the page triggers a
DataTransferread, a reader such asclipboard_util::GetPlainTextlocks theHGLOBALand calls a NUL-scanning constructor ondata.data(). - 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). - 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
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
| Function | Change | Notes |
|---|---|---|
MockUnterminatedTextDataObjectui/base/clipboard/clipboard_util_win_unittest.cc |
modified | |
ifui/base/clipboard/clipboard_util_win_unittest.cc |
modified | |
TEST_Fui/base/clipboard/clipboard_util_win_unittest.cc |
modified |
Files Changed
ui/base/clipboard/clipboard_util_win.ccui/base/clipboard/clipboard_util_win_unittest.cc
Audit Directions
- Pointer-only reads of sized buffersFlag any site that passes a bare
data.data()(or other raw pointer into an externally sized allocation) into a NUL-scanning constructor whiledata.size()/GlobalSizeis available but unused. - Untrusted `IDataObject`/OS payload parsingReview every clipboard, drag-and-drop, and shell-integration reader that assumes external
HGLOBAL/STGMEDIUMcontents are NUL-terminated, well-formed, or self-describing in length. - Inconsistent hardening across sibling pathsWhen 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.
Patch
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
Regression Test / PoC
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