Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate Implementation in Browser UI
DescriptionInappropriate Implementation in Browser UI
ComponentBrowser UI
Sub-componentAndroid
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker329476341
Fix commit9e3dedd19349 (chromium/src) +69/-0
CWECWE-1021
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedKhalil Zhani
Reported2024-03-14 · bounty $2000 (per vendor advisory)
Fix landed2025-01-07 (public git)
Disclosed2025-03-04
Patch gap56 days — fix public in source before the advisory

Background

Floating action bar
The Android ActionMode menu (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.Callback2 hook 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.

Key insight
The core mistake was reporting the selection rectangle to the framework without accounting for the space consumed by Chrome’s own top controls, letting the floating action bar be placed over the omnibox/toolbar. The fix corrects the geometry fed to the anchor heuristic so the bar is forced below the selection whenever it would otherwise collide with the top controls.

Attack Path

  1. 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.
  2. Trigger selection The user long-presses or otherwise selects the crafted text, which invokes ChromeActionModeHandler and reports the near-top content rect via onGetContentRect.
  3. 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.
  4. 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

An attacker gains a browser-UI spoofing/occlusion primitive on Chrome for Android, where a page-driven floating action bar can overlap the security-relevant top controls (omnibox and toolbar) and mislead the user about the page’s origin or trust state. This occurs in the browser process’s UI layer and requires only that the user select text near the top of the viewport while the top controls are visible, with no additional privileges. The impact is limited to inappropriate implementation of the UI (visual overlap/confusion) rather than memory corruption or direct code execution.

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/ChromeActionModeHandler.java
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/ChromeActionModeHandler.java
  • chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
  • chrome/android/junit/src/org/chromium/chrome/browser/ChromeActionModeHandlerUnitTest.java

Audit Directions

  • Framework overlay anchoring
    Review 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 UI
    Audit 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 injection
    Look for UI components that make placement or sizing decisions without a BrowserControlsStateProvider or equivalent visibility signal, since they cannot reason about the space occupied by browser chrome.
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();
Loading diff…

Original Bug Report

reported by ch...@gmail.com

Text Selection menu able to overlap URL bar

Steps to reproduce the problem

  1. Open the testcase.html or navigate to https://lbstyle.github.io/repro.html
  2. 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

View on issue tracker