Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in UI
DescriptionInsufficient validation of untrusted input in UI
ComponentUI
Bug ClassLogic Error
Tracker517670731
Fix commiteb11d0fa4e65 (chromium/src) +53/-18
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
for
ui/android/java/src/org/chromium/ui/base/EventForwarder.java
modified
if
ui/android/java/src/org/chromium/ui/base/EventForwarder.java
modified

Files Changed

  • base/android/java/src/org/chromium/base/ContentUriUtils.java
  • ui/android/BUILD.gn
  • ui/android/java/src/org/chromium/ui/base/EventForwarder.java
  • ui/android/junit/src/org/chromium/ui/base/EventForwarderTest.java
From eb11d0fa4e6528b38d7321801b99adccf1ff6126 Mon Sep 17 00:00:00 2001
From: Joel Hockey <[email protected]>
Date: Tue, 14 Jul 2026 20:04:28 -0700
Subject: [PATCH] Disallow drag-drop of internal chrome files

Stop malicious apps from injecting paths of internal chrome files in a
drag-drop operation.

Clipboard and drag-drop allow content-URIs and regular posix file
paths.  ContentUriUtils.isOpenableFile() was unintentionally filtering
all absolute paths, so it has been updated to allow them.

ContentUriUtils.isUriFromThisApp() is now updated to also check
absolute paths as well as content-URIs.  This function was being
used only by Clipboard, but it is now also used in EventForwarder
for drag-drop.

Bug: 517670731
Change-Id: I47fe8bff8633105aa51855e91fcf984d23cbbd6b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8083706
Commit-Queue: Joel Hockey <[email protected]>
Reviewed-by: Eric Lok <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1662350}
---

