Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Core
DescriptionUse after free in Core
ComponentCore
Sub-componentInternals>Core
Bug ClassUAF
Tracker497026542
Fix commitd89c94536118 (chromium/src) +331/-34
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
for
content/browser/android/drop_data_android.cc
modified
WebContentsViewDragSecurityInfo
content/browser/android/drop_data_android.h
modified
RenderWidgetHostImpl
content/browser/web_contents/web_contents_view_drag_security_info.h
modified
if
content/browser/web_contents/web_contents_view_android_unittest.cc
modified
WebContentsViewAndroidTest
content/browser/web_contents/web_contents_view_android_unittest.cc
modified
TEST_F
content/browser/web_contents/web_contents_view_android_unittest.cc
modified

Files Changed

  • content/browser/android/drop_data_android.cc
  • content/browser/web_contents/web_contents_view_android_unittest.cc
  • content/browser/web_contents/web_contents_view_drag_security_info.h
  • content/browser/android/drop_data_android.h
  • content/browser/web_contents/web_contents_view_aura_unittest.cc
  • content/browser/web_contents/web_contents_view_aura.h
  • content/browser/web_contents/web_contents_view_android.h
  • content/browser/web_contents/web_contents_view_android.cc
From d89c9453611873f40fe200d7076eb5d7ce98879d Mon Sep 17 00:00:00 2001
From: Shu Yang <shuyng@google.com>
Date: Fri, 18 Sep 2026 11:15:10 -0700
Subject: [PATCH] Validate image accessibility in WebContentsViewAndroid drag handling

In WebContentsViewAndroid, drag-and-drop operations now consult the
originating frame's image accessibility state when populating drag
metadata and handling drop events. When an image is not accessible from
the originating frame, file metadata is omitted during drag enter.
During a drop event, the accessibility state is passed to
PopulateDropDataFromEvent() to strictly prevent the population of
`filenames` and any other file-related fields.

This aligns Android drag event handling with desktop behavior, ensuring
that drop payloads accurately reflect image accessibility state and that
restricted file data is never leaked to the target renderer.

Bug: 497026542
Change-Id: If1dbb9cc5f7d4a8daffbced18bac9d79e612ac24
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8392048
Commit-Queue: Dave Tapuska <dtapuska@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1700966}
---
diff --git a/content/browser/android/drop_data_android.cc b/content/browser/android/drop_data_android.cc
index 8543f95..f2c3238 100644
--- a/content/browser/android/drop_data_android.cc
+++ b/content/browser/android/drop_data_android.cc
@@ -14,6 +14,7 @@
 #include "base/strings/utf_string_conversions.h"
 #include "base/time/time.h"
 #include "base/values.h"
+#include "content/browser/web_contents/web_contents_view_drag_security_info.h"
 #include "ui/events/android/drag_event_android.h"
 #include "url/android/gurl_android.h"
 #include "url/gurl.h"
@@ -135,19 +136,27 @@
 }
 }  // namespace
 
