CVE-2026-17766
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java |
modified | |
ifui/android/java/src/org/chromium/ui/base/ClipboardImpl.java |
modified |
Files Changed
base/android/java/src/org/chromium/base/ContentUriUtils.javachrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.javaui/android/java/src/org/chromium/ui/base/ClipboardImpl.javaui/android/javatests/src/org/chromium/ui/base/ClipboardAndroidTest.java
Patch
From 1164d3993bed6a3389c45bd2a52952c4b7f55e55 Mon Sep 17 00:00:00 2001
From: Charles Cai <charlesyc@google.com>
Date: Tue, 16 Jun 2026 12:00:13 -0700
Subject: [PATCH] Clipboard: Add defense against confused deputy URI reading
Add a new "ClipboardConfusedDeputyDefense" feature flag to protect against
malicious apps leveraging Chrome to read private own-app content URIs.
ClipboardImpl now rejects URIs originating from itself unless the URI
was explicitly shared/copied by Chrome.
Bug: 511799537
Change-Id: I6b021e50b912b1bf6b8857c1c520a6edcfff9253
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7933728
Reviewed-by: Sky Malice <skym@chromium.org>
Reviewed-by: Yaron Friedman <yfriedman@chromium.org>
Commit-Queue: Charles Cai <charlesyc@google.com>
Reviewed-by: Tomasz Wiszkowski <ender@google.com>
Cr-Commit-Position: refs/heads/main@{#1647778}
---
diff --git a/base/android/java/src/org/chromium/base/ContentUriUtils.java b/base/android/java/src/org/chromium/base/ContentUriUtils.java
index 7eda2c9..7bb1bb2f 100644
--- a/base/android/java/src/org/chromium/base/ContentUriUtils.java
+++ b/base/android/java/src/org/chromium/base/ContentUriUtils.java
@@ -8,6 +8,8 @@
import android.content.ContentResolver;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
@@ -711,6 +713,27 @@
}
}
+ public static boolean isUriFromThisApp(@Nullable Uri uri) {
+ return isUriFromThisApp(uri, ContextUtils.getApplicationContext());
+ }
+
+ /**
+ * Returns whether the content URI is served by a ContentProvider belonging to the current
+ * application (i.e. running under the same UID).
+ *
+ * @param uri The URI to check.
+ * @param context The context to retrieve package and provider info.
+ * @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;
+ String authority = uri.getAuthority();
+ if (TextUtils.isEmpty(authority)) return false;
+ PackageManager pm = context.getPackageManager();
+ ProviderInfo info = pm.resolveContentProvider(authority, 0);
+ return info != null && TextUtils.equals(info.packageName, context.getPackageName());
+ }
+
@NativeMethods
interface Natives {
void addFileInfoToVector(
diff --git a/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java b/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
index 0756d2f..b4ad6fe 100644
--- a/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
+++ b/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
@@ -6,8 +6,6 @@
import android.content.ContentResolver;
import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.Build;
import android.os.ext.SdkExtensions;
@@ -461,19 +459,7 @@
return false;
}
- PackageManager pm = context.getPackageManager();
- ProviderInfo providerInfo = pm.resolveContentProvider(authority, 0);
- // If the provider cannot be resolved, it is either not registered or belongs to a
- // third-party app that Chrome cannot query due to Android package visibility restrictions.
- // In either case, it is not Chrome's provider, so it is safe to allow.
- if (providerInfo == null) {
- return true;
- }
-
- String myPackageName = context.getPackageName();
- // We only restrict URIs pointing to Chrome's own providers. Third-party providers
- // are responsible for their own security.
- if (!myPackageName.equals(providerInfo.packageName)) {
+ if (!ContentUriUtils.isUriFromThisApp(uri, context)) {
return true;
}
diff --git a/ui/android/java/src/org/chromium/ui/base/ClipboardImpl.java b/ui/android/java/src/org/chromium/ui/base/ClipboardImpl.java
index b407173..a7671732 100644
--- a/ui/android/java/src/org/chromium/ui/base/ClipboardImpl.java
+++ b/ui/android/java/src/org/chromium/ui/base/ClipboardImpl.java
@@ -102,11 +102,19 @@
// getPrimaryClip() has been observed to throw unexpected exceptions for some devices (see
// crbug.com/654802 and b/31501780)
try {
- return mClipboardManager
- .getPrimaryClip()
- .getItemAt(0)
- .coerceToText(mContext)
- .toString();
+ ClipData.Item item = mClipboardManager.getPrimaryClip().getItemAt(0);
+
+ // Reject URIs that point to this app when pasting as text. This prevents malicious
+ // apps from using us to read our own private files via coerceToText().
+ if (UiAndroidFeatureMap.isEnabled(
+ UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_TEXT)) {
+ Uri uri = item.getUri();
+ if (item.getText() == null && ContentUriUtils.isUriFromThisApp(uri)) {
+ return null;
+ }
+ }
+
+ return item.coerceToText(mContext).toString();
} catch (Exception e) {
return null;
}
@@ -302,6 +310,16 @@
Uri uri = getImageUri();
if (uri == null) return null;
+ // Only honor URIs originating from this app when they match the exact one recorded during a
+ // copy operation. Other apps' URIs are bounded by the OS grant model.
+ if (UiAndroidFeatureMap.isEnabled(
+ UiAndroidFeatures.CLIPBOARD_CONFUSED_DEPUTY_DEFENSE_IMAGES)) {
+ if (ContentUriUtils.isUriFromThisApp(uri)
+ && !uri.equals(getImageUriIfSharedByThisApp())) {
+ return null;
+ }
+ }
+
ContentResolver cr = ContextUtils.getApplicationContext().getContentResolver();
String mimeType = cr.getType(uri);
if (!PNG_MIME_TYPE.equalsIgnoreCase(mimeType)) {
@@ -375,7 +393,14 @@
ClipData clipData = mClipboardManager.getPrimaryClip();
for (int i = 0; i < clipData.getItemCount(); i++) {
Uri uri = clipData.getItemAt(i).getUri();
- if (uri != null && ContentUriUtils.isOpenableFile(uri)) {
+ // 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 (ContentUriUtils.isOpenableFile(uri)) {
String uriString = uri.toString();
String displayName = ContentUriUtils.maybeGetDisplayName(uriString);
if (displayName == null) {
@@ -398,7 +423,14 @@
ClipData clipData = mClipboardManager.getPrimaryClip();
for (int i = 0; i < clipData.getItemCount(); i++) {
Uri uri = clipData.getItemAt(i).getUri();
- if (uri != null && ContentUriUtils.isOpenableFile(uri)) {
+ // 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 (ContentUriUtils.isOpenableFile(uri)) {
return true;
}
}
diff --git a/ui/android/javatests/src/org/chromium/ui/base/ClipboardAndroidTest.java b/ui/android/javatests/src/org/chromium/ui/base/ClipboardAndroidTest.java
index 93c070df..527007c 100644
--- a/ui/android/javatests/src/org/chromium/ui/base/ClipboardAndroidTest.java
+++ b/ui/android/javatests/src/org/chromium/ui/base/ClipboardAndroidTest.java
@@ -4,10 +4,17 @@
package org.chromium.ui.base;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.net.Uri;
import android.os.Build;
import android.text.SpannableString;
import android.text.Spanned;
@@ -21,9 +28,14 @@
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
+import org.junit.Rule;
Original Bug Report
Missing URI validation in ClipboardImpl.getPng allows local file disclosure
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Confused Deputy vulnerability exists in Chrome for Android where ClipboardImpl.getPng() blindly opens content:// URIs retrieved from the system clipboard. A malicious zero-permission app could place a URI pointing to a sensitive file on the clipboard, causing Chrome to read and disclose it to an attacker-controlled website during a paste action.
Affected files:
ui/android/java/src/org/chromium/ui/base/ClipboardImpl.java
Estimated timestamp from git blame: 2022-02-22
Summary
A potential Confused Deputy vulnerability exists in Chrome for Android’s clipboard handling. Specifically, ClipboardImpl.getPng() retrieves content:// URIs from the system clipboard and opens them using Chrome’s ContentResolver without validating the URI’s origin or destination. This could allow a zero-permission malicious app to trick Chrome into reading sensitive local files (e.g., photos from MediaStore, assuming Chrome holds storage permissions) and disclosing their contents to a web page upon a user’s paste action.
Technical Details
When a web page requests image data from the clipboard (e.g., via event.clipboardData.items[i].getAsFile() during a paste event), the renderer process makes a synchronous Mojo call to the browser process to fetch the data. On Android, this eventually delegates to ui/android/java/src/org/chromium/ui/base/ClipboardImpl.java.
The implementation of getPng() performs the following steps:
- It calls
getImageUri(), which retrieves a URI from the Android system’sClipboardManager. - It calls
ContentResolver.getType(uri)to determine the MIME type. On Android, this typically relies on the file extension rather than content inspection. - If the MIME type is
image/png, it directly callsContentResolver.openAssetFileDescriptor(uri, "r")to read the raw bytes.
The vulnerability is that Chrome does not verify if the URI is safe to open. It blindly passes the attacker-provided URI to ContentResolver. A malicious application can set the system clipboard to a URI such as content://media/external/images/media/1.png. When the user pastes into an attacker-controlled web page, Chrome reads the file using its own potentially elevated permissions and returns the raw data to the renderer, allowing the page to exfiltrate it.
Impact Constraints:
- The attack requires a malicious local application to poison the system clipboard.
- The attack requires a user-initiated paste gesture on an attacker-controlled web page.
- The target file must be accessible via a
content://URI and generally requires an extension that resolves to animage/*MIME type (e.g.,.png) to bypass transcoding checks.
Potential Reproduction Steps
Note: These are suggested steps; our tooling agent cannot execute code to verify a working proof of concept.
- Install a malicious zero-permission app that sets the system clipboard to a
ClipDatacontaining a URI pointing to a sensitive file accessible by Chrome (e.g., a photo in the MediaStore). - In Chrome, navigate to a website controlled by the attacker that listens for the
pasteevent. - The user is tricked into performing a paste gesture on the page.
- The website’s JavaScript reads the file from
event.clipboardData.itemsand exfiltrates it.
Recommended Fix
Implement a validation check in ClipboardImpl.getPng() and/or getImageUri() to ensure that clipboard URIs do not point to sensitive local data before attempting to open them. Chrome should perhaps only accept URIs that it placed on the clipboard itself, or restrict reading to a safe subset of authorities.
Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.