CVE-2025-1917
Overview
Background
- Floating action bar
- The Android
ActionModemenu (Copy, Share, Web Search, etc.) that floats over a web page when the user selects text. - Top controls
- The Chrome-for-Android browser chrome (the URL/omnibox and toolbar) drawn at the top of the content area, whose visibility is tracked by
BrowserControlsStateProvider. - `onGetContentRect`
- The
ActionMode.Callback2hook that reports the bounding rectangle of the selected content so the framework can decide where to anchor the floating action bar. - Anchor heuristic
- The Android framework’s rule that places the floating action bar above the content rect when there is room, and below it otherwise.
Root Cause Analysis
When the user selected text near the top of the viewport, ChromeActionModeHandler passed the raw selection rectangle to the Android framework via onGetContentRect, and the framework’s anchor heuristic saw sufficient vertical space above the selection and drew the floating action bar there. Because Chrome’s top controls (omnibox and toolbar) occupy that same top region, the floating bar was rendered overlapping the browser chrome, so its buttons visually sat on top of trustworthy browser UI. The violated invariant is that page-originated overlay UI must never occlude or be confusable with the browser’s own security-relevant controls such as the omnibox.
The fix injects a BrowserControlsStateProvider into the handler and, in onGetContentRect, checks whether controls are visible and whether the selection sits within roughly two control heights of the top; if so it inflates outRect upward so the framework concludes there is not enough room above the text and instead anchors the action bar below the selection. This removes the overlap because the assumed action-mode height is approximated as one controlsHeight, keeping the bar clear of the top controls.
Attack Path
- Craft a lure page The attacker serves a page with selectable text (or a fake control) positioned near the very top of the viewport, just under where Chrome’s toolbar sits.
- Trigger selection
The user long-presses or otherwise selects the crafted text, which invokes
ChromeActionModeHandlerand reports the near-top content rect viaonGetContentRect. - Force top anchoring Because there appears to be room above the selection, the framework anchors the floating action bar in the top-control region, overlapping the omnibox/toolbar.
- Exploit the overlap The overlapping bar obscures or is confusable with genuine browser chrome, enabling UI confusion or spoofing where the user misattributes the page’s overlay to the trusted browser UI.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/ChromeActionModeHandler.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/ChromeActionModeHandler.javachrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.javachrome/android/junit/src/org/chromium/chrome/browser/ChromeActionModeHandlerUnitTest.java
Audit Directions
- Framework overlay anchoringReview every place Chrome reports geometry (content rects, anchor points, popup positions) to Android framework UI, checking that top controls and other browser chrome are subtracted before the framework chooses placement.
- Page-controlled overlays over trusted UIAudit floating menus, popups, tooltips, and toasts that can be triggered by page content for their ability to overlap the omnibox/toolbar or other security indicators.
- Missing state injectionLook for UI components that make placement or sizing decisions without a
BrowserControlsStateProvideror equivalent visibility signal, since they cannot reason about the space occupied by browser chrome.
Patch
From 9e3dedd19349d812d7dba808f07ebee91302623e Mon Sep 17 00:00:00 2001
From: Jinsuk Kim <jinsukkim@chromium.org>
Date: Tue, 07 Jan 2025 09:44:22 -0800
Subject: [PATCH] [Android] Fix floating action bar/top control overlapping bug
In order to avoid the floating bar and the top control overlapping
with each other, this CL ensures that the action bar will be drawn
above the selected text only if there is enough space.
Bug: 329476341
Change-Id: I7095863a845db2aa38a4d9d93efde2f2075e82a3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6148920
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Reviewed-by: Theresa Sullivan <twellington@chromium.org>
Commit-Queue: Jinsuk Kim <jinsukkim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1403053}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActionModeHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActionModeHandler.java
index b658d84..d01c14ba0 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActionModeHandler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActionModeHandler.java
@@ -23,6 +23,7 @@
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
+import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.readaloud.ReadAloudController;
@@ -60,12 +61,15 @@
* @param searchCallback Callback to run when search action is selected in the action mode.
* @param shareDelegateSupplier The {@link Supplier} of the {@link ShareDelegate} that will be
* notified when a share action is performed.
+ * @param controlsState Provides browser controls visibility state.
+ * @param readAloudControllerSupplier Supplies {@link ReadAloudController}.
*/
public ChromeActionModeHandler(
ActivityTabProvider activityTabProvider,
Callback<String> searchCallback,
boolean showWebSearch,
Supplier<ShareDelegate> shareDelegateSupplier,
+ BrowserControlsStateProvider controlsState,
Supplier<ReadAloudController> readAloudControllerSupplier) {
mInitWebContentsObserver =
(webContents) -> {
@@ -78,6 +82,7 @@
searchCallback,
showWebSearch,
shareDelegateSupplier,
+ controlsState,
readAloudControllerSupplier));
spc.setDropdownMenuDelegate(new ChromeSelectionDropdownMenuDelegate());
};
@@ -124,6 +129,7 @@
private final boolean mShowWebSearch;
private final Supplier<ShareDelegate> mShareDelegateSupplier;
private final Supplier<ReadAloudController> mReadAloudControllerSupplier;
+ private final BrowserControlsStateProvider mControlsState;
// Used for recording UMA histograms.
private long mContextMenuStartTime;
@@ -134,12 +140,14 @@
Callback<String> searchCallback,
boolean showWebSearch,
Supplier<ShareDelegate> shareDelegateSupplier,
+ BrowserControlsStateProvider controlsState,
Supplier<ReadAloudController> readAloudControllerSupplier) {
mTab = tab;
mHelper = getActionModeCallbackHelper(webContents);
mShowWebSearch = showWebSearch;
mSearchCallback = searchCallback;
mShareDelegateSupplier = shareDelegateSupplier;
+ mControlsState = controlsState;
mReadAloudControllerSupplier = readAloudControllerSupplier;
}
@@ -287,6 +295,17 @@
@Override
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
mHelper.onGetContentRect(mode, view, outRect);
+ boolean controlsVisible = mControlsState.getBrowserControlHiddenRatio() < 1.f;
+ int controlsHeight = mControlsState.getTopControlsHeight();
+ if (controlsVisible && outRect.top < 2 * controlsHeight) {
+ // Make |outRect| taller to so the framework thinks there is not enough space
+ // above the selected text to place the floating action mode. This helps the action
+ // mode and the top controls avoid overlapping - the action mode will be positioned
+ // below the text.
+ // The right condition should be |outRect.top < controlsHeight + actionModeHeight|
+ // but we do not know |actionModeHeight|. Assume actionModeHeight ~= controlsHeight.
+ outRect.top -= controlsHeight;
+ }
}
private Set<String> getPackageNames(List<ResolveInfo> list) {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
index 29173959..08cc31e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
@@ -843,6 +843,7 @@
},
showWebSearchInActionMode(),
mShareDelegateSupplier,
+ mBrowserControlsManager,
mReadAloudControllerSupplier);
mCaptureController =
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/ChromeActionModeHandlerUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/ChromeActionModeHandlerUnitTest.java
index b98108e..a08755e 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/ChromeActionModeHandlerUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/ChromeActionModeHandlerUnitTest.java
@@ -14,6 +14,7 @@
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
+import android.graphics.Rect;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
@@ -35,6 +36,7 @@
import org.chromium.base.Callback;
import org.chromium.base.PackageManagerUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.locale.LocaleManagerDelegate;
@@ -62,6 +64,7 @@
@Mock private Menu mMenu;
@Mock private ShareDelegate mShareDelegate;
@Mock private ReadAloudController mReadAloudController;
+ @Mock private BrowserControlsStateProvider mControlsState;
private class TestChromeActionModeCallback
extends ChromeActionModeHandler.ChromeActionModeCallback {
@@ -72,6 +75,7 @@
urlParams -> {},
true,
() -> mShareDelegate,
+ mControlsState,
() -> mReadAloudController);
}
@@ -237,6 +241,51 @@
verify(mReadAloudController).maybePauseForOutgoingIntent(eq(intent));
}
+ @Test
+ public void testAvoidOverlapWithTopControls() {
+ final int topControlsHeight = 150;
+ final int height = 80;
+ Mockito.when(mControlsState.getTopControlsHeight()).thenReturn(topControlsHeight);
+
+ // Set up for the case where top controls are hidden.
+ Mockito.when(mControlsState.getBrowserControlHiddenRatio()).thenReturn(1.f);
+
+ // If there's enough space between the selected text and the top of the content view for
+ // action mode, the content rect is left untouched.
+ int top = topControlsHeight * 3;
+ Rect outRect = new Rect(20, top, 500, top + height);
+ mActionModeCallback.onGetContentRect(mActionMode, null, outRect);
+ Assert.assertEquals(top, outRect.top);
+ Assert.assertEquals(height, outRect.height());
+
+ // Not enough space for action mode to fit in. The content rect is left untouched.
+ top = topControlsHeight;
+ outRect = new Rect(20, top, 500, top + height);
+ mActionModeCallback.onGetContentRect(mActionMode, null, outRect);
+ Assert.assertEquals(top, outRect.top);
+ Assert.assertEquals(height, outRect.height());
+
+ // Set up for the case where top controls are visible.
+ Mockito.when(mControlsState.getBrowserControlHiddenRatio()).thenReturn(0.f);
+
+ // We have enough space for action mode to fit in. The content rect is left untouched.
+ top = topControlsHeight * 3;
+ outRect = new Rect(20, top, 500, top + height);
+ mActionModeCallback.onGetContentRect(mActionMode, null, outRect);
+ Assert.assertEquals(top, outRect.top);
+ Assert.assertEquals(height, outRect.height());
+
+ // Not enough space for action mode to fit in. Verify that |onGetContentRect| bloated
+ // the content rect (top got taller) so action mode won't fit between the top controls
+ // and the selected text, therefore will be positioned below the text. This helps action
+ // mode avoid overlapping top controls.
+ top = topControlsHeight;
+ outRect = new Rect(20, top, 500, top + height);
+ mActionModeCallback.onGetContentRect(mActionMode, null, outRect);
+ Assert.assertEquals(top - topControlsHeight, outRect.top);
+ Assert.assertEquals(topControlsHeight + height, outRect.height());
+ }
+
private ResolveInfo createResolveInfo(String packageName) {
ResolveInfo resolveInfo = new ResolveInfo();
ActivityInfo activityInfo = new ActivityInfo();
Original Bug Report
Text Selection menu able to overlap URL bar
Steps to reproduce the problem
- Open the testcase.html or navigate to https://lbstyle.github.io/repro.html
- tap anywhere
Problem Description
When the user taps somewhere on the page, the text selection menu can appear over the URL bar, in this case, the user can be manipulated.
Summary
Text Selection menu able to overlap URL bar
Additional Data
Category: Security
Chrome Channel: Not sure
Regression: N/A