-void PopulateDropDataFromEvent(const ui::DragEventAndroid& event,
-                               DropData* drop_data) {
+void PopulateDropDataFromEvent(
+    const ui::DragEventAndroid& event,
+    const WebContentsViewDragSecurityInfo& drag_security_info,
+    DropData* drop_data) {
   JNIEnv* env = AttachCurrentThread();
 
-  std::vector<std::vector<std::string>> filenames;
-  if (!event.GetJavaFilenames().is_null()) {
-    base::android::Java2dStringArrayTo2dStringVector(
-        env, event.GetJavaFilenames(), &filenames);
-  }
-  for (const auto& info : filenames) {
-    CHECK_EQ(info.size(), 2u);
-    drop_data->filenames.emplace_back(base::FilePath(info[0]),
-                                      base::FilePath(info[1]));
+  // If the image is not accessible from the frame, it means the drag contains a
+  // cross-origin image or otherwise restricted file data. To prevent this data
+  // from leaking to the renderer, we must not populate `filenames` or any
+  // future file-related fields.
+  if (drag_security_info.IsImageAccessibleFromFrame()) {
+    std::vector<std::vector<std::string>> filenames;
+    if (!event.GetJavaFilenames().is_null()) {
+      base::android::Java2dStringArrayTo2dStringVector(
+          env, event.GetJavaFilenames(), &filenames);
+    }
+    for (const auto& info : filenames) {
+      CHECK_EQ(info.size(), 2u);
+      drop_data->filenames.emplace_back(base::FilePath(info[0]),
+                                        base::FilePath(info[1]));
+    }
   }
 
   if (!event.GetJavaText().is_null()) {
diff --git a/content/browser/android/drop_data_android.h b/content/browser/android/drop_data_android.h
index 80f1448..0ec4781 100644
--- a/content/browser/android/drop_data_android.h
+++ b/content/browser/android/drop_data_android.h
@@ -20,14 +20,22 @@
 
 namespace content {
 
+class WebContentsViewDragSecurityInfo;
+
 // Generate a java equivalent DropData object from |drop_data|. Note that the
 // timeline of these object are not equivalent.
 base::android::ScopedJavaLocalRef<jobject> ToJavaDropData(
     const DropData& drop_data);
 
 // Populates |drop_data| with data from the Android |event|.
-CONTENT_EXPORT void PopulateDropDataFromEvent(const ui::DragEventAndroid& event,
-                                              DropData* drop_data);
+// Uses `drag_security_info` to determine if the drag contains restricted
+// file data (e.g., cross-origin images). If restricted, prevents populating
+// `filenames` and any future file-related fields to avoid leaking data to
+// the renderer.
+CONTENT_EXPORT void PopulateDropDataFromEvent(
+    const ui::DragEventAndroid& event,
+    const WebContentsViewDragSecurityInfo& drag_security_info,
+    DropData* drop_data);
 
 // Parses and returns the custom data JSON payload from the Android |event|.
 // Returns std::nullopt if the event does not contain custom data or parsing
diff --git a/content/browser/web_contents/web_contents_view_android.cc b/content/browser/web_contents/web_contents_view_android.cc
index 2bff3326..28b106d 100644
--- a/content/browser/web_contents/web_contents_view_android.cc
+++ b/content/browser/web_contents/web_contents_view_android.cc
@@ -572,6 +572,9 @@
           // Ignore, we will unpack custom data from the event.
           continue;
         } else {
+          if (!drag_security_info_.IsImageAccessibleFromFrame()) {
+            continue;
+          }
           // Create a file extension from the mime type.
           std::string ext = base::UTF16ToUTF8(mime_type);
           if (!net::GetPreferredExtensionForMimeType(ext, &ext)) {
@@ -603,7 +606,7 @@
       drop_data_ = std::make_unique<DropData>();
       drop_data_->did_originate_from_renderer = false;
       drop_data_->document_is_handling_drag = document_is_handling_drag_;
-      PopulateDropDataFromEvent(event, drop_data_.get());
+      PopulateDropDataFromEvent(event, drag_security_info_, drop_data_.get());
 
       OnPerformDrop(event.location(), event.screen_location());
       break;
@@ -816,6 +819,10 @@
   }
   drag_security_info_.OnDragEnded();
 
+  // Note: `drop_data_` is intentionally NOT reset here. The OS can fire
+  // ACTION_DRAG_ENDED asynchronously before `PerformDropCallback` finishes
+  // executing. If we destroy `drop_data_` here, we risk a race condition where
+  // the callback attempts to dereference a null pointer or dropped payload.
   drag_metadata_.clear();
   current_source_rwh_for_drag_.reset();
   current_target_rwh_for_drag_.reset();
diff --git a/content/browser/web_contents/web_contents_view_android.h b/content/browser/web_contents/web_contents_view_android.h
index e9da88ff..32c07865 100644
--- a/content/browser/web_contents/web_contents_view_android.h
+++ b/content/browser/web_contents/web_contents_view_android.h
@@ -8,6 +8,7 @@
 #include <memory>
 #include <optional>
 
+#include "base/gtest_prod_util.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "content/browser/renderer_host/render_view_host_delegate_view.h"
@@ -185,6 +186,18 @@
       RenderWidgetHostViewCreateFunction create_render_widget_host_view);
 
  private:
+  FRIEND_TEST_ALL_PREFIXES(WebContentsViewAndroidTest,
+                           DragInaccessibleImage_SameTab_Filtered);
+  FRIEND_TEST_ALL_PREFIXES(WebContentsViewAndroidTest,
+                           DragAccessibleImage_SameTab_Allowed);
+  FRIEND_TEST_ALL_PREFIXES(WebContentsViewAndroidTest,
+                           DragImage_ExternalSource_Allowed);
+  FRIEND_TEST_ALL_PREFIXES(
+      WebContentsViewAndroidTest,
+      DragInaccessibleImage_MixedMimeTypes_PreservesStringTypes);
+  FRIEND_TEST_ALL_PREFIXES(WebContentsViewAndroidTest,
+                           OnDragEnded_ResetsDropDataAndSecurityInfo);
+
   void OnDragEntered(const gfx::PointF& location,
                      const gfx::PointF& screen_location);
   void DragEnteredCallback(const gfx::PointF& location,
diff --git a/content/browser/web_contents/web_contents_view_aura.h b/content/browser/web_contents/web_contents_view_aura.h
index 98df67ad..0c11686 100644
--- a/content/browser/web_contents/web_contents_view_aura.h
+++ b/content/browser/web_contents/web_contents_view_aura.h
@@ -139,6 +139,8 @@
   FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest,
                            DragDropFilesOriginateFromRenderer);
   FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, DragDropImageFromRenderer);
+  FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest,
+                           DragDropImageFromRenderer_InaccessibleImageOmitted);
   FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, DragDropVirtualFiles);
   FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest,
                            DragDropVirtualFilesOriginateFromRenderer);