diff --git a/base/android/java/src/org/chromium/base/ContentUriUtils.java b/base/android/java/src/org/chromium/base/ContentUriUtils.java
index 0e9a0cc..d04cafe5 100644
--- a/base/android/java/src/org/chromium/base/ContentUriUtils.java
+++ b/base/android/java/src/org/chromium/base/ContentUriUtils.java
@@ -656,6 +656,7 @@
      */
     public static boolean isOpenableFile(@Nullable Uri uri) {
         if (uri == null) return false;
+        if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return true;
         ContentResolver cr = ContextUtils.getApplicationContext().getContentResolver();
         try (Cursor cursor =
                 cr.query(
@@ -726,7 +727,10 @@
      * @return True if the URI is from the current application, false otherwise.
      */
     public static boolean isUriFromThisApp(@Nullable Uri uri, Context context) {
-        if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return false;
+        if (uri == null) return false;
+        if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
+            return PathUtils.isPathUnderAppDir(uri.toString(), context);
+        }
         String authority = uri.getAuthority();
         if (TextUtils.isEmpty(authority)) return false;
 
diff --git a/ui/android/BUILD.gn b/ui/android/BUILD.gn
index a3299be6..e3b5b78 100644
--- a/ui/android/BUILD.gn
+++ b/ui/android/BUILD.gn
@@ -907,6 +907,7 @@
     "//third_party/androidx:androidx_lifecycle_lifecycle_common_java",
     "//third_party/androidx:androidx_test_core_java",
     "//third_party/androidx:androidx_test_ext_junit_java",
+    "//third_party/androidx:androidx_test_monitor_java",
     "//third_party/androidx:androidx_test_runner_java",
     "//third_party/google-truth:google_truth_java",
     "//third_party/hamcrest:hamcrest_java",
diff --git a/ui/android/java/src/org/chromium/ui/base/EventForwarder.java b/ui/android/java/src/org/chromium/ui/base/EventForwarder.java
index dd982c5..f8b4aec 100644
--- a/ui/android/java/src/org/chromium/ui/base/EventForwarder.java
+++ b/ui/android/java/src/org/chromium/ui/base/EventForwarder.java
@@ -757,6 +757,13 @@
                 for (int i = 0; i < itemCount; i++) {
                     // If there are any Uris, set them as files.
                     Uri uri = clipData.getItemAt(i).getUri();
+                    // Reject URIs originating from this app to prevent the browser from opening
+                    // private files on behalf of an untrusted paste request.
+                    if (UiAndroidFeatureMap.isEnabled(
+                                    UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES)
+                            && ContentUriUtils.isUriFromThisApp(uri)) {
+                        continue;
+                    }
                     if (uri != null) {
                         String uriString = uri.toString();
                         String displayName = ContentUriUtils.maybeGetDisplayName(uriString);
diff --git a/ui/android/junit/src/org/chromium/ui/base/EventForwarderTest.java b/ui/android/junit/src/org/chromium/ui/base/EventForwarderTest.java
index 5d0db4e..13eb3aa 100644
--- a/ui/android/junit/src/org/chromium/ui/base/EventForwarderTest.java
+++ b/ui/android/junit/src/org/chromium/ui/base/EventForwarderTest.java
@@ -21,6 +21,7 @@
 
 import android.content.ClipData;
 import android.content.ClipDescription;
+import android.content.Context;
 import android.net.Uri;
 import android.os.Build;
 import android.view.DragEvent;
@@ -30,6 +31,8 @@
 import android.view.Surface;
 import android.view.View;
 
+import androidx.test.InstrumentationRegistry;
+
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
@@ -43,9 +46,14 @@
 import org.robolectric.shadows.ShadowLooper;
 
 import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.base.test.util.Features.EnableFeatures;
 import org.chromium.base.test.util.HistogramWatcher;
 import org.chromium.ui.util.MotionEventUtils;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
 /** Tests logic in the {@link EventForwarder} class. */
 @RunWith(BaseRobolectricTestRunner.class)
 @Config(manifest = Config.NONE)
@@ -248,33 +256,34 @@
     }
 
     @Test
-    public void testDragDropEvent() {
+    @EnableFeatures(UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_FILES)
+    public void testDragDropEvent() throws IOException {
         // Text.
         validateDragDropEvent(
                 new String[] {"text/plain"},
                 new ClipData.Item[] {new ClipData.Item("text content")},
-                new String[][] {}, // expectedFilenames
-                "text content", // expectedText
-                null, // expectedHtml
-                null); // expectedUrl
+                new String[][] {},
+                /* expectedText= */ "text content",
+                /* expectedHtml= */ null,
+                /* expectedUrl= */ null);
 
         // Html.
         validateDragDropEvent(
                 new String[] {"text/html"},
                 new ClipData.Item[] {new ClipData.Item("text content", "html content")},
-                new String[][] {}, // expectedFilenames
-                "text content", // expectedText
-                "html content", // expectedHtml
-                null); // expectedUrl
+                /* expectedFilenames= */ new String[][] {},
+                /* expectedText= */ "text content",
+                /* expectedHtml= */ "html content",
+                /* expectedUrl= */ null);
 
         // Url.
         validateDragDropEvent(
                 new String[] {"text/x-moz-url"},
                 new ClipData.Item[] {new ClipData.Item("url content")},
-                new String[][] {}, // expectedFilenames
-                "url content", // expectedText
-                null, // expectedHtml
-                "url content"); // expectedUrl
+                /* expectedFilenames= */ new String[][] {},
+                /* expectedText= */ "url content",
+                /* expectedHtml= */ null,
+                /* expectedUrl= */ "url content");
 
         // Files.
         validateDragDropEvent(
@@ -283,10 +292,24 @@
                     new ClipData.Item(Uri.parse("image.jpg")),
                     new ClipData.Item(Uri.parse("hello.txt"))
                 },
-                new String[][] {{"image.jpg", ""}, {"hello.txt", ""}}, // expectedFilenames
-                null, // expectedText
-                null, // expectedHtml
-                null); // expectedUrl
+                /* expectedFilenames= */ new String[][] {{"image.jpg", ""}, {"hello.txt", ""}},
+                /* expectedText= */ null,
+                /* expectedHtml= */ null,
+                /* expectedUrl= */ null);
+
+        // Internal Files disallowed.
+        Context context = InstrumentationRegistry.getTargetContext();
+        File fileUnderDataDir = new File(context.getDataDir(), "test.txt");
+        Files.writeString(fileUnderDataDir.toPath(), "file content");
+        validateDragDropEvent(
+                new String[] {"text/plain"},
+                new ClipData.Item[] {
+                    new ClipData.Item(Uri.parse(fileUnderDataDir.getAbsolutePath())),
+                },
+                /* expectedFilenames= */ new String[][] {},
+                /* expectedText= */ null,
+                /* expectedHtml= */ null,
+                /* expectedUrl= */ null);
     }
 
     @Test
Loading diff…

Original Bug Report

reported by [email protected]

Potential private file exfiltration via drag-and-drop on Chrome for Android

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: A potential vulnerability in Chrome for Android allows a co-installed application to exfiltrate files from Chrome’s private profile directory via drag-and-drop. By initiating a global drag containing a null-scheme absolute POSIX path, a malicious app could bypass URI scheme validation. When dropped onto an attacker-controlled page, Chrome potentially grants the renderer process read access to the target file.

