Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in DataTransfer
DescriptionMemory corruption in DataTransfer
ComponentDataTransfer
Sub-componentBlink>DataTransfer
Bug ClassMemory Corruption
Tracker553150456
Fix commit4d730c8e7b35 (chromium/src) +206/-15
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
chrome/browser/glic/host/glic_drag_and_drop_util.cc
modified
if
content/browser/renderer_host/data_transfer_util.cc
modified
for
content/browser/renderer_host/data_transfer_util.cc
modified
for
content/browser/renderer_host/render_widget_host_impl.cc
modified
if
content/browser/renderer_host/render_widget_host_impl.cc
modified
TEST_F
content/browser/renderer_host/render_widget_host_unittest.cc
modified
for
content/browser/renderer_host/render_widget_host_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/render_widget_host_unittest.cc
  • content/browser/web_contents/web_contents_impl_unittest.cc
  • content/public/common/drop_data.h
  • chrome/browser/glic/host/glic_drag_and_drop_util.cc
  • content/browser/renderer_host/render_widget_host_impl.cc
  • content/browser/web_contents/web_contents_impl.cc
  • content/browser/renderer_host/data_transfer_util.cc
From 4d730c8e7b35e5723be179cfa45bc7fa9bd53a73 Mon Sep 17 00:00:00 2001
From: Victor Miura <[email protected]>
Date: Mon, 21 Sep 2026 18:09:25 -0700
Subject: [PATCH] Discard drag-source-supplied drag id and prevent egress leakage

The "chromium/x-drag-id" custom data entry links an in-progress
drag back to the WebContents it started from. It is assigned in
WebContentsImpl::OnStartDragging(), which overwrites any value
already present - except on file-only drags, which return early
before the id is assigned, so an entry provided by the drag
source was kept and later attributed the drag to whichever
WebContents that id named.

Filter the entry out in DragDataToDropData() instead, so that
incoming drag data can never populate it and the browser-side
assignment remains its only source regardless of which path
OnStartDragging() takes. Share a single constant for the key
between the code that assigns it and the code that consumes it,
and add drag start filtering tests for the entry.

Additionally, filter out the entry in DropDataToDragData() and
DropDataToMetaData() so that this internal tracking token is
never exposed across the IPC boundary to untrusted drop target
renderers.

Bug: 553150456
Change-Id: I737be9eac1dcd6aa42fc4499ece75faf6c321251
Fixed: 553150456
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8433161
Reviewed-by: Nick Birnie <[email protected]>
Auto-Submit: Victor Miura <[email protected]>
Reviewed-by: Alex Moshchuk <[email protected]>
Commit-Queue: Alex Moshchuk <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1702279}
---
diff --git a/chrome/browser/glic/host/glic_drag_and_drop_util.cc b/chrome/browser/glic/host/glic_drag_and_drop_util.cc
index e315170..f19986be 100644
--- a/chrome/browser/glic/host/glic_drag_and_drop_util.cc
+++ b/chrome/browser/glic/host/glic_drag_and_drop_util.cc
@@ -34,8 +34,6 @@
 
 namespace {
 
-const char16_t kGlicDragIdKey[] = u"chromium/x-drag-id";
-
 // Constructs the `glic::mojom::AdditionalContext` containing the dropped data
 // and the retrieved page context of the originating source tab.
 // Needed to bundle all relevant multimodal web content (raw file bytes,
@@ -242,7 +240,7 @@
 }  // namespace
 
 bool IsGlicWebDrag(const content::DropData& drop_data) {
-  return drop_data.custom_data.contains(kGlicDragIdKey);
+  return drop_data.custom_data.contains(content::kDragIdCustomDataKey);
 }
 
 // Initiates the Glic drag-and-drop invocation workflow when a web drag is
@@ -282,8 +280,8 @@
   }
 
   std::optional<base::UnguessableToken> raw_drag_id =