diff --git a/content/browser/web_contents/web_contents_view_drag_security_info.h b/content/browser/web_contents/web_contents_view_drag_security_info.h
index cc4f7d1..0b075e69 100644
--- a/content/browser/web_contents/web_contents_view_drag_security_info.h
+++ b/content/browser/web_contents/web_contents_view_drag_security_info.h
@@ -15,8 +15,8 @@
 class RenderWidgetHostImpl;
 
 // Used to track security-salient details about a drag source. This class is to
-// be owned by a WebContentsView indirectly via the Aura or Mac impls. Because
-// the security concerns that it addresses are those that arise when the
+// be owned by a WebContentsView indirectly via the Aura, Mac, or Android impls.
+// Because the security concerns that it addresses are those that arise when the
 // WebContentsView that initiated the drag is also the target of that drag, this
 // class's main question is "is the WebContentsView that is receiving the drag
 // the same one that initiated that drag?" The answer to that question may be
diff --git a/content/browser/web_contents/web_contents_view_android_unittest.cc b/content/browser/web_contents/web_contents_view_android_unittest.cc
index 9a11ca93..bf931b5 100644
--- a/content/browser/web_contents/web_contents_view_android_unittest.cc
+++ b/content/browser/web_contents/web_contents_view_android_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "base/android/jni_array.h"
 #include "base/android/jni_string.h"
 #include "content/browser/android/drop_data_android.h"
 #include "content/browser/renderer_host/render_widget_host_impl.h"
@@ -16,6 +17,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/skia/include/core/SkBitmap.h"
 #include "ui/android/window_android.h"
+#include "ui/base/clipboard/clipboard_constants.h"
 #include "ui/color/color_provider.h"
 #include "ui/events/android/drag_event_android.h"
 #include "ui/gfx/image/image_skia.h"
@@ -46,29 +48,14 @@
     system_drag_ended_called_ = true;
   }
 
-  bool OnDragEvent(const ui::DragEventAndroid& event) override {
-    if (event.action() == DragEventJni::ACTION_DROP) {
-      mock_drop_data_ = std::make_unique<DropData>();
-      PopulateDropDataFromEvent(event, mock_drop_data_.get());
-      return true;
-    }
-    return WebContentsViewAndroid::OnDragEvent(event);
-  }
-
-  DropData* GetDropData() const override {
-    if (mock_drop_data_) {
-      return mock_drop_data_.get();
-    }
-    return WebContentsViewAndroid::GetDropData();
-  }
-
  private:
   bool was_called_ = false;
   bool system_drag_ended_called_ = false;
   bool allowed_ = false;
-  std::unique_ptr<DropData> mock_drop_data_;
 };
 
