Low watch CVSS 9.8 chrome Spoofing 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
9.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI
DescriptionIncorrect security UI
ComponentChromium
Sub-componentUI>Browser>Mobile>Toolbar
Bug ClassSpoofing
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.
Tracker467448811
Fix commitd9cb337ff4a7 (chromium/src) +68/-44
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedKhalil Zhani
Reported2025-12-10 · bounty $2000 (per vendor advisory)
Fix landed2025-12-11 (public git)
Disclosed2026-01-13
Patch gap33 days — fix public in source before the advisory

Background

`BrowserControlsManager`
the Android Chrome class that positions and shows/hides the browser controls (toolbar/URL bar container, mControlContainer) as the page scrolls or enters fullscreen.
`BrowserControlsState`
the constraint enum (SHOWN, HIDDEN, BOTH) that says whether the controls must be locked visible, locked hidden, or may move freely.
Constraint sources
constraints can change from the browser side via mBrowserVisibilityDelegate (a BrowserStateBrowserControlsVisibilityDelegate) or from the tab side, e.g. when the page focuses a form field and the tab requests SHOWN; the tab path reaches the manager through a tab controls observer (onOffsetTagsInfoChanged in the test).
`mForceRelayoutOnVisibilityChange`
a flag that makes the next visibility update force a requestLayout() on the control container so a visibility change actually gets redrawn instead of leaving the toolbar hidden.

Root Cause Analysis

Before the patch, the logic that reacts to a SHOWN/BOTH constraint — calling setPositionsForTabToNonFullscreen() (unless a render-driven show is possible), calling scheduleVisibilityUpdate(), and, when getAndroidControlsVisibility() != View.VISIBLE, setting mForceRelayoutOnVisibilityChange = true — lived only inside an anonymous lambda registered as an observer on mBrowserVisibilityDelegate. The commit message states this “only runs for browserdelegate-driven constraint changes, which misses e.g. form-field focus driven SHOWN”: when the constraint came from the tab, the tab-observer path only called notifyConstraintsChanged(...) for offset tags and never ran the show/relayout logic.

As a result, a tab-driven request to lock the controls visible could leave the Android control container (which hosts the URL bar / security indicator) hidden or not redrawn, violating the invariant that a SHOWN constraint results in visible browser controls.

The fix extracts the lambda body verbatim into a new private method onConstraintsChanged(int), registers it as the delegate observer via this::onConstraintsChanged, and additionally calls onConstraintsChanged(constraints) from the tab-driven offset-tags path before notifying observers (now renamed notifyOffsetTagsChanged). The new unit test testConstraintChangeFromTab confirms that a tab-originated SHOWN with hidden, bottom-positioned controls now triggers requestLayout() on the container.

Key insight
The “constraints became SHOWN → force the controls visible and relayout” handling was wired to only one of the two constraint sources (the browser visibility delegate), so tab-driven constraint changes skipped it. The fix centralises it in onConstraintsChanged and invokes it from both the delegate observer and the tab offset-tags path.

Attack Path

  1. Page hides controls Web content on Chrome for Android scrolls or otherwise causes the browser controls (and with them the URL bar) to be hidden or moved off-screen, a normal state in the tested scenario (controls bottom-positioned and hidden).
  2. Tab-driven `SHOWN` constraint The page triggers a tab-side constraint change such as focusing a form field, which the commit message names as a SHOWN source that bypassed the delegate observer.
  3. Show/relayout logic skipped Pre-patch, the tab path only notified offset-tag observers; setPositionsForTabToNonFullscreen(), scheduleVisibilityUpdate(), and the forced relayout were never run, so the control container could stay hidden or un-redrawn.
  4. Misleading UI state While the browser believes controls are locked shown, the user may not see the URL bar/security indicator, which an attacker could use to make page content appear to be browser UI or obscure the true origin (inferred).

Impact Assessment

An attacker controlling ordinary web content in Chrome for Android could leave the browser controls (URL bar / security indicators) hidden when they should have been forced visible, yielding incorrect security UI in the browser process’s UI layer; the diff touches only Android Java code, so the issue is Android-specific. Preconditions inferred from the diff and commit message are a page-induced tab-side SHOWN constraint (e.g. form-field focus) while controls are hidden, possibly with a user interaction to focus the field; exact exploitation as an origin-spoofing vector is (inferred). The vendor rates this as low severity (“Incorrect security UI”); the advisory provided does not state in-the-wild exploitation.

Changed Functions

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

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.java
  • chrome/android/junit/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManagerUnitTest.java