-      base::UnguessableToken::DeserializeFromString(
-          base::UTF16ToUTF8(drop_data.custom_data.at(kGlicDragIdKey)));
+      base::UnguessableToken::DeserializeFromString(base::UTF16ToUTF8(
+          drop_data.custom_data.at(content::kDragIdCustomDataKey)));
   if (!raw_drag_id) {
     base::UmaHistogramEnumeration(
         "Glic.DragAndDrop.ValidationResult",
diff --git a/content/browser/renderer_host/data_transfer_util.cc b/content/browser/renderer_host/data_transfer_util.cc
index 5caa3f3a..00a9eaba 100644
--- a/content/browser/renderer_host/data_transfer_util.cc
+++ b/content/browser/renderer_host/data_transfer_util.cc
@@ -268,6 +268,12 @@
   }
   for (const std::pair<const std::u16string, std::u16string>& data :
        drop_data.custom_data) {
+    // The drag id entry is assigned by the browser for internal tracking
+    // (e.g. Glic drag attribution); never expose it to untrusted drop target
+    // renderers.
+    if (data.first == kDragIdCustomDataKey) {
+      continue;
+    }
     blink::mojom::DragItemStringPtr item = blink::mojom::DragItemString::New();
     item->string_type = base::UTF16ToUTF8(data.first);
     item->string_data = data.second;
@@ -302,6 +308,9 @@
 
   for (const auto& meta_data_item : drop_meta_data) {
     if (meta_data_item.kind == DropData::Kind::STRING) {
+      if (meta_data_item.mime_type == kDragIdCustomDataKey) {
+        continue;
+      }
       blink::mojom::DragItemStringPtr item =
           blink::mojom::DragItemString::New();
       item->string_type = base::UTF16ToUTF8(meta_data_item.mime_type);
@@ -399,9 +408,14 @@
           if (string_item->base_url)
             result.html_base_url = *string_item->base_url;
         } else {
-          result.custom_data.emplace(
-              base::UTF8ToUTF16(string_item->string_type),
-              string_item->string_data);
+          std::u16string custom_type =
+              base::UTF8ToUTF16(string_item->string_type);
+          // The drag id entry is assigned by the browser when a drag starts;
+          // discard any value supplied with the incoming drag data.
+          if (custom_type != kDragIdCustomDataKey) {
+            result.custom_data.emplace(std::move(custom_type),
+                                       string_item->string_data);
+          }
         }
         break;
       }
diff --git a/content/browser/renderer_host/render_widget_host_impl.cc b/content/browser/renderer_host/render_widget_host_impl.cc
index 7a8f170a..13461134 100644
--- a/content/browser/renderer_host/render_widget_host_impl.cc
+++ b/content/browser/renderer_host/render_widget_host_impl.cc
@@ -298,6 +298,9 @@
   }
 
   for (const auto& custom_data_item : drop_data.custom_data) {
+    if (custom_data_item.first == kDragIdCustomDataKey) {
+      continue;
+    }
     metadata.push_back(DropData::Metadata::CreateForMimeType(
         DropData::Kind::STRING, custom_data_item.first));
   }
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index 7af34859..5132122 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -8132,7 +8132,7 @@
 
   active_drag_id_ = drag_id;
   SetDragSource(drag_id, source_rfh_token);
-  drop_data->custom_data[u"chromium/x-drag-id"] =
+  drop_data->custom_data[kDragIdCustomDataKey] =
       base::ASCIIToUTF16(drag_id.value().ToString());
 }
 