+}  // namespace
+
 class WebContentsViewAndroidTest : public RenderViewHostTestHarness {
  public:
   void SetUp() override {
@@ -148,6 +135,204 @@
   EXPECT_EQ(restored_data->source_effect_allowed, u"move");
 }
 
+TEST_F(WebContentsViewAndroidTest, DragInaccessibleImage_SameTab_Filtered) {
+  DropData drop_data;
+  drop_data.file_contents = {'t', 'e', 's', 't'};
+  drop_data.file_contents_image_accessible = false;
+  drop_data.file_contents_source_url =
+      GURL("https://different-origin.com/image.png");
+  drop_data.file_contents_filename_extension = "png";
+  view()->drag_security_info_.OnDragInitiated(GetRenderWidgetHost(), drop_data);
+
+  JNIEnv* env = base::android::AttachCurrentThread();
+
+  // ACTION_DRAG_ENTERED with image MIME type.
+  std::vector<std::u16string> enter_mime_types = {u"image/png"};
+  ui::DragEventAndroid enter_event(
+      env, DragEventJni::ACTION_DRAG_ENTERED, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, base::android::JavaRef<jobjectArray>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>());
+  view()->OnDragEvent(enter_event);
+
+  // File metadata should not be advertised when image access is not allowed.
+  EXPECT_TRUE(view()->drag_metadata_.empty());
+
+  // ACTION_DROP with filenames.
+  std::vector<std::vector<std::string>> filenames_vec = {
+      {"content://org.chromium.test/image.png", "image.png"}};
+  base::android::ScopedJavaLocalRef<jobjectArray> j_filenames =
+      base::android::ToJavaArrayOfStringArray(env, filenames_vec);
+  ui::DragEventAndroid drop_event(
+      env, DragEventJni::ACTION_DROP, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, j_filenames, base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>());
+  view()->OnDragEvent(drop_event);
+
+  DropData* restored_data = view()->GetDropData();
+  ASSERT_TRUE(restored_data);
+  EXPECT_TRUE(restored_data->filenames.empty());
+  EXPECT_TRUE(restored_data->file_contents.empty());
+  EXPECT_FALSE(restored_data->file_contents_image_accessible);
+}
+
+TEST_F(WebContentsViewAndroidTest, DragAccessibleImage_SameTab_Allowed) {
+  DropData drop_data;
+  drop_data.file_contents = {'t', 'e', 's', 't'};
+  drop_data.file_contents_image_accessible = true;
+  drop_data.file_contents_source_url = GURL("https://google.com/image.png");
+  drop_data.file_contents_filename_extension = "png";
+  view()->drag_security_info_.OnDragInitiated(GetRenderWidgetHost(), drop_data);
+
+  JNIEnv* env = base::android::AttachCurrentThread();
+
+  // ACTION_DRAG_ENTERED with image MIME type.
+  std::vector<std::u16string> enter_mime_types = {u"image/png"};
+  ui::DragEventAndroid enter_event(
+      env, DragEventJni::ACTION_DRAG_ENTERED, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, base::android::JavaRef<jobjectArray>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>());
+  view()->OnDragEvent(enter_event);
+
+  // File metadata should be advertised when image access is allowed.
+  ASSERT_EQ(view()->drag_metadata_.size(), 1u);
+  EXPECT_EQ(view()->drag_metadata_[0].kind, DropData::Kind::FILENAME);
+  EXPECT_EQ(view()->drag_metadata_[0].filename, base::FilePath("file.png"));
+
+  // ACTION_DROP with filenames.
+  std::vector<std::vector<std::string>> filenames_vec = {
+      {"content://org.chromium.test/image.png", "image.png"}};
+  base::android::ScopedJavaLocalRef<jobjectArray> j_filenames =
+      base::android::ToJavaArrayOfStringArray(env, filenames_vec);
+  ui::DragEventAndroid drop_event(
+      env, DragEventJni::ACTION_DROP, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, j_filenames, base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>());
+  view()->OnDragEvent(drop_event);
+
+  DropData* restored_data = view()->GetDropData();
+  ASSERT_TRUE(restored_data);
+  ASSERT_EQ(restored_data->filenames.size(), 1u);
+  EXPECT_EQ(restored_data->filenames[0].path,
+            base::FilePath("content://org.chromium.test/image.png"));
+  EXPECT_EQ(restored_data->filenames[0].display_name,
+            base::FilePath("image.png"));
+}
+
+TEST_F(WebContentsViewAndroidTest, DragImage_ExternalSource_Allowed) {
+  // No drag initiated on view.
+  EXPECT_FALSE(view()->drag_security_info_.did_initiate());
+
+  JNIEnv* env = base::android::AttachCurrentThread();
+
+  // ACTION_DRAG_ENTERED with image MIME type.
+  std::vector<std::u16string> enter_mime_types = {u"image/png"};
+  ui::DragEventAndroid enter_event(
+      env, DragEventJni::ACTION_DRAG_ENTERED, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, base::android::JavaRef<jobjectArray>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>());
+  view()->OnDragEvent(enter_event);
+
+  ASSERT_EQ(view()->drag_metadata_.size(), 1u);
+  EXPECT_EQ(view()->drag_metadata_[0].kind, DropData::Kind::FILENAME);
+
+  // ACTION_DROP with filenames.
+  std::vector<std::vector<std::string>> filenames_vec = {
+      {"content://external.app/photo.png", "photo.png"}};
+  base::android::ScopedJavaLocalRef<jobjectArray> j_filenames =
+      base::android::ToJavaArrayOfStringArray(env, filenames_vec);
+  ui::DragEventAndroid drop_event(
+      env, DragEventJni::ACTION_DROP, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, j_filenames, base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>());
+  view()->OnDragEvent(drop_event);
+
+  DropData* restored_data = view()->GetDropData();
+  ASSERT_TRUE(restored_data);
+  ASSERT_EQ(restored_data->filenames.size(), 1u);
+  EXPECT_EQ(restored_data->filenames[0].path,
+            base::FilePath("content://external.app/photo.png"));
+}
+
+TEST_F(WebContentsViewAndroidTest,
+       DragInaccessibleImage_MixedMimeTypes_PreservesStringTypes) {
+  DropData drop_data;
+  drop_data.file_contents = {'t', 'e', 's', 't'};
+  drop_data.file_contents_image_accessible = false;
+  drop_data.file_contents_source_url =
+      GURL("https://different-origin.com/image.png");
+  drop_data.file_contents_filename_extension = "png";
+  view()->drag_security_info_.OnDragInitiated(GetRenderWidgetHost(), drop_data);
+
+  JNIEnv* env = base::android::AttachCurrentThread();
+
+  // ACTION_DRAG_ENTERED with text and image MIME types.
+  std::vector<std::u16string> enter_mime_types = {ui::kMimeTypePlainText16,
+                                                  u"image/png"};
+  ui::DragEventAndroid enter_event(
+      env, DragEventJni::ACTION_DRAG_ENTERED, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, base::android::JavaRef<jobjectArray>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/web_contents/web_contents_view_android_unittest.cc b/content/browser/web_contents/web_contents_view_android_unittest.cc
index 9a11ca93..bf931b5 100644
--- a/content/browser/web_contents/web_contents_view_android_unittest.cc
+++ b/content/browser/web_contents/web_contents_view_android_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "base/android/jni_array.h"
 #include "base/android/jni_string.h"
 #include "content/browser/android/drop_data_android.h"
 #include "content/browser/renderer_host/render_widget_host_impl.h"
@@ -16,6 +17,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/skia/include/core/SkBitmap.h"
 #include "ui/android/window_android.h"
+#include "ui/base/clipboard/clipboard_constants.h"
 #include "ui/color/color_provider.h"
 #include "ui/events/android/drag_event_android.h"
 #include "ui/gfx/image/image_skia.h"
@@ -46,29 +48,14 @@
     system_drag_ended_called_ = true;
   }
 
-  bool OnDragEvent(const ui::DragEventAndroid& event) override {
-    if (event.action() == DragEventJni::ACTION_DROP) {
-      mock_drop_data_ = std::make_unique<DropData>();
-      PopulateDropDataFromEvent(event, mock_drop_data_.get());
-      return true;
-    }
-    return WebContentsViewAndroid::OnDragEvent(event);
-  }
-
-  DropData* GetDropData() const override {
-    if (mock_drop_data_) {
-      return mock_drop_data_.get();
-    }
-    return WebContentsViewAndroid::GetDropData();
-  }
-
  private:
   bool was_called_ = false;
   bool system_drag_ended_called_ = false;
   bool allowed_ = false;
-  std::unique_ptr<DropData> mock_drop_data_;
 };
 
+}  // namespace
+
 class WebContentsViewAndroidTest : public RenderViewHostTestHarness {
  public:
   void SetUp() override {
@@ -148,6 +135,204 @@
   EXPECT_EQ(restored_data->source_effect_allowed, u"move");
 }
 
+TEST_F(WebContentsViewAndroidTest, DragInaccessibleImage_SameTab_Filtered) {
+  DropData drop_data;
+  drop_data.file_contents = {'t', 'e', 's', 't'};
+  drop_data.file_contents_image_accessible = false;
+  drop_data.file_contents_source_url =
+      GURL("https://different-origin.com/image.png");
+  drop_data.file_contents_filename_extension = "png";
+  view()->drag_security_info_.OnDragInitiated(GetRenderWidgetHost(), drop_data);
+
+  JNIEnv* env = base::android::AttachCurrentThread();
+
+  // ACTION_DRAG_ENTERED with image MIME type.
+  std::vector<std::u16string> enter_mime_types = {u"image/png"};
+  ui::DragEventAndroid enter_event(
+      env, DragEventJni::ACTION_DRAG_ENTERED, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, base::android::JavaRef<jobjectArray>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>());
+  view()->OnDragEvent(enter_event);
+
+  // File metadata should not be advertised when image access is not allowed.
+  EXPECT_TRUE(view()->drag_metadata_.empty());
+
+  // ACTION_DROP with filenames.
+  std::vector<std::vector<std::string>> filenames_vec = {
+      {"content://org.chromium.test/image.png", "image.png"}};
+  base::android::ScopedJavaLocalRef<jobjectArray> j_filenames =
+      base::android::ToJavaArrayOfStringArray(env, filenames_vec);
+  ui::DragEventAndroid drop_event(
+      env, DragEventJni::ACTION_DROP, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, j_filenames, base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>());
+  view()->OnDragEvent(drop_event);
+
+  DropData* restored_data = view()->GetDropData();
+  ASSERT_TRUE(restored_data);
+  EXPECT_TRUE(restored_data->filenames.empty());
+  EXPECT_TRUE(restored_data->file_contents.empty());
+  EXPECT_FALSE(restored_data->file_contents_image_accessible);
+}
+
+TEST_F(WebContentsViewAndroidTest, DragAccessibleImage_SameTab_Allowed) {
+  DropData drop_data;
+  drop_data.file_contents = {'t', 'e', 's', 't'};
+  drop_data.file_contents_image_accessible = true;
+  drop_data.file_contents_source_url = GURL("https://google.com/image.png");
+  drop_data.file_contents_filename_extension = "png";
+  view()->drag_security_info_.OnDragInitiated(GetRenderWidgetHost(), drop_data);
+
+  JNIEnv* env = base::android::AttachCurrentThread();
+
+  // ACTION_DRAG_ENTERED with image MIME type.
+  std::vector<std::u16string> enter_mime_types = {u"image/png"};
+  ui::DragEventAndroid enter_event(
+      env, DragEventJni::ACTION_DRAG_ENTERED, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, base::android::JavaRef<jobjectArray>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>());
+  view()->OnDragEvent(enter_event);
+
+  // File metadata should be advertised when image access is allowed.
+  ASSERT_EQ(view()->drag_metadata_.size(), 1u);
+  EXPECT_EQ(view()->drag_metadata_[0].kind, DropData::Kind::FILENAME);
+  EXPECT_EQ(view()->drag_metadata_[0].filename, base::FilePath("file.png"));
+
+  // ACTION_DROP with filenames.
+  std::vector<std::vector<std::string>> filenames_vec = {
+      {"content://org.chromium.test/image.png", "image.png"}};
+  base::android::ScopedJavaLocalRef<jobjectArray> j_filenames =
+      base::android::ToJavaArrayOfStringArray(env, filenames_vec);
+  ui::DragEventAndroid drop_event(
+      env, DragEventJni::ACTION_DROP, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, j_filenames, base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>());
+  view()->OnDragEvent(drop_event);
+
+  DropData* restored_data = view()->GetDropData();
+  ASSERT_TRUE(restored_data);
+  ASSERT_EQ(restored_data->filenames.size(), 1u);
+  EXPECT_EQ(restored_data->filenames[0].path,
+            base::FilePath("content://org.chromium.test/image.png"));
+  EXPECT_EQ(restored_data->filenames[0].display_name,
+            base::FilePath("image.png"));
+}
+
+TEST_F(WebContentsViewAndroidTest, DragImage_ExternalSource_Allowed) {
+  // No drag initiated on view.
+  EXPECT_FALSE(view()->drag_security_info_.did_initiate());
+
+  JNIEnv* env = base::android::AttachCurrentThread();
+
+  // ACTION_DRAG_ENTERED with image MIME type.
+  std::vector<std::u16string> enter_mime_types = {u"image/png"};
+  ui::DragEventAndroid enter_event(
+      env, DragEventJni::ACTION_DRAG_ENTERED, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, base::android::JavaRef<jobjectArray>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>());
+  view()->OnDragEvent(enter_event);
+
+  ASSERT_EQ(view()->drag_metadata_.size(), 1u);
+  EXPECT_EQ(view()->drag_metadata_[0].kind, DropData::Kind::FILENAME);
+
+  // ACTION_DROP with filenames.
+  std::vector<std::vector<std::string>> filenames_vec = {
+      {"content://external.app/photo.png", "photo.png"}};
+  base::android::ScopedJavaLocalRef<jobjectArray> j_filenames =
+      base::android::ToJavaArrayOfStringArray(env, filenames_vec);
+  ui::DragEventAndroid drop_event(
+      env, DragEventJni::ACTION_DROP, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, j_filenames, base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>());
+  view()->OnDragEvent(drop_event);
+
+  DropData* restored_data = view()->GetDropData();
+  ASSERT_TRUE(restored_data);
+  ASSERT_EQ(restored_data->filenames.size(), 1u);
+  EXPECT_EQ(restored_data->filenames[0].path,
+            base::FilePath("content://external.app/photo.png"));
+}
+
+TEST_F(WebContentsViewAndroidTest,
+       DragInaccessibleImage_MixedMimeTypes_PreservesStringTypes) {
+  DropData drop_data;
+  drop_data.file_contents = {'t', 'e', 's', 't'};
+  drop_data.file_contents_image_accessible = false;
+  drop_data.file_contents_source_url =
+      GURL("https://different-origin.com/image.png");
+  drop_data.file_contents_filename_extension = "png";
+  view()->drag_security_info_.OnDragInitiated(GetRenderWidgetHost(), drop_data);
+
+  JNIEnv* env = base::android::AttachCurrentThread();
+
+  // ACTION_DRAG_ENTERED with text and image MIME types.
+  std::vector<std::u16string> enter_mime_types = {ui::kMimeTypePlainText16,
+                                                  u"image/png"};
+  ui::DragEventAndroid enter_event(
+      env, DragEventJni::ACTION_DRAG_ENTERED, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, base::android::JavaRef<jobjectArray>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>());
+  view()->OnDragEvent(enter_event);
+
+  // String MIME type is kept in metadata, while file metadata is omitted.
+  ASSERT_EQ(view()->drag_metadata_.size(), 1u);
+  EXPECT_EQ(view()->drag_metadata_[0].kind, DropData::Kind::STRING);
+  EXPECT_EQ(view()->drag_metadata_[0].mime_type, ui::kMimeTypePlainText16);
+
+  // ACTION_DROP with text and filenames.
+  base::android::ScopedJavaLocalRef<jstring> j_text =
+      base::android::ConvertUTF8ToJavaString(env, "sample text");
+  std::vector<std::vector<std::string>> filenames_vec = {
+      {"content://org.chromium.test/image.png", "image.png"}};
+  base::android::ScopedJavaLocalRef<jobjectArray> j_filenames =
+      base::android::ToJavaArrayOfStringArray(env, filenames_vec);
+  ui::DragEventAndroid drop_event(
+      env, DragEventJni::ACTION_DROP, gfx::PointF(), gfx::PointF(),
+      enter_mime_types, false, j_filenames, j_text,
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>());
+  view()->OnDragEvent(drop_event);
+
+  DropData* restored_data = view()->GetDropData();
+  ASSERT_TRUE(restored_data);
+  EXPECT_EQ(restored_data->text, u"sample text");
+  EXPECT_TRUE(restored_data->filenames.empty());
+}
+
+TEST_F(WebContentsViewAndroidTest, OnDragEnded_ResetsDropDataAndSecurityInfo) {
+  DropData drop_data;
+  drop_data.file_contents = {'t', 'e', 's', 't'};
+  drop_data.file_contents_image_accessible = false;
+  view()->drag_security_info_.OnDragInitiated(GetRenderWidgetHost(), drop_data);
+
+  EXPECT_TRUE(view()->drag_security_info_.did_initiate());
+  EXPECT_FALSE(view()->drag_security_info_.IsImageAccessibleFromFrame());
+
+  JNIEnv* env = base::android::AttachCurrentThread();
+  ui::DragEventAndroid end_event(
+      env, DragEventJni::ACTION_DRAG_ENDED, gfx::PointF(), gfx::PointF(), {},
+      false, base::android::JavaRef<jobjectArray>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>(), base::android::JavaRef<jstring>(),
+      base::android::JavaRef<jstring>());
+
+  view()->OnDragEvent(end_event);
+
+  EXPECT_FALSE(view()->drag_security_info_.did_initiate());
+  EXPECT_TRUE(view()->drag_security_info_.IsImageAccessibleFromFrame());
+}
+
 TEST_F(WebContentsViewAndroidTest, ColorProviderSourceFallback) {
   WebContentsImpl* web_contents_impl =
       static_cast<WebContentsImpl*>(web_contents());
@@ -178,5 +363,4 @@
   web_contents()->GetColorProvider();
 }
 
-}  // namespace
 }  // namespace content