Audit Directions

  • Split event sources for security UI
    Look for other browser-controls or omnibox visibility handlers in chrome/android/.../fullscreen/ and browser_controls/ that are subscribed to only one of several constraint/visibility producers (delegate, tab, fullscreen API, keyboard, offset tags).
  • State recorded but not rendered
    Audit places where a visibility/constraint value is updated but the actual View visibility or layout is deferred (e.g. postOnAnimation, touch-in-progress delays), checking that every path eventually forces a redraw.
  • Page-controllable UI toggles
    Review page-triggerable transitions (fullscreen exit, form focus, virtual keyboard, scroll-driven hiding) for cases where the URL bar can remain hidden or stale while page content occupies its area.
From d9cb337ff4a744969259fa8cac73512f4f5fe2a4 Mon Sep 17 00:00:00 2001
From: Patrick Noland <[email protected]>
Date: Thu, 11 Dec 2025 15:20:05 -0800
Subject: [PATCH] Force relayout for tab-driven constraint changes

This logic currently only runs for browserdelegate-driven constraint
changes, which misses e.g. form-field focus driven SHOWN.

Bug: 467448811
Change-Id: I5662353216c8faea84a98e2f3edfa584cd6adf2c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7253201
Reviewed-by: Wenyu Fu <[email protected]>
Commit-Queue: Patrick Noland <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1557729}
---

diff --git a/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.java b/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.java
index 1f6a93c..9a78efa6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManager.java
@@ -222,47 +222,7 @@
         mBrowserVisibilityDelegate =
                 new BrowserStateBrowserControlsVisibilityDelegate(
                         mHtmlApiHandler.getPersistentFullscreenModeSupplier());