diff --git a/content/public/common/drop_data.h b/content/public/common/drop_data.h
index 0ffa04a7..58830001 100644
--- a/content/public/common/drop_data.h
+++ b/content/public/common/drop_data.h
@@ -27,6 +27,13 @@
 
 namespace content {
 
+// Key of the DropData::custom_data entry holding the id that links an
+// in-progress drag back to the WebContents it started from. This entry is
+// assigned by the browser when a drag starts (see
+// `WebContentsImpl::OnStartDragging()`); values supplied by the drag source
+// are discarded (see `DragDataToDropData()`).
+inline constexpr char16_t kDragIdCustomDataKey[] = u"chromium/x-drag-id";
+
 struct CONTENT_EXPORT DownloadUrlMetadata {
   DownloadUrlMetadata();
   ~DownloadUrlMetadata();
@@ -67,7 +74,7 @@
     LAST = BINARY
   };
 
-  struct Metadata {
+  struct CONTENT_EXPORT Metadata {
     static Metadata CreateForMimeType(Kind kind,
                                       const std::u16string& mime_type);
     static Metadata CreateForFilePath(
diff --git a/content/browser/renderer_host/render_widget_host_unittest.cc b/content/browser/renderer_host/render_widget_host_unittest.cc
index 50f291cd..c728a07 100644
--- a/content/browser/renderer_host/render_widget_host_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_unittest.cc
@@ -16,6 +16,7 @@
 #include "base/location.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/run_until.h"
@@ -23,6 +24,7 @@
 #include "base/test/simple_test_tick_clock.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
+#include "base/unguessable_token.h"
 #include "build/build_config.h"
 #include "cc/mojom/render_frame_metadata.mojom.h"
 #include "cc/trees/render_frame_metadata.h"
@@ -94,7 +96,6 @@
 #include "ui/gfx/canvas.h"
 
 #if BUILDFLAG(IS_ANDROID)
-#include "base/strings/utf_string_conversions.h"
 #include "content/browser/renderer_host/render_widget_host_view_android.h"
 #include "ui/android/screen_android.h"
 #endif
@@ -2689,6 +2690,174 @@
             FILE_PATH_LITERAL("payload.so"));
 }
 
+// The "chromium/x-drag-id" custom data entry is assigned by the browser when
+// a drag starts. A value supplied by the drag source must be discarded, also
+// on file-only drags, which do not receive a browser-assigned id.
+TEST_F(RenderWidgetHostDragTest, DragIdCustomDataDiscardedOnFileOnlyDrag) {
+  NavigateAndCommit(GURL("https://example.com"));
+  EXPECT_EQ(start_dragging_count(), 0);
+
+  auto drag_data = blink::mojom::DragData::New();
+  auto file_item = blink::mojom::DataTransferFile::New();
+  file_item->path = base::FilePath(FILE_PATH_LITERAL("/path/to/file.txt"));
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewFile(std::move(file_item)));
+  auto id_item = blink::mojom::DragItemString::New();
+  id_item->string_type = base::UTF16ToUTF8(kDragIdCustomDataKey);
+  id_item->string_data = u"11223344556677889900aabbccddeeff";
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewString(std::move(id_item)));
+
+  StartDragWithDragData(std::move(drag_data));
+
+  EXPECT_EQ(start_dragging_count(), 1);
+  EXPECT_FALSE(drop_data().custom_data.contains(kDragIdCustomDataKey));
+}
+
+// On drags that are stamped with a drag id, a "chromium/x-drag-id" value
+// supplied by the drag source must be replaced with a browser-assigned one.
+TEST_F(RenderWidgetHostDragTest, DragIdCustomDataReplacedOnTextDrag) {
+  NavigateAndCommit(GURL("https://example.com"));
+  EXPECT_EQ(start_dragging_count(), 0);
+
+  auto drag_data = blink::mojom::DragData::New();
+  auto text_item = blink::mojom::DragItemString::New();
+  text_item->string_type = ui::kMimeTypePlainText;
+  text_item->string_data = u"dragged text";
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewString(std::move(text_item)));
+  auto id_item = blink::mojom::DragItemString::New();
+  id_item->string_type = base::UTF16ToUTF8(kDragIdCustomDataKey);
+  id_item->string_data = u"11223344556677889900aabbccddeeff";
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewString(std::move(id_item)));
+
+  StartDragWithDragData(std::move(drag_data));
+
+  EXPECT_EQ(start_dragging_count(), 1);
+  auto it = drop_data().custom_data.find(kDragIdCustomDataKey);
+  ASSERT_NE(it, drop_data().custom_data.end());
+  EXPECT_NE(it->second, u"11223344556677889900aabbccddeeff");
+  EXPECT_TRUE(base::UnguessableToken::DeserializeFromString(
+                  base::UTF16ToASCII(it->second))
+                  .has_value());
+}
+
+// Custom data types other than "chromium/x-drag-id" pass through unmodified.
+TEST_F(RenderWidgetHostDragTest, OtherCustomDataPreservedOnFileOnlyDrag) {
+  NavigateAndCommit(GURL("https://example.com"));
+  EXPECT_EQ(start_dragging_count(), 0);
+
+  auto drag_data = blink::mojom::DragData::New();
+  auto file_item = blink::mojom::DataTransferFile::New();
+  file_item->path = base::FilePath(FILE_PATH_LITERAL("/path/to/file.txt"));
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewFile(std::move(file_item)));
+  auto custom_item = blink::mojom::DragItemString::New();
+  custom_item->string_type = "application/x-example-type";
+  custom_item->string_data = u"example data";
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewString(std::move(custom_item)));
+
+  StartDragWithDragData(std::move(drag_data));
+
+  EXPECT_EQ(start_dragging_count(), 1);
+  auto it = drop_data().custom_data.find(u"application/x-example-type");
+  ASSERT_NE(it, drop_data().custom_data.end());
+  EXPECT_EQ(it->second, u"example data");
+  // File-only drags are not stamped with a drag id.
+  EXPECT_FALSE(drop_data().custom_data.contains(kDragIdCustomDataKey));
+}
+
+// Internal drag id in DropData must never be exposed to untrusted drop target
+// renderers in outgoing DragData.
+TEST_F(RenderWidgetHostDragTest, DragIdNotExposedInOutgoingDragData) {
+  DropData drop_data;
+  drop_data.custom_data[kDragIdCustomDataKey] =
+      u"11223344556677889900aabbccddeeff";
+  drop_data.custom_data[u"application/x-custom-key"] = u"custom-value";
+
+  blink::mojom::DragDataPtr drag_data =
+      DropDataToDragData(drop_data, GetFileSystemAccessManager(),
+                         main_test_rfh()->GetProcess()->GetDeprecatedID(),
+                         GetChromeBlobStorageContext());
+
+  ASSERT_TRUE(drag_data);
+  bool found_custom_key = false;
+  for (const auto& item : drag_data->items) {
+    if (item->is_string()) {
+      EXPECT_NE(item->get_string()->string_type,
+                base::UTF16ToUTF8(kDragIdCustomDataKey));
+      if (item->get_string()->string_type == "application/x-custom-key") {
+        found_custom_key = true;
+        EXPECT_EQ(item->get_string()->string_data, u"custom-value");
+      }
+    }
+  }
+  EXPECT_TRUE(found_custom_key);
+}
+
+// Internal drag id in DropData must never be exposed to renderers in dragenter
+// metadata.
+TEST_F(RenderWidgetHostDragTest, DragIdNotExposedInDragEnterMetaData) {
+  mojo::PendingAssociatedReceiver<blink::mojom::FrameWidget> receiver =
+      BindFakeFrameWidgetInterfaces(main_test_rfh());
+  DragCaptureFrameWidget mock_frame_widget(std::move(receiver));
+
+  DropData drop_data;
+  drop_data.custom_data[kDragIdCustomDataKey] =
+      u"11223344556677889900aabbccddeeff";
+  drop_data.custom_data[u"application/x-custom-key"] = u"custom-value";
+
+  base::RunLoop run_loop;
+  GetRenderWidgetHost()->DragTargetDragEnter(
+      drop_data, gfx::PointF(), gfx::PointF(),
+      blink::DragOperationsMask::kDragOperationEvery, 0,
+      base::BindOnce(
+          [](base::OnceClosure quit_closure, ui::mojom::DragOperation operation,
+             bool document_is_handling_drag) { std::move(quit_closure).Run(); },
+          run_loop.QuitClosure()));
+  run_loop.Run();
+
+  const auto& captured_drag_data = mock_frame_widget.drag_data();
+  ASSERT_TRUE(captured_drag_data);
+  bool found_custom_key = false;
+  for (const auto& item : captured_drag_data->items) {
+    if (item->is_string()) {
+      EXPECT_NE(item->get_string()->string_type,
+                base::UTF16ToUTF8(kDragIdCustomDataKey));
+      if (item->get_string()->string_type == "application/x-custom-key") {
+        found_custom_key = true;
+      }
+    }
+  }
+  EXPECT_TRUE(found_custom_key);
+}
+
+// Internal drag id in DropData::Metadata must never be exposed to renderers
+// in DropMetaDataToDragData().
+TEST_F(RenderWidgetHostDragTest, DragIdNotExposedInDropMetaDataToDragData) {
+  std::vector<DropData::Metadata> metadata;
+  metadata.push_back(DropData::Metadata::CreateForMimeType(
+      DropData::Kind::STRING, kDragIdCustomDataKey));
+  metadata.push_back(DropData::Metadata::CreateForMimeType(
+      DropData::Kind::STRING, u"application/x-custom-key"));
+
+  blink::mojom::DragDataPtr drag_data = DropMetaDataToDragData(metadata);
+  ASSERT_TRUE(drag_data);
+  bool found_custom_key = false;
+  for (const auto& item : drag_data->items) {
+    if (item->is_string()) {
+      EXPECT_NE(item->get_string()->string_type,
+                base::UTF16ToUTF8(kDragIdCustomDataKey));
+      if (item->get_string()->string_type == "application/x-custom-key") {
+        found_custom_key = true;
+      }
+    }
+  }
+  EXPECT_TRUE(found_custom_key);
+}
+
 TEST_F(RenderWidgetHostDragTest, DragEnterDoesNotLeakPaths) {
   // Bind our mock frame widget.
   mojo::PendingAssociatedReceiver<blink::mojom::FrameWidget> receiver =
diff --git a/content/browser/web_contents/web_contents_impl_unittest.cc b/content/browser/web_contents/web_contents_impl_unittest.cc
index 39322c18..d28ab6d 100644
--- a/content/browser/web_contents/web_contents_impl_unittest.cc
+++ b/content/browser/web_contents/web_contents_impl_unittest.cc
@@ -4383,7 +4383,7 @@
 
   contents()->OnStartDragging(&drop_data, source_rfh_token);
 
-  auto it = drop_data.custom_data.find(u"chromium/x-drag-id");
+  auto it = drop_data.custom_data.find(kDragIdCustomDataKey);
   ASSERT_NE(it, drop_data.custom_data.end())
       << "drag_id should be added to custom_data when file_contents is "
          "present.";
@@ -4412,7 +4412,7 @@
 
   contents()->OnStartDragging(&drop_data, GlobalRenderFrameHostToken());
 
-  EXPECT_EQ(drop_data.custom_data.find(u"chromium/x-drag-id"),
+  EXPECT_EQ(drop_data.custom_data.find(kDragIdCustomDataKey),
             drop_data.custom_data.end())
       << "No drag_id should be created if file_contents is empty.";
 }
@@ -4427,14 +4427,14 @@
       base::ToVector(base::byte_span_from_cstring("content 1"));
   contents()->OnStartDragging(&data1, source_rfh_token);
   base::UnguessableToken id1 = *base::UnguessableToken::DeserializeFromString(
-      base::UTF16ToASCII(data1.custom_data[u"chromium/x-drag-id"]));
+      base::UTF16ToASCII(data1.custom_data[kDragIdCustomDataKey]));
 
   DropData data2;
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/render_widget_host_unittest.cc b/content/browser/renderer_host/render_widget_host_unittest.cc
index 50f291cd..c728a07 100644
--- a/content/browser/renderer_host/render_widget_host_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_unittest.cc
@@ -16,6 +16,7 @@
 #include "base/location.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