diff --git a/content/browser/web_contents/web_contents_view_aura_unittest.cc b/content/browser/web_contents/web_contents_view_aura_unittest.cc
index 89f96676..dafcabe 100644
--- a/content/browser/web_contents/web_contents_view_aura_unittest.cc
+++ b/content/browser/web_contents/web_contents_view_aura_unittest.cc
@@ -322,11 +322,15 @@
 #define MAYBE_DragDropFilesOriginateFromRenderer \
   DISABLED_DragDropFilesOriginateFromRenderer
 #define MAYBE_DragDropImageFromRenderer DISABLED_DragDropImageFromRenderer
+#define MAYBE_DragDropImageFromRenderer_InaccessibleImageOmitted \
+  DISABLED_DragDropImageFromRenderer_InaccessibleImageOmitted
 #else
 #define MAYBE_DragDropFiles DragDropFiles
 #define MAYBE_DragDropFilesOriginateFromRenderer \
   DragDropFilesOriginateFromRenderer
 #define MAYBE_DragDropImageFromRenderer DragDropImageFromRenderer
+#define MAYBE_DragDropImageFromRenderer_InaccessibleImageOmitted \
+  DragDropImageFromRenderer_InaccessibleImageOmitted
 #endif
 
 TEST_F(WebContentsViewAuraTest, MAYBE_DragDropFiles) {
@@ -602,6 +606,76 @@
             drop_complete_data_->drop_data.file_contents_content_disposition);
 }
 