-        mBrowserVisibilityDelegate.addObserver(
-                (constraints) -> {
-                    if (constraints == BrowserControlsState.SHOWN) {
-                        // When compositor can drive the animation to show controls, do not call
-                        // setPositionsForTabToNonFullscreen to avoid control offset being forced
-                        // set to 0 before the render-driven animation kicks in.
-                        boolean allowRenderDrivenShowConstraint =
-                                ChromeFeatureList.sBrowserControlsRenderDrivenShowConstraint
-                                        .isEnabled();
-                        boolean renderDrivenShowConstraint =
-                                allowRenderDrivenShowConstraint
-                                        && canAnimateNativeBrowserControls();
-                        if (!renderDrivenShowConstraint) {
-                            setPositionsForTabToNonFullscreen();
-                        }
-
-                        // TODO(https://crbug.com/449011189): Maybe cleanup
-                        if (allowRenderDrivenShowConstraint) {
-                            RecordHistogram.recordBooleanHistogram(
-                                    "Android.BrowserControls.RenderDrivenShowConstraint",
-                                    renderDrivenShowConstraint);
-                        }
-
-                        // If controls become locked, it's possible we've previously delayed
-                        // actually setting visibility until a touch event is over. In this case, we
-                        // need to trigger an update again now, which should go through due to
-                        // constraints.
-                        scheduleVisibilityUpdate();
-                    }
-
-                    // From https://crbug.com/452885338, https://crbug.com/461532432: When changing
-                    // controls visibility when exiting fullscreen, the visibility change might not
-                    // honor a redraw. We do this through forcing a relayout to avoid the toolbar
-                    // remains hidden.
-                    if ((constraints == BrowserControlsState.SHOWN
-                                    || constraints == BrowserControlsState.BOTH)
-                            && getAndroidControlsVisibility() != View.VISIBLE) {
-                        mForceRelayoutOnVisibilityChange = true;
-                        scheduleVisibilityUpdate();
-                    }
-                });
+        mBrowserVisibilityDelegate.addObserver(this::onConstraintsChanged);
     }
 
     /**
@@ -354,9 +314,10 @@
                                 new OffsetTagConstraints(
                                         0, 0, -(mTopControlsHeight + hairlineHeight), 0);
 
+                        onConstraintsChanged(constraints);
                         // Notify observers of changes before passing tags to native so observers
                         // can set their relevant fields in offsetTagsInfo.
-                        notifyConstraintsChanged(oldOffsetTagsInfo, offsetTagsInfo, constraints);
+                        notifyOffsetTagsChanged(oldOffsetTagsInfo, offsetTagsInfo, constraints);
 
                         offsetTagsInfo
                                 .getConstraints()
@@ -850,7 +811,9 @@
             return;
         }
         final int desiredVisibility = shouldShowAndroidControls() ? View.VISIBLE : View.INVISIBLE;
-        if (mControlContainer.getView().getVisibility() == desiredVisibility) return;
+        if (mControlContainer.getView().getVisibility() == desiredVisibility) {
+            return;
+        }
         mControlContainer.getView().removeCallbacks(mUpdateVisibilityRunnable);
         mControlContainer.getView().postOnAnimation(mUpdateVisibilityRunnable);
     }
@@ -1022,7 +985,45 @@
         }
     }
 
-    private void notifyConstraintsChanged(
+    private void onConstraintsChanged(@BrowserControlsState int constraints) {
+        if (constraints == BrowserControlsState.SHOWN) {
+            // When compositor can drive the animation to show controls, do not call
+            // setPositionsForTabToNonFullscreen to avoid control offset being forced
+            // set to 0 before the render-driven animation kicks in.
+            boolean allowRenderDrivenShowConstraint =
+                    ChromeFeatureList.sBrowserControlsRenderDrivenShowConstraint.isEnabled();
+            boolean renderDrivenShowConstraint =
+                    allowRenderDrivenShowConstraint && canAnimateNativeBrowserControls();
+            if (!renderDrivenShowConstraint) {
+                setPositionsForTabToNonFullscreen();
+            }
+
+            // TODO(https://crbug.com/449011189): Maybe cleanup
+            if (allowRenderDrivenShowConstraint) {
+                RecordHistogram.recordBooleanHistogram(
+                        "Android.BrowserControls.RenderDrivenShowConstraint",
+                        renderDrivenShowConstraint);
+            }
+
+            // If controls become locked, it's possible we've previously delayed
+            // actually setting visibility until a touch event is over. In this case, we
+            // need to trigger an update again now, which should go through due to
+            // constraints.
+            scheduleVisibilityUpdate();
+        }
+
+        // From https://crbug.com/452885338, https://crbug.com/461532432: When changing
+        // controls visibility when exiting fullscreen, the visibility change might not
+        // honor a redraw. We do this through forcing a relayout to avoid the toolbar
+        // remains hidden.
+        if ((constraints == BrowserControlsState.SHOWN || constraints == BrowserControlsState.BOTH)
+                && getAndroidControlsVisibility() != View.VISIBLE) {
+            mForceRelayoutOnVisibilityChange = true;
+            scheduleVisibilityUpdate();
+        }
+    }
+
+    private void notifyOffsetTagsChanged(
             BrowserControlsOffsetTagsInfo oldOffsetTagsInfo,
             BrowserControlsOffsetTagsInfo offsetTagsInfo,
             @BrowserControlsState int constraints) {
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManagerUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManagerUnitTest.java
index 60399a7..e1d8392 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManagerUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/fullscreen/BrowserControlsManagerUnitTest.java
@@ -54,6 +54,7 @@
 import org.chromium.cc.input.BrowserControlsState;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.ActivityTabProvider;
+import org.chromium.chrome.browser.browser_controls.BrowserControlsOffsetTagsInfo;
 import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
 import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider.ControlsPosition;
 import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
@@ -784,6 +785,28 @@
                         anyBoolean());
     }
 
+    @Test
+    public void testConstraintChangeFromTab() {
+        remakeWithoutSpy();
+        notifyAddTab(mTab);
+        mActivityTabProvider.setForTesting(mTab);
+        // Put the control container in a hidden state and bottom-positioned.
+        mBrowserControlsManager.setControlsPosition(
+                ControlsPosition.BOTTOM, 0, 0, 0, TOOLBAR_HEIGHT, 10, TOOLBAR_HEIGHT);
+        ShadowLooper.idleMainLooper();
+        Mockito.clearInvocations(mContainerView);
+        // Locking the controls via the TabControlsObserver should check for forced relayout.
+        mBrowserControlsManager
+                .getTabControlsObserverForTesting()
+                .onOffsetTagsInfoChanged(
+                        mTab,
+                        new BrowserControlsOffsetTagsInfo(),
+                        new BrowserControlsOffsetTagsInfo(),
+                        BrowserControlsState.SHOWN);
+        ShadowLooper.idleMainLooper();
+        verify(mContainerView).requestLayout();
+    }
+
     private void verifyUpdateOffsetTagDefinitions(
             OffsetTagConstraints top, OffsetTagConstraints content, OffsetTagConstraints bottom) {
         BrowserControlsOffsetTagConstraints expectedConstraints =
Loading diff…

Original Bug Report

reported by [email protected]

Mini bar not rendered when omnibox is hidden (similar to issue 461532432)

Steps to reproduce the problem

  1. Open the testcase.
  2. Scroll down to the middle of the page until the omnibox disappears (normal fullscreen scroll behavior).
  3. Tap inside the <textarea>.

Problem Description

This issue appears very similar to Chromium issue 461532432.

In Chrome Canary on Android, when the omnibox auto-hides during scroll, tapping inside a <textarea> causes the keyboard’s accessory bar (the mini bar above the virtual keyboard) to not render at all. The space where the accessory bar normally appears is still allocated, but it is completely empty (no icons, no background, just blank UI).

This empty UI zone appears exactly where a user expects browser controls. A website can use CSS to place a fake omnibox just below it, enabling UI spoofing.

Summary

Mini bar not rendered when omnibox is hidden (similar to issue 461532432)

Additional Data

Category: Security
Chrome Channel: Canary
Regression: N/A \

View on issue tracker