+#include "base/strings/utf_string_conversions.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/run_until.h"
@@ -23,6 +24,7 @@
 #include "base/test/simple_test_tick_clock.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
+#include "base/unguessable_token.h"
 #include "build/build_config.h"
 #include "cc/mojom/render_frame_metadata.mojom.h"
 #include "cc/trees/render_frame_metadata.h"
@@ -94,7 +96,6 @@
 #include "ui/gfx/canvas.h"
 
 #if BUILDFLAG(IS_ANDROID)
-#include "base/strings/utf_string_conversions.h"
 #include "content/browser/renderer_host/render_widget_host_view_android.h"
 #include "ui/android/screen_android.h"
 #endif
@@ -2689,6 +2690,174 @@
             FILE_PATH_LITERAL("payload.so"));
 }
 
+// The "chromium/x-drag-id" custom data entry is assigned by the browser when
+// a drag starts. A value supplied by the drag source must be discarded, also
+// on file-only drags, which do not receive a browser-assigned id.
+TEST_F(RenderWidgetHostDragTest, DragIdCustomDataDiscardedOnFileOnlyDrag) {
+  NavigateAndCommit(GURL("https://example.com"));
+  EXPECT_EQ(start_dragging_count(), 0);
+
+  auto drag_data = blink::mojom::DragData::New();
+  auto file_item = blink::mojom::DataTransferFile::New();
+  file_item->path = base::FilePath(FILE_PATH_LITERAL("/path/to/file.txt"));
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewFile(std::move(file_item)));
+  auto id_item = blink::mojom::DragItemString::New();
+  id_item->string_type = base::UTF16ToUTF8(kDragIdCustomDataKey);
+  id_item->string_data = u"11223344556677889900aabbccddeeff";
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewString(std::move(id_item)));
+
+  StartDragWithDragData(std::move(drag_data));
+
+  EXPECT_EQ(start_dragging_count(), 1);
+  EXPECT_FALSE(drop_data().custom_data.contains(kDragIdCustomDataKey));
+}
+
+// On drags that are stamped with a drag id, a "chromium/x-drag-id" value
+// supplied by the drag source must be replaced with a browser-assigned one.
+TEST_F(RenderWidgetHostDragTest, DragIdCustomDataReplacedOnTextDrag) {
+  NavigateAndCommit(GURL("https://example.com"));
+  EXPECT_EQ(start_dragging_count(), 0);
+
+  auto drag_data = blink::mojom::DragData::New();
+  auto text_item = blink::mojom::DragItemString::New();
+  text_item->string_type = ui::kMimeTypePlainText;
+  text_item->string_data = u"dragged text";
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewString(std::move(text_item)));
+  auto id_item = blink::mojom::DragItemString::New();
+  id_item->string_type = base::UTF16ToUTF8(kDragIdCustomDataKey);
+  id_item->string_data = u"11223344556677889900aabbccddeeff";
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewString(std::move(id_item)));
+
+  StartDragWithDragData(std::move(drag_data));
+
+  EXPECT_EQ(start_dragging_count(), 1);
+  auto it = drop_data().custom_data.find(kDragIdCustomDataKey);
+  ASSERT_NE(it, drop_data().custom_data.end());
+  EXPECT_NE(it->second, u"11223344556677889900aabbccddeeff");
+  EXPECT_TRUE(base::UnguessableToken::DeserializeFromString(
+                  base::UTF16ToASCII(it->second))
+                  .has_value());
+}
+
+// Custom data types other than "chromium/x-drag-id" pass through unmodified.
+TEST_F(RenderWidgetHostDragTest, OtherCustomDataPreservedOnFileOnlyDrag) {
+  NavigateAndCommit(GURL("https://example.com"));
+  EXPECT_EQ(start_dragging_count(), 0);
+
+  auto drag_data = blink::mojom::DragData::New();
+  auto file_item = blink::mojom::DataTransferFile::New();
+  file_item->path = base::FilePath(FILE_PATH_LITERAL("/path/to/file.txt"));
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewFile(std::move(file_item)));
+  auto custom_item = blink::mojom::DragItemString::New();
+  custom_item->string_type = "application/x-example-type";
+  custom_item->string_data = u"example data";
+  drag_data->items.push_back(
+      blink::mojom::DragItem::NewString(std::move(custom_item)));
+
+  StartDragWithDragData(std::move(drag_data));
+
+  EXPECT_EQ(start_dragging_count(), 1);
+  auto it = drop_data().custom_data.find(u"application/x-example-type");
+  ASSERT_NE(it, drop_data().custom_data.end());
+  EXPECT_EQ(it->second, u"example data");
+  // File-only drags are not stamped with a drag id.
+  EXPECT_FALSE(drop_data().custom_data.contains(kDragIdCustomDataKey));
+}
+
+// Internal drag id in DropData must never be exposed to untrusted drop target
+// renderers in outgoing DragData.
+TEST_F(RenderWidgetHostDragTest, DragIdNotExposedInOutgoingDragData) {
+  DropData drop_data;
+  drop_data.custom_data[kDragIdCustomDataKey] =
+      u"11223344556677889900aabbccddeeff";
+  drop_data.custom_data[u"application/x-custom-key"] = u"custom-value";
+
+  blink::mojom::DragDataPtr drag_data =
+      DropDataToDragData(drop_data, GetFileSystemAccessManager(),
+                         main_test_rfh()->GetProcess()->GetDeprecatedID(),
+                         GetChromeBlobStorageContext());
+
+  ASSERT_TRUE(drag_data);
+  bool found_custom_key = false;
+  for (const auto& item : drag_data->items) {
+    if (item->is_string()) {
+      EXPECT_NE(item->get_string()->string_type,
+                base::UTF16ToUTF8(kDragIdCustomDataKey));
+      if (item->get_string()->string_type == "application/x-custom-key") {
+        found_custom_key = true;
+        EXPECT_EQ(item->get_string()->string_data, u"custom-value");
+      }
+    }
+  }
+  EXPECT_TRUE(found_custom_key);
+}
+
+// Internal drag id in DropData must never be exposed to renderers in dragenter
+// metadata.
+TEST_F(RenderWidgetHostDragTest, DragIdNotExposedInDragEnterMetaData) {
+  mojo::PendingAssociatedReceiver<blink::mojom::FrameWidget> receiver =
+      BindFakeFrameWidgetInterfaces(main_test_rfh());
+  DragCaptureFrameWidget mock_frame_widget(std::move(receiver));
+
+  DropData drop_data;
+  drop_data.custom_data[kDragIdCustomDataKey] =
+      u"11223344556677889900aabbccddeeff";
+  drop_data.custom_data[u"application/x-custom-key"] = u"custom-value";
+
+  base::RunLoop run_loop;
+  GetRenderWidgetHost()->DragTargetDragEnter(
+      drop_data, gfx::PointF(), gfx::PointF(),
+      blink::DragOperationsMask::kDragOperationEvery, 0,
+      base::BindOnce(
+          [](base::OnceClosure quit_closure, ui::mojom::DragOperation operation,
+             bool document_is_handling_drag) { std::move(quit_closure).Run(); },
+          run_loop.QuitClosure()));
+  run_loop.Run();
+
+  const auto& captured_drag_data = mock_frame_widget.drag_data();
+  ASSERT_TRUE(captured_drag_data);
+  bool found_custom_key = false;
+  for (const auto& item : captured_drag_data->items) {
+    if (item->is_string()) {
+      EXPECT_NE(item->get_string()->string_type,
+                base::UTF16ToUTF8(kDragIdCustomDataKey));
+      if (item->get_string()->string_type == "application/x-custom-key") {
+        found_custom_key = true;
+      }
+    }
+  }
+  EXPECT_TRUE(found_custom_key);
+}
+
+// Internal drag id in DropData::Metadata must never be exposed to renderers
+// in DropMetaDataToDragData().
+TEST_F(RenderWidgetHostDragTest, DragIdNotExposedInDropMetaDataToDragData) {
+  std::vector<DropData::Metadata> metadata;
+  metadata.push_back(DropData::Metadata::CreateForMimeType(
+      DropData::Kind::STRING, kDragIdCustomDataKey));
+  metadata.push_back(DropData::Metadata::CreateForMimeType(
+      DropData::Kind::STRING, u"application/x-custom-key"));
+
+  blink::mojom::DragDataPtr drag_data = DropMetaDataToDragData(metadata);
+  ASSERT_TRUE(drag_data);
+  bool found_custom_key = false;
+  for (const auto& item : drag_data->items) {
+    if (item->is_string()) {
+      EXPECT_NE(item->get_string()->string_type,
+                base::UTF16ToUTF8(kDragIdCustomDataKey));
+      if (item->get_string()->string_type == "application/x-custom-key") {
+        found_custom_key = true;
+      }
+    }
+  }
+  EXPECT_TRUE(found_custom_key);
+}
+
 TEST_F(RenderWidgetHostDragTest, DragEnterDoesNotLeakPaths) {
   // Bind our mock frame widget.
   mojo::PendingAssociatedReceiver<blink::mojom::FrameWidget> receiver =
diff --git a/content/browser/web_contents/web_contents_impl_unittest.cc b/content/browser/web_contents/web_contents_impl_unittest.cc
index 39322c18..d28ab6d 100644
--- a/content/browser/web_contents/web_contents_impl_unittest.cc
+++ b/content/browser/web_contents/web_contents_impl_unittest.cc
@@ -4383,7 +4383,7 @@
 
   contents()->OnStartDragging(&drop_data, source_rfh_token);
 
-  auto it = drop_data.custom_data.find(u"chromium/x-drag-id");
+  auto it = drop_data.custom_data.find(kDragIdCustomDataKey);
   ASSERT_NE(it, drop_data.custom_data.end())
       << "drag_id should be added to custom_data when file_contents is "
          "present.";
@@ -4412,7 +4412,7 @@
 
   contents()->OnStartDragging(&drop_data, GlobalRenderFrameHostToken());
 
-  EXPECT_EQ(drop_data.custom_data.find(u"chromium/x-drag-id"),
+  EXPECT_EQ(drop_data.custom_data.find(kDragIdCustomDataKey),
             drop_data.custom_data.end())
       << "No drag_id should be created if file_contents is empty.";
 }
@@ -4427,14 +4427,14 @@
       base::ToVector(base::byte_span_from_cstring("content 1"));
   contents()->OnStartDragging(&data1, source_rfh_token);
   base::UnguessableToken id1 = *base::UnguessableToken::DeserializeFromString(
-      base::UTF16ToASCII(data1.custom_data[u"chromium/x-drag-id"]));
+      base::UTF16ToASCII(data1.custom_data[kDragIdCustomDataKey]));
 
   DropData data2;
   data2.file_contents =
       base::ToVector(base::byte_span_from_cstring("content 2"));
   contents()->OnStartDragging(&data2, source_rfh_token);
   base::UnguessableToken id2 = *base::UnguessableToken::DeserializeFromString(
-      base::UTF16ToASCII(data2.custom_data[u"chromium/x-drag-id"]));
+      base::UTF16ToASCII(data2.custom_data[kDragIdCustomDataKey]));
 
   EXPECT_NE(id1, id2);
   EXPECT_EQ(WebContents::FromDragId(contents()->GetBrowserContext(),
Loading diff…

Original Bug Report

reported by [email protected]

Drag ID forgery via is_pure_file_drag early-return in WebContentsImpl

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: When starting a drag operation, WebContentsImpl::OnStartDragging evaluates is_pure_file_drag on raw data before security filtering and returns early without overwriting or sanitizing chromium/x-drag-id in custom data. An attacker page or compromised renderer can attach a file item alongside a harvested drag ID from a victim tab to bypass drag ID overwrite logic. When dropped onto the Glic (Gemini) panel, the browser resolves the forged ID to the victim tab and fetches its full annotated page context into the AI conversation.

Affected files:

  • content/browser/web_contents/web_contents_impl.cc
  • content/browser/renderer_host/render_widget_host_impl.cc
  • content/browser/renderer_host/data_transfer_util.cc
  • chrome/browser/glic/host/glic_drag_and_drop_util.cc
  • chrome/browser/ui/tab_contents/chrome_web_contents_view_handle_drop.cc
  • third_party/blink/renderer/core/clipboard/data_object.cc

Estimated timestamp from git blame: 2026-06-18

Summary

There is a potential authorization and provenance forgery vulnerability in Chromium’s drag-and-drop subsystem affecting Glic (Gemini in Chrome). In WebContentsImpl::OnStartDragging, a pure file drag check (is_pure_file_drag) causes an early return that bypasses the overwriting of internal drag identification metadata (chromium/x-drag-id). This allows a web page or compromised renderer that previously received a victim tab’s drag ID to replay that token on a crafted file drag, causing drops on the Glic panel to misattribute the source and extract cross-origin page context from the victim tab.

Note: The reproduction steps and impact analysis described below are potential steps identified via static code analysis, as our analysis environment cannot execute live browser binaries.


Root Cause Analysis

  1. Pre-filter Delegate Call in RenderWidgetHostImpl::StartDragging: In content/browser/renderer_host/render_widget_host_impl.cc:3028-3032, renderer-provided DragData is converted to DropData and passed directly to the delegate before ChildProcessSecurityPolicy filtering:

    DropData drop_data = DragDataToDropData(*drag_data);
    if (delegate_) {
      delegate_->OnStartDragging(&drop_data, source_rfh.GetGlobalFrameToken());
    }
    DropData filtered_data(drop_data);
    
  2. Early Return Bypasses Drag ID Stamping: In content/browser/web_contents/web_contents_impl.cc:8006-8022, WebContentsImpl::OnStartDragging inspects the unfiltered fields:

    bool is_pure_file_drag =
        !drop_data->filenames.empty() && !drop_data->text.has_value() &&
        !drop_data->html.has_value() && drop_data->file_contents.empty();
    
    if (is_pure_file_drag) {
      return;
    }
    
    DragId drag_id(base::UnguessableToken::Create());
    DragSourceDocumentTracker::GetOrCreateForCurrentDocument(source_frame)
        ->AddDragId(drag_id);
    active_drag_id_ = drag_id;
    SetDragSource(drag_id, source_rfh_token);
    drop_data->custom_data[u"chromium/x-drag-id"] =
        base::ASCIIToUTF16(drag_id.value().ToString());
    

    If is_pure_file_drag is true, the function returns immediately. Any renderer-supplied entry for chromium/x-drag-id in custom_data is neither erased nor overwritten with a fresh token.

  3. Custom Data Preservation & Token Exposure:

    • In content/browser/renderer_host/data_transfer_util.cc:401-405, DragDataToDropData places unrecognized string types directly into custom_data.
    • In content/browser/renderer_host/data_transfer_util.cc:269-275, DropDataToDragData forwards all entries in custom_data to drop targets, allowing any web page receiving a drag from a victim tab to read chromium/x-drag-id via standard JS event.dataTransfer.getData('chromium/x-drag-id').
  4. Misattribution in Glic: When dropped on the Glic guest web contents (chrome/browser/ui/tab_contents/chrome_web_contents_view_handle_drop.cc:257-262), standard enterprise content scanning is bypassed and glic::StartDragAndDropInvoke is called. content::WebContents::FromDragId (content/browser/web_contents/web_contents_impl.cc:856-874) resolves the replayed token via DragSourceMap (which remains valid for 10 seconds post-drag) and DragSourceDocumentTracker back to the victim tab. Glic then initiates FetchPageContext(source_tab, {annotated_page_content: true}) (chrome/browser/glic/host/glic_drag_and_drop_util.cc:331-342), attaching the victim tab’s full DOM structure, title, and page text to the AI session.


Potential Attack Scenario & Steps to Reproduce

  1. Harvest Victim Drag ID:

    • The user drags content (e.g., an image or link) from a sensitive tab (Tab $V$) and drops it onto an attacker page (Tab $A$) in the same browser profile.
    • Tab $A$’s drop handler reads and stores the drag ID string: const victimDragId = event.dataTransfer.getData('chromium/x-drag-id');.
  2. Construct Forged Pure File Drag:

    • Within 10 seconds (before the delayed cleanup in WebContentsImpl::OnDragSourceEnded removes the ID), Tab $A$ prepares a drag operation containing a valid file (e.g., from an <input type="file"> or File System Access API) and sets the custom data key:
      event.dataTransfer.items.add(pickedFile);
      event.dataTransfer.setData('chromium/x-drag-id', victimDragId);
      
    • (Variant for compromised renderer): The compromised renderer can invoke LocalFrameHost::StartDragging with a fake file path and the harvested chromium/x-drag-id string item.
  3. Bypass Stamping & Drop on Glic:

    • Because drop_data->filenames is non-empty and text/html/file_contents are empty, is_pure_file_drag evaluates to true in WebContentsImpl::OnStartDragging, returning early without overwriting the key.
    • The user drops the payload onto the Glic side panel.
    • StartDragAndDropInvoke resolves victimDragId to Tab $V$ and fetches Tab $V$’s annotated page content, sending it into the Glic conversation.

Impact

  • Cross-Origin Data Disclosure: An attacker can potentially steal the full page content (including private text, structure, and metadata) of another tab in the same profile by luring the user to complete two drag-and-drop gestures.
  • Enterprise Scanning Bypass: Enterprise Cloud Content Analysis / deep scanning is skipped for Glic drops because the payload is assumed to be an authenticated browser drag.

Suggested Fix

  1. Sanitize Reserved Keys: Explicitly sanitize internal keys like u"chromium/x-drag-id" from custom_data when entering the browser via DragDataToDropData or RenderWidgetHostImpl::StartDragging, ensuring renderer processes cannot provide them.
  2. Ensure Unconditional Stripping/Stamping: In WebContentsImpl::OnStartDragging, ensure drop_data->custom_data.erase(u"chromium/x-drag-id") is called even when is_pure_file_drag is true.
  3. Filter Outgoing Custom Data: In DropDataToDragData, do not expose chromium/x-drag-id to untrusted drop target renderers.

Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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