+TEST_F(WebContentsViewAuraTest,
+       MAYBE_DragDropImageFromRenderer_InaccessibleImageOmitted) {
+  WebContentsViewAura* view = GetView();
+
+  DropData initiating_drop_data;
+  initiating_drop_data.file_contents = {0x89, 0x50, 0x4E, 0x47};
+  initiating_drop_data.file_contents_image_accessible = false;
+
+  view->drag_security_info_.OnDragInitiated(
+      static_cast<RenderWidgetHostImpl*>(main_rfh()->GetRenderWidgetHost()),
+      initiating_drop_data);
+
+  EXPECT_TRUE(view->drag_security_info_.did_initiate());
... (truncated)
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential SOP bypass: Cross-origin image data leak via Android drag-and-drop

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: Dragging and dropping a cross-origin image on Chrome for Android potentially leaks its raw bytes to the page, bypassing the Same-Origin Policy. The Android drag-and-drop implementation fails to respect the image_accessible security flag and improperly grants the renderer read access to a generated content:// URI containing the image data.

Affected files:

  • content/browser/web_contents/web_contents_view_android.cc
  • content/browser/android/drop_data_android.cc
  • ui/android/java/src/org/chromium/ui/dragdrop/DragAndDropDelegateImpl.java

Estimated timestamp from git blame: 2025-03-10

Description

There is a potential Same-Origin Policy (SOP) bypass in Chrome for Android’s drag-and-drop implementation. An attacker can trick a user into dragging a cross-origin image and dropping it onto the same page, allowing the attacker’s JavaScript to read the raw image bytes.

When a user drags an image, Blink’s DataTransfer::DeclareAndWriteDragImage correctly evaluates ImageResourceContent::IsAccessAllowed(). For cross-origin images without CORS, this returns false, which is propagated via IPC as is_image_accessible = false in the DropData struct.

However, the Android browser process mishandles this in several ways:

  1. Security Flag Ignored: In content/browser/android/drop_data_android.cc, ToJavaDropData unconditionally converts drop_data.file_contents into a Java byte array (jimage_bytes). It completely ignores the drop_data.file_contents_image_accessible flag.
  2. URI Generation: The Java layer caches these bytes in DropDataProviderImpl and generates an Android content:// URI for them during drag initiation.
  3. Hardcoded Origin: When the image is dropped, the DragEventJni::ACTION_DROP handler in content/browser/web_contents/web_contents_view_android.cc creates a new DropData object and explicitly hardcodes drop_data_->did_originate_from_renderer = false; (line 589).
  4. Filter Bypass: Because did_originate_from_renderer is false, RenderWidgetHostImpl::FilterDropData fails to clear the filenames list.
  5. Missing Access Check: WebContentsViewAndroid::PerformDropCallback fails to check drag_security_info_.IsImageAccessibleFromFrame(). Consequently, GrantFileAccessFromDropData explicitly grants the renderer process read access to the content:// URI containing the cross-origin image bytes.

The attacker’s Javascript receives the content:// URI disguised as a File object in the drop event and can read its contents using a standard FileReader, completely bypassing SOP.

Potential Reproduction Steps

Note: These are potential steps based on code analysis; our tooling agent does not currently have the capability to execute a working Proof of Concept.

  1. An attacker hosts an HTML page containing a cross-origin image (e.g., <img src="https://victim.test/secret-image.png">) and a drop zone (<div id="dropzone"></div>).
  2. The drop zone has an ondrop event listener that reads event.dataTransfer.files[0] using a FileReader (e.g., readAsArrayBuffer()).
  3. The user visits the page on Chrome for Android.
  4. The user is convinced to long-press the cross-origin image and drag it over to the drop zone, releasing it.
  5. The ondrop handler receives a File backed by the content:// URI. The FileReader successfully reads the cross-origin image bytes, which the attacker can then exfiltrate.

Suggested Fix

Multiple layers of defense should be patched to resolve this:

  1. drop_data_android.cc (ToJavaDropData): Only populate jimage_bytes (and subsequently jimage_extension and jimage_filename) if drop_data.file_contents_image_accessible is true.
  2. web_contents_view_android.cc (DragEventJni::ACTION_DROP): Do not hardcode did_originate_from_renderer = false. Ensure the true origin of the drag is tracked (e.g., using state stored during StartDragging) and restored here.
  3. web_contents_view_android.cc (PerformDropCallback): Enforce the IsImageAccessibleFromFrame() check before allowing file_contents or their corresponding content:// URIs to be sent back to the renderer, aligning Android’s security checks with the Aura (desktop) implementation (WebContentsViewAura::PrepareDropData).

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker