CVE-2026-78991
Overview
Files Changed
content/browser/renderer_host/clipboard_host_impl.cc
Patch
From 0f84a3df80b9ec4ba4774c4a66ecc00cce39bcd3 Mon Sep 17 00:00:00 2001 From: Dominique Fauteux-Chapleau <[email protected]> Date: Wed, 15 Jul 2026 12:51:37 -0700 Subject: [PATCH] Capture clipboard sequence number at start of read in ClipboardHostImpl Previously, `ClipboardHostImpl` retrieved the clipboard sequence number inside `PasteIfPolicyAllowed` after asynchronous read operations (such as reading PNGs, files, or custom formats) completed. If a clipboard write occurred while an async read was in flight, `PasteIfPolicyAllowed` would use the newer sequence number rather than the token associated with the read data, creating a potential race condition for enterprise policy checks. This change fixes the race condition by capturing the `ui::ClipboardSequenceNumberToken` at the start of clipboard read operations in `ClipboardHostImpl` and passing it through the async callbacks into `PasteIfPolicyAllowed`. Bug: 502888857 Change-Id: I6d493ddefe50c051af1cab911dec687a9d513bab Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8099582 Reviewed-by: Avi Drissman <[email protected]> Commit-Queue: Dominique Fauteux-Chapleau <[email protected]> Cr-Commit-Position: refs/heads/main@{#1662801} --- diff --git a/content/browser/renderer_host/clipboard_host_impl.cc b/content/browser/renderer_host/clipboard_host_impl.cc index dc1dd4f..9738c912 100644 --- a/content/browser/renderer_host/clipboard_host_impl.cc +++ b/content/browser/renderer_host/clipboard_host_impl.cc @@ -252,20 +252,24 @@ return; } - ExtractText(clipboard_buffer, CreateDataEndpoint(render_frame_host()), - base::BindOnce(&ClipboardHostImpl::OnReadText, - weak_ptr_factory_.GetWeakPtr(), clipboard_buffer, - std::move(callback))); + ExtractText( + clipboard_buffer, CreateDataEndpoint(render_frame_host()), + base::BindOnce(&ClipboardHostImpl::OnReadText, + weak_ptr_factory_.GetWeakPtr(), clipboard_buffer, + ui::Clipboard::GetForCurrentThread()->GetSequenceNumber( + clipboard_buffer), + std::move(callback))); } void ClipboardHostImpl::OnReadText(ui::ClipboardBuffer clipboard_buffer, + ui::ClipboardSequenceNumberToken seqno, ReadTextCallback callback, std::u16string text) { ClipboardPasteData clipboard_paste_data; clipboard_paste_data.text = std::move(text); PasteIfPolicyAllowed( - clipboard_buffer, ui::ClipboardFormatType::PlainTextType(), + clipboard_buffer, ui::ClipboardFormatType::PlainTextType(), seqno, std::move(clipboard_paste_data), base::BindOnce( [](ReadTextCallback callback, @@ -289,13 +293,17 @@ } ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread(); auto data_dst = CreateDataEndpoint(render_frame_host()); - clipboard->ReadHTML(clipboard_buffer, data_dst, - base::BindOnce(&ClipboardHostImpl::OnReadHtml, - weak_ptr_factory_.GetWeakPtr(), - clipboard_buffer, std::move(callback))); + clipboard->ReadHTML( + clipboard_buffer, data_dst, + base::BindOnce(&ClipboardHostImpl::OnReadHtml, + weak_ptr_factory_.GetWeakPtr(), clipboard_buffer, + ui::Clipboard::GetForCurrentThread()->GetSequenceNumber( + clipboard_buffer), + std::move(callback))); } void ClipboardHostImpl::OnReadHtml(ui::ClipboardBuffer clipboard_buffer, + ui::ClipboardSequenceNumberToken seqno, ReadHtmlCallback callback, std::u16string markup, GURL src_url, @@ -305,7 +313,7 @@ clipboard_paste_data.html = std::move(markup); PasteIfPolicyAllowed( - clipboard_buffer, ui::ClipboardFormatType::HtmlType(), + clipboard_buffer, ui::ClipboardFormatType::HtmlType(), seqno, std::move(clipboard_paste_data), base::BindOnce( [](GURL src_url, uint32_t fragment_start, uint32_t fragment_end, @@ -335,17 +343,20 @@ clipboard_buffer, data_dst, base::BindOnce(&ClipboardHostImpl::OnReadSvg, weak_ptr_factory_.GetWeakPtr(), clipboard_buffer, + ui::Clipboard::GetForCurrentThread()->GetSequenceNumber( + clipboard_buffer), std::move(callback))); } void ClipboardHostImpl::OnReadSvg(ui::ClipboardBuffer clipboard_buffer, + ui::ClipboardSequenceNumberToken seqno, ReadSvgCallback callback, std::u16string svg) { ClipboardPasteData clipboard_paste_data; clipboard_paste_data.svg = std::move(svg); PasteIfPolicyAllowed( - clipboard_buffer, ui::ClipboardFormatType::SvgType(), + clipboard_buffer, ui::ClipboardFormatType::SvgType(), seqno, std::move(clipboard_paste_data), base::BindOnce( [](ReadSvgCallback callback, @@ -373,17 +384,20 @@ clipboard_buffer, data_dst, base::BindOnce(&ClipboardHostImpl::OnReadRtf, weak_ptr_factory_.GetWeakPtr(), clipboard_buffer, + ui::Clipboard::GetForCurrentThread()->GetSequenceNumber( + clipboard_buffer), std::move(callback))); } void ClipboardHostImpl::OnReadRtf(ui::ClipboardBuffer clipboard_buffer, + ui::ClipboardSequenceNumberToken seqno, ReadRtfCallback callback, std::string rtf) { ClipboardPasteData clipboard_paste_data; clipboard_paste_data.rtf = std::move(rtf); PasteIfPolicyAllowed( - clipboard_buffer, ui::ClipboardFormatType::RtfType(), + clipboard_buffer, ui::ClipboardFormatType::RtfType(), seqno, std::move(clipboard_paste_data), base::BindOnce( [](ReadRtfCallback callback, @@ -410,30 +424,35 @@ clipboard_buffer, data_dst, base::BindOnce(&ClipboardHostImpl::OnReadPng, weak_ptr_factory_.GetWeakPtr(), clipboard_buffer, + ui::Clipboard::GetForCurrentThread()->GetSequenceNumber( + clipboard_buffer), std::move(callback))); } void ClipboardHostImpl::OnReadPng(ui::ClipboardBuffer clipboard_buffer, + ui::ClipboardSequenceNumberToken seqno, ReadPngCallback callback, const std::vector<uint8_t>& data) { // Pass both image and associated text for content analysis. ExtractText(clipboard_buffer, CreateDataEndpoint(render_frame_host()), base::BindOnce(&ClipboardHostImpl::OnReadPngWithText, weak_ptr_factory_.GetWeakPtr(), clipboard_buffer, - std::move(callback), std::move(data))); + seqno, std::move(callback), std::move(data))); } -void ClipboardHostImpl::OnReadPngWithText(ui::ClipboardBuffer clipboard_buffer, - ReadPngCallback callback, - std::vector<uint8_t> data, - std::u16string text) { +void ClipboardHostImpl::OnReadPngWithText( + ui::ClipboardBuffer clipboard_buffer, + ui::ClipboardSequenceNumberToken seqno, + ReadPngCallback callback, + std::vector<uint8_t> data, + std::u16string text) { // Pass both image and associated text for content analysis. ClipboardPasteData clipboard_paste_data; clipboard_paste_data.text = std::move(text); clipboard_paste_data.png = std::move(data); PasteIfPolicyAllowed( - clipboard_buffer, ui::ClipboardFormatType::PngType(), + clipboard_buffer, ui::ClipboardFormatType::PngType(), seqno, std::move(clipboard_paste_data), base::BindOnce( [](ReadPngCallback callback, @@ -463,10 +482,13 @@ clipboard_buffer, data_dst, base::BindOnce(&ClipboardHostImpl::OnReadFiles, weak_ptr_factory_.GetWeakPtr(), clipboard_buffer, + ui::Clipboard::GetForCurrentThread()->GetSequenceNumber( + clipboard_buffer), std::move(callback))); } void ClipboardHostImpl::OnReadFiles(ui::ClipboardBuffer clipboard_buffer, + ui::ClipboardSequenceNumberToken seqno, ReadFilesCallback callback, std::vector<ui::FileInfo> filenames) { blink::mojom::ClipboardFilesPtr result = blink::mojom::ClipboardFiles::New(); @@ -503,7 +525,7 @@ std::move(files.begin(), files.end(), std::back_inserter(result->files)); PasteIfPolicyAllowed( - clipboard_buffer, ui::ClipboardFormatType::FilenamesType(), + clipboard_buffer, ui::ClipboardFormatType::FilenamesType(), seqno, std::move(clipboard_paste_data), base::BindOnce( [](blink::mojom::ClipboardFilesPtr result, ReadFilesCallback callback, @@ -550,12 +572,15 @@ clipboard_buffer, type, data_dst,
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 9c8cb86c..47b94ea 100644
--- a/content/browser/renderer_host/clipboard_host_impl_unittest.cc
+++ b/content/browser/renderer_host/clipboard_host_impl_unittest.cc
@@ -382,8 +382,11 @@
bool is_policy_callback_called = false;
ClipboardHostImpl::ClipboardPasteData clipboard_paste_data;
clipboard_paste_data.text = u"data";
+
fake_clipboard_host_impl_grandchild->PasteIfPolicyAllowed(
ui::ClipboardBuffer::kCopyPaste, ui::ClipboardFormatType::PlainTextType(),
+ ui::Clipboard::GetForCurrentThread()->GetSequenceNumber(
+ ui::ClipboardBuffer::kCopyPaste),
clipboard_paste_data,
base::BindLambdaForTesting(
[&is_policy_callback_called](
@@ -1322,4 +1325,375 @@
EXPECT_EQ(0u, future.Get().size());
}
+class SequenceNumberInterceptBrowserClient : public TestContentBrowserClient {
+ public:
+ SequenceNumberInterceptBrowserClient() = default;
+ ~SequenceNumberInterceptBrowserClient() override = default;
+
+ void IsClipboardPasteAllowedByPolicy(
+ const ClipboardEndpoint& source,
+ const ClipboardEndpoint& destination,
+ const ui::ClipboardMetadata& metadata,
+ ClipboardPasteData data,
+ IsClipboardPasteAllowedCallback callback) override {
+ last_seqno_ = metadata.seqno;
+ std::move(callback).Run(std::move(data));
+ }
+
+ ui::ClipboardSequenceNumberToken last_seqno() const { return last_seqno_; }
+
+ private:
+ ui::ClipboardSequenceNumberToken last_seqno_;
+};
+
+class RaceConditionTestClipboard : public ui::TestClipboard {
+ public:
+ RaceConditionTestClipboard() = default;
+ ~RaceConditionTestClipboard() override = default;
+
+ void SetCallbackOnRead(base::RepeatingClosure callback) {
+ on_read_callback_ = std::move(callback);
+ }
+
+ void ReadText(ui::ClipboardBuffer buffer,
+ const std::optional<ui::DataTransferEndpoint>& data_dst,
+ ReadTextCallback callback) const override {
+ ui::TestClipboard::ReadText(
+ buffer, data_dst,
+ base::BindOnce(
+ [](base::RepeatingClosure on_read_callback,
+ ReadTextCallback callback, std::u16string result) {
+ if (on_read_callback) {
+ on_read_callback.Run();
+ }
+ std::move(callback).Run(std::move(result));
+ },
+ on_read_callback_, std::move(callback)));
+ }
+
+ void ReadHTML(ui::ClipboardBuffer buffer,
+ const std::optional<ui::DataTransferEndpoint>& data_dst,
+ ReadHtmlCallback callback) const override {
+ ui::TestClipboard::ReadHTML(
+ buffer, data_dst,
+ base::BindOnce(
+ [](base::RepeatingClosure on_read_callback,
+ ReadHtmlCallback callback, std::u16string markup, GURL src_url,
+ uint32_t fragment_start, uint32_t fragment_end) {
+ if (on_read_callback) {
+ on_read_callback.Run();
+ }
+ std::move(callback).Run(std::move(markup), std::move(src_url),
+ fragment_start, fragment_end);
+ },
+ on_read_callback_, std::move(callback)));
+ }
+
+ void ReadSvg(ui::ClipboardBuffer buffer,
+ const std::optional<ui::DataTransferEndpoint>& data_dst,
+ ReadSvgCallback callback) const override {
+ ui::TestClipboard::ReadSvg(
+ buffer, data_dst,
+ base::BindOnce(
+ [](base::RepeatingClosure on_read_callback,
+ ReadSvgCallback callback, std::u16string svg) {
+ if (on_read_callback) {
+ on_read_callback.Run();
+ }
+ std::move(callback).Run(std::move(svg));
+ },
+ on_read_callback_, std::move(callback)));
+ }
+
+ void ReadRTF(ui::ClipboardBuffer buffer,
+ const std::optional<ui::DataTransferEndpoint>& data_dst,
+ ReadRTFCallback callback) const override {
+ ui::TestClipboard::ReadRTF(
+ buffer, data_dst,
+ base::BindOnce(
+ [](base::RepeatingClosure on_read_callback,
+ ReadRTFCallback callback, std::string rtf) {
+ if (on_read_callback) {
+ on_read_callback.Run();
+ }
+ std::move(callback).Run(std::move(rtf));
+ },
+ on_read_callback_, std::move(callback)));
+ }
+
+ void ReadPng(ui::ClipboardBuffer buffer,
+ const std::optional<ui::DataTransferEndpoint>& data_dst,
+ ReadPngCallback callback) const override {
+ ui::TestClipboard::ReadPng(
+ buffer, data_dst,
+ base::BindOnce(
+ [](base::RepeatingClosure on_read_callback,
+ ReadPngCallback callback, const std::vector<uint8_t>& data) {
+ std::vector<uint8_t> png_copy = data;
+ if (on_read_callback) {
+ on_read_callback.Run();
+ }
+ std::move(callback).Run(std::move(png_copy));
+ },
+ on_read_callback_, std::move(callback)));
+ }
+
+ void ReadFilenames(ui::ClipboardBuffer buffer,
+ const std::optional<ui::DataTransferEndpoint>& data_dst,
+ ReadFilenamesCallback callback) const override {
+ ui::TestClipboard::ReadFilenames(
+ buffer, data_dst,
+ base::BindOnce(
+ [](base::RepeatingClosure on_read_callback,
+ ReadFilenamesCallback callback,
+ std::vector<ui::FileInfo> filenames) {
+ if (on_read_callback) {
+ on_read_callback.Run();
+ }
+ std::move(callback).Run(std::move(filenames));
+ },
+ on_read_callback_, std::move(callback)));
+ }
+
+ void ReadDataTransferCustomData(
+ ui::ClipboardBuffer buffer,
+ const std::u16string& type,
+ const std::optional<ui::DataTransferEndpoint>& data_dst,
+ ReadDataTransferCustomDataCallback callback) const override {
+ ui::TestClipboard::ReadDataTransferCustomData(
+ buffer, type, data_dst,
+ base::BindOnce(
+ [](base::RepeatingClosure on_read_callback,
+ ReadDataTransferCustomDataCallback callback,
+ std::u16string result) {
+ if (on_read_callback) {
+ on_read_callback.Run();
+ }
+ std::move(callback).Run(std::move(result));
+ },
+ on_read_callback_, std::move(callback)));
+ }
+
+ void ReadData(const ui::ClipboardFormatType& format,
+ const std::optional<ui::DataTransferEndpoint>& data_dst,
+ ReadDataCallback callback) const override {
+ ui::TestClipboard::ReadData(
+ format, data_dst,
+ base::BindOnce(
+ [](base::RepeatingClosure on_read_callback,
+ ReadDataCallback callback, std::string result) {
+ if (on_read_callback) {
+ on_read_callback.Run();
+ }
+ std::move(callback).Run(std::move(result));
+ },
+ on_read_callback_, std::move(callback)));
+ }
+
+ private:
+ base::RepeatingClosure on_read_callback_;
+};
+
+class ClipboardHostImplRaceConditionTest : public ClipboardHostImplTest {
+ protected:
+ void SetUp() override {
+ ClipboardHostImplTest::SetUp();
+ browser_client_setting_ =
+ std::make_unique<ScopedContentBrowserClientSetting>(&browser_client_);
+
+ ui::Clipboard::DestroyClipboardForCurrentThread();
+ auto test_clipboard = std::make_unique<RaceConditionTestClipboard>();
+ test_clipboard_ = test_clipboard.get();
+ ui::Clipboard::SetClipboardForCurrentThread(std::move(test_clipboard));
+ }
+
+ void TearDown() override {
+ test_clipboard_ = nullptr;
+ DeleteAndRecreateClipboard();
+ browser_client_setting_.reset();
+ ClipboardHostImplTest::TearDown();
+ }
+
+ SequenceNumberInterceptBrowserClient& browser_client() {
+ return browser_client_;
+ }
+
+ RaceConditionTestClipboard* test_clipboard() { return test_clipboard_; }
+
+ private:
+ SequenceNumberInterceptBrowserClient browser_client_;
+ std::unique_ptr<ScopedContentBrowserClientSetting> browser_client_setting_;
+ raw_ptr<RaceConditionTestClipboard> test_clipboard_ = nullptr;
+};
+
+TEST_F(ClipboardHostImplRaceConditionTest, ReadTextUsesCapturedSequenceNumber) {
+ {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteText(u"initial text");
+ }
+
+ ui::ClipboardSequenceNumberToken expected_seqno =
+ test_clipboard()->GetSequenceNumber(ui::ClipboardBuffer::kCopyPaste);
+
+ test_clipboard()->SetCallbackOnRead(base::BindLambdaForTesting([]() {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteText(u"Benign text to increment sequence number");
+ }));
+
+ std::u16string result;
+ mojo_clipboard()->ReadText(ui::ClipboardBuffer::kCopyPaste, &result);
+
+ EXPECT_EQ(browser_client().last_seqno(), expected_seqno);
+ EXPECT_EQ(result, u"initial text");
+}
+
+TEST_F(ClipboardHostImplRaceConditionTest, ReadHtmlUsesCapturedSequenceNumber) {
+ {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteHTML(u"<b>html</b>", "https://example.com");
+ }
+
+ ui::ClipboardSequenceNumberToken expected_seqno =
+ test_clipboard()->GetSequenceNumber(ui::ClipboardBuffer::kCopyPaste);
+
+ test_clipboard()->SetCallbackOnRead(base::BindLambdaForTesting([]() {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteText(u"Benign text to increment sequence number");
+ }));
+
+ std::u16string markup;
+ GURL url;
+ uint32_t start = 0;
+ uint32_t end = 0;
+ mojo_clipboard()->ReadHtml(ui::ClipboardBuffer::kCopyPaste, &markup, &url,
+ &start, &end);
+
+ EXPECT_EQ(browser_client().last_seqno(), expected_seqno);
+ EXPECT_EQ(markup, u"<b>html</b>");
+}
+
+TEST_F(ClipboardHostImplRaceConditionTest, ReadSvgUsesCapturedSequenceNumber) {
+ {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteSvg(u"<svg></svg>");
+ }
+
+ ui::ClipboardSequenceNumberToken expected_seqno =
+ test_clipboard()->GetSequenceNumber(ui::ClipboardBuffer::kCopyPaste);
+
+ test_clipboard()->SetCallbackOnRead(base::BindLambdaForTesting([]() {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteText(u"Benign text to increment sequence number");
+ }));
+
+ base::test::TestFuture<const std::u16string&> future;
+ mojo_clipboard()->ReadSvg(ui::ClipboardBuffer::kCopyPaste,
+ future.GetCallback());
+ ASSERT_TRUE(future.Wait());
+
+ EXPECT_EQ(browser_client().last_seqno(), expected_seqno);
+ EXPECT_EQ(future.Get(), u"<svg></svg>");
+}
+
+TEST_F(ClipboardHostImplRaceConditionTest, ReadRtfUsesCapturedSequenceNumber) {
+ {
+ ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
+ writer.WriteRTF("{\\rtf1\\ansi}");
+ }
+
+ ui::ClipboardSequenceNumberToken expected_seqno =
+ test_clipboard()->GetSequenceNumber(ui::ClipboardBuffer::kCopyPaste);
+
+ test_clipboard()->SetCallbackOnRead(base::BindLambdaForTesting([]() {
... (truncated)
Original Bug Report
Potential DLP bypass via TOCTOU in Enterprise paste caching
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 Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A race condition in Chrome’s clipboard handling allows a compromised renderer to bypass Enterprise Data Loss Prevention (DLP) checks. An attacker can write benign data to the clipboard while a slow image read is occurring, priming a cache that subsequently allows the sensitive image data through uninspected.
Affected files:
chrome/browser/enterprise/data_protection/paste_allowed_request.cccontent/browser/renderer_host/clipboard_host_impl.cccontent/public/browser/clipboard_types.cc
Estimated timestamp from git blame: 2024-04-26
Summary
A Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists in Chrome’s clipboard processing that can allow a compromised renderer to bypass Enterprise Data Loss Prevention (DLP) content analysis when reading images from the clipboard.
The issue stems from the fact that reading an image from the clipboard (e.g., via ReadPng) is an asynchronous operation that may involve slow background-thread encoding. However, the sequence number used to cache the DLP policy decision is fetched after this asynchronous work finishes. A malicious renderer can exploit this window to modify the clipboard content, triggering a fast, benign policy check that caches an “allowed” verdict. When the slow image encoding finally completes, it re-uses this newly cached “allowed” verdict, permitting the sensitive image data to be delivered without inspection.
Detailed Analysis
- Initiating a Slow Read: The user copies a confidential image (e.g., a bitmap) to the OS clipboard. The clipboard is currently at sequence number
S1. A compromised renderer callsnavigator.clipboard.read()or uses direct Mojo access to send aReadPngmessage to the browser. - Background Processing:
ClipboardHostImpl::ReadPnginitiates a read from the OS clipboard. On Windows (features::kNonBlockingOsClipboardReads) or standard Non-Backed clipboards, reading a bitmap and converting it to PNG format involves dispatching a time-consuming task to a backgroundbase::ThreadPool(ui/base/clipboard/clipboard_win.cc:814). The UI thread does not block and continues processing Mojo messages. - The Race (Writing Benign Data): While the background encoding task is running, the renderer sends a
WriteSmartPasteMarkermessage followed by aCommitWritemessage. UnlikeWriteText, these specific write methods do not trigger asynchronous policy checks and immediately commit to the clipboard (content/browser/renderer_host/clipboard_host_impl.cc:597). This updates the OS clipboard state and increments the global sequence number toS2. - Priming the Cache: The renderer immediately sends a
ReadTextmessage. Because the clipboard now only contains a smart paste marker,ExtractTextquickly returns an empty string. The browser proceeds to check DLP policies for this empty text by callingPasteIfPolicyAllowed(content/browser/renderer_host/clipboard_host_impl.cc:833). - Caching the Benign Verdict:
PasteIfPolicyAllowedcaptures the current OS sequence number, which is nowS2. It initiates an Enterprise Content Analysis request. The check concludes that an empty string is safe and callsPasteAllowedRequest::Complete(chrome/browser/enterprise/data_protection/paste_allowed_request.cc:153). This populates the cache inRequestsMapStoragewith an “allowed” entry for sequence numberS2containing empty data. - The Bypass: Eventually, the original background task encoding the confidential PNG completes. Execution resumes on the UI thread at
ClipboardHostImpl::OnReadPngWithText(content/browser/renderer_host/clipboard_host_impl.cc:401), which bundles the sensitive PNG data into aClipboardPasteDataobject. - Re-fetching the Sequence Number:
OnReadPngWithTextcallsPasteIfPolicyAllowed. Crucially, this function callsGetSequenceNumberagain. Since the clipboard hasn’t changed since step 3, it receivesS2. - Data Merging:
PasteAllowedRequest::StartPasteAllowedRequestis called with the sensitive PNG data and the sequence numberS2. It finds the completed, “allowed” cache entry from step 5. Because the entry is marked complete, it skips the DLP scan and immediately callsrequest.InvokeCallback(...). - Delivery: Inside
InvokeCallback, the incoming sensitive data is merged with the cached data (data.Merge(data_)). Because the cached data consists of empty fields, theMergefunction (content/public/browser/clipboard_types.cc:41) leaves the sensitive PNG data intact. The callback delivers the confidential image directly to the attacker’s renderer.
Impact
A compromised renderer or malicious web application with clipboard read permissions could reliably extract confidential images or other data types from the clipboard, bypassing active Enterprise DLP content analysis policies.
Suggested Fix
The root cause is that PasteIfPolicyAllowed retrieves the clipboard sequence number at the end of the read pipeline, rather than capturing the sequence number that was associated with the data when it was originally extracted from the OS clipboard.
The ui::Clipboard::ReadPng (and related async read functions) should be modified to return the sequence number that was current when the read began. This captured sequence number should then be passed down the callback chain to ClipboardHostImpl::OnReadPng -> ExtractText -> OnReadPngWithText -> PasteIfPolicyAllowed. This ensures the DLP cache key strictly matches the generation of the data being analyzed.
Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09
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.
Raised in root component due to access or custom field issues on 1208119