CVE-2026-17698
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forui/android/java/src/org/chromium/ui/base/EventForwarder.java |
modified | |
ifui/android/java/src/org/chromium/ui/base/EventForwarder.java |
modified |
Files Changed
base/android/java/src/org/chromium/base/ContentUriUtils.javaui/android/BUILD.gnui/android/java/src/org/chromium/ui/base/EventForwarder.javaui/android/junit/src/org/chromium/ui/base/EventForwarderTest.java
Patch
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
Original Bug Report
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.javacontent/browser/android/drop_data_android.cccontent/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
-
Ingress and Parameter Extraction (Java): In
ui/android/java/src/org/chromium/ui/base/EventForwarder.javainsideonDragEvent, duringDragEvent.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()). -
Native Conversion: The filenames array is passed over JNI to the native side. In
content/browser/android/drop_data_android.cc,PopulateDropDataFromEventparses these JNI string arrays into nativebase::FilePathstructures insideDropData::filenames:for (const auto& info : filenames) { CHECK_EQ(info.size(), 2u); drop_data->filenames.emplace_back(base::FilePath(info[0]), base::FilePath(info[1])); } -
Renderer Taint and Filter Bypass: Because the drag originates from an external Android application, the browser process hardcodes
drop_data_->did_originate_from_renderer = falseinsidecontent/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::FilterDropDatais called, the file list is spared from being cleared becausedid_originate_from_rendereris false. -
Capability and Read Privilege Grant: During drop processing,
RenderWidgetHostImpl::DragTargetDropinvokesPrepareDropDataForChildProcess, which routes toPrepareDataTransferFilenamesForChildProcessincontent/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)
- 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") - 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.
- The drop target is an attacker-controlled page containing a standard drag-and-drop event listener.
- 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:
- Ensure that any received URIs have a valid and expected scheme (such as
content://orfile://). - 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 returnstrue.
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.