CVE-2026-95328
Overview
Background
- Confused deputy
- a security flaw where a privileged component is tricked into misusing its authority on behalf of a less-privileged caller, here Chrome’s browser process acting on attacker-controlled clipboard content.
- `ClipData.Item.coerceToText()`
- an Android
ClipboardManagerAPI that resolves a clipboard item to a string, including dereferencingcontent://URIs, which can force the calling app to read internal resources it has permission to but the source did not. - `Clipboard.getCoercedText()`
- Chrome’s wrapper in
org.chromium.ui.base.Clipboard(ClipboardImpl) that coerces clipboard text while rejecting untrusted internal URIs, centralizing the confused-deputy protections. - `FindToolbar`
- the in-page find bar UI (
org.chromium.chrome.browser.findinpage.FindToolbar) whose queryEditTexthandles paste viaonTextContextMenuItem().
Root Cause Analysis
The onTextContextMenuItem() handler in FindToolbar handled the android.R.id.paste action by reading the system clipboard directly through ClipboardManager.getPrimaryClip() and calling ClipData.Item.coerceToText() on each item. Because coerceToText() will dereference a content:// URI using the browser’s own permissions, an attacker who placed a crafted URI on the clipboard could induce Chrome to resolve internal/privileged content and paste its contents into the find field — the classic confused-deputy pattern. The invariant violated is that all clipboard coercion in Chrome must pass through the guarded Clipboard/ClipboardImpl path that filters untrusted internal URIs, which FindToolbar bypassed.
The fix routes paste through Clipboard.getInstance().getCoercedText(), so untrusted internal URIs are rejected before any text is produced. It also handles a null result by returning true to consume the event, preventing TextView’s default paste handler from re-running the unguarded coercion as a fallback.
ClipData.Item.coerceToText() API instead of Chrome’s guarded Clipboard.getCoercedText(), which meant the confused-deputy URI filtering never ran; the fix simply funnels the paste path through the guarded API and consumes the event on a null result.Attack Path
- Plant a malicious clipboard item
The attacker arranges for the Android clipboard’s primary clip to hold a crafted
content://URI (or similar item) whose coercion would leak internal resources. (inferred) - Trigger the find bar paste
The user (or an automated flow) opens
FindToolbarand invokes Paste on the queryEditText, dispatchingandroid.R.id.pastetoonTextContextMenuItem(). - Unguarded coercion runs
The handler calls
clipData.getItemAt(i).coerceToText(getContext()), causing the browser to dereference the attacker’s URI with its own permissions instead of rejecting it. - Privileged content is surfaced
The coerced internal content is written into the find field via
text.replace(min, max, builder.toString()), exposing data the attacker’s context could not otherwise read. (inferred)
Impact Assessment
FindToolbar. The advisory classifies this as a LogicError / confused deputy in Mobile and states no exploited-in-the-wild status. Any escalation beyond exposing the coerced content into the find field is not demonstrated by the patch. (inferred)Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/findinpage/FindToolbar.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/findinpage/FindToolbar.java
Audit Directions
- Raw clipboard coercionSearch for direct uses of
ClipData.Item.coerceToText(),ClipboardManager.getPrimaryClip(), orgetSystemService(Context.CLIPBOARD_SERVICE)in Java UI code that should instead go throughClipboard/ClipboardImpl. - Custom paste handlersAudit overrides of
onTextContextMenuItem()andTextViewpaste paths across Chrome’s Android widgets for bypasses of the guarded clipboard API, including missingnull-consume handling that lets the default paste re-run. - URI-dereferencing deputiesLook for other spots where the browser resolves
content://or similar URIs from untrusted sources using its own permissions without the internal-URI filtering.
Patch
From 180b36d71ad9533a4bddac90ac3ef1fadc0d9b40 Mon Sep 17 00:00:00 2001
From: Charles Cai <charlesyc@google.com>
Date: Mon, 31 Aug 2026 15:58:00 -0700
Subject: [PATCH] FindToolbar: Use guarded Clipboard API for paste
FindToolbar previously called ClipData.Item.coerceToText() directly,
bypassing the confused-deputy protections in ClipboardImpl.
Update FindToolbar to use Clipboard.getInstance().getCoercedText()
so untrusted internal URIs are rejected when pasting plain text.
If coerced text is null, consume the event to prevent falling back
to TextView's default paste handler.
Bug: 553148673
Change-Id: Ief6bdcfafa0871f40e3b856f21ba0238cbe3a316
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8324467
Commit-Queue: Charles Cai <charlesyc@google.com>
Reviewed-by: Charles Hager <clhager@google.com>
Reviewed-by: Andrew Grieve <agrieve@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1689357}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/findinpage/FindToolbar.java b/chrome/android/java/src/org/chromium/chrome/browser/findinpage/FindToolbar.java
index 2ac6b54..f19611a 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/findinpage/FindToolbar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/findinpage/FindToolbar.java
@@ -9,8 +9,6 @@
import android.animation.Animator;
import android.annotation.SuppressLint;
-import android.content.ClipData;
-import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
@@ -64,6 +62,7 @@
import org.chromium.components.find_in_page.FindMatchRectsDetails;
import org.chromium.components.find_in_page.FindNotificationDetails;
import org.chromium.components.find_in_page.FindResultBar;
+import org.chromium.ui.base.Clipboard;
import org.chromium.ui.base.UiAndroidFeatureList;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.text.EmptyTextWatcher;
@@ -174,34 +173,29 @@
@Override
public boolean onTextContextMenuItem(int id) {
if (id == android.R.id.paste) {
- ClipboardManager clipboard =
- (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clipData = clipboard.getPrimaryClip();
- if (clipData != null) {
- // Convert the clip data to a simple string
- StringBuilder builder = new StringBuilder();
- for (int i = 0; i < clipData.getItemCount(); i++) {
- builder.append(clipData.getItemAt(i).coerceToText(getContext()));
- }
-
- // Identify how much of the original text should be replaced
- var text = getText();
- assumeNonNull(text);
- int min = 0;
- int max = text.length();
-
- if (isFocused()) {
- final int selStart = getSelectionStart();
- final int selEnd = getSelectionEnd();
-
- min = Math.max(0, Math.min(selStart, selEnd));
- max = Math.max(0, Math.max(selStart, selEnd));
- }
-
- Selection.setSelection(text, max);
- text.replace(min, max, builder.toString());
+ String textToPaste = Clipboard.getInstance().getCoercedText();
+ if (textToPaste == null) {
+ // Consume the paste action to avoid falling back to TextView's default paste.
return true;
}
+
+ // Identify how much of the original text should be replaced
+ var text = getText();
+ assumeNonNull(text);
+ int min = 0;
+ int max = text.length();
+
+ if (isFocused()) {
+ final int selStart = getSelectionStart();
+ final int selEnd = getSelectionEnd();
+
+ min = Math.max(0, Math.min(selStart, selEnd));
+ max = Math.max(0, Math.max(selStart, selEnd));
+ }
+
+ Selection.setSelection(text, max);
+ text.replace(min, max, textToPaste);
+ return true;
}
return super.onTextContextMenuItem(id);
}