Affected files:

  • ui/android/java/src/org/chromium/ui/base/EventForwarder.java
  • content/browser/android/drop_data_android.cc
  • content/browser/file_system/browser_file_system_helper.cc

Estimated timestamp from git blame: 2024-07-17

Description

There is a potential security vulnerability in Chrome for Android where a co-installed malicious application can leverage global drag-and-drop to exfiltrate Chrome’s private application data (such as cookies, history, and logins).

When a drag-and-drop event is handled by Chrome, the Java layer extracts URI strings from the ClipData without validating their scheme or verifying whether the path points to Chrome’s private data directory. On the native side, because the drop originated from outside the browser, standard security filters that block renderer-originated files are bypassed. Chrome then registers the absolute path in the isolated filesystem and grants the destination renderer read access to the specified file, allowing an attacker-controlled webpage to exfiltrate its contents.

Technical Analysis

  1. Ingress and Parameter Extraction (Java): In ui/android/java/src/org/chromium/ui/base/EventForwarder.java inside onDragEvent, during DragEvent.ACTION_DROP, the application iterates over the drop items and extracts their URIs:

    Uri uri = clipData.getItemAt(i).getUri();
    if (uri != null) {
        String uriString = uri.toString();
        String displayName = ContentUriUtils.maybeGetDisplayName(uriString);
        if (displayName == null) {
            displayName = new String();
        }
        filenames.add(new String[] {uriString, displayName});
    }
    

    At this stage, there is no validation to reject absolute paths or check if the target path resides under Chrome’s private directory (e.g., via PathUtils.isPathUnderAppDir()).

  2. Native Conversion: The filenames array is passed over JNI to the native side. In content/browser/android/drop_data_android.cc, PopulateDropDataFromEvent parses these JNI string arrays into native base::FilePath structures inside DropData::filenames:

    for (const auto& info : filenames) {
      CHECK_EQ(info.size(), 2u);
      drop_data->filenames.emplace_back(base::FilePath(info[0]), base::FilePath(info[1]));
    }
    
  3. Renderer Taint and Filter Bypass: Because the drag originates from an external Android application, the browser process hardcodes drop_data_->did_originate_from_renderer = false inside content/browser/web_contents/web_contents_view_android.cc:

    case DragEventJni::ACTION_DROP: {
      drop_data_ = std::make_unique<DropData>();
      drop_data_->did_originate_from_renderer = false;
      ...
    

    Consequently, when RenderWidgetHostImpl::FilterDropData is called, the file list is spared from being cleared because did_originate_from_renderer is false.

  4. Capability and Read Privilege Grant: During drop processing, RenderWidgetHostImpl::DragTargetDrop invokes PrepareDropDataForChildProcess, which routes to PrepareDataTransferFilenamesForChildProcess in content/browser/file_system/browser_file_system_helper.cc. For each filename in the drop data, the browser process explicitly grants the target renderer process read capabilities for that path:

    security_policy->GrantRequestOfSpecificFile(child_id, filename.path);
    if (!security_policy->CanReadFile(child_id, filename.path))
      security_policy->GrantReadFile(child_id, filename.path);
    

    As a result, the destination renderer process is authorized to read the file, bypassing standard origin and sandbox access controls.

Potential Attack Steps

(Note: These are theoretical/potential steps; our security review tooling does not have the capability to run code or execute live tests on a device)

  1. A co-installed zero-permission application on the victim’s Android device initiates a global drag with a null-scheme URI referencing a private Chrome file: Uri.parse("/data/data/com.android.chrome/app_chrome/Default/Cookies")
  2. The user is induced to perform a drag-and-drop gesture from the malicious app to a split-screen or multi-window instance of Chrome.
  3. The drop target is an attacker-controlled page containing a standard drag-and-drop event listener.
  4. Upon receiving the drop, the webpage uses the File API (FileReader.readAsArrayBuffer()) to read and upload the cookies database to an external server.

Suggested Fix

Apply strict ingress validation inside the Android drag-and-drop handler to match the security controls used in other upload pathways (like SelectFileDialog.java).

Before passing filename URIs to JNI:

  1. Ensure that any received URIs have a valid and expected scheme (such as content:// or file://).
  2. Validate that the resolved file path does not point to Chrome’s own private application directories by checking the path with PathUtils.isPathUnderAppDir(path, context) and rejecting it if it returns true.

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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