Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Autofill
DescriptionUse after free in Autofill
ComponentAutofill
Sub-componentAndroid
Bug ClassUAF
Tracker562236428
Fix commit9b2d17248855 (chromium/src) +286/-91
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
for
components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java
modified

Files Changed

  • components/android_autofill/browser/android_autofill_features.cc
  • components/android_autofill/browser/java/src/org/chromium/components/autofill/AndroidAutofillFeatures.java
  • android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java
  • components/android_autofill/browser/android_autofill_features.h
  • components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java
  • components/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.java
  • components/android_autofill/browser/junit/BUILD.gn
From 9b2d1724885538396738e52ca2e5c9eadd067c2d Mon Sep 17 00:00:00 2001
From: Friedrich Horschig <friedrichh@chromium.org>
Date: Mon, 28 Sep 2026 03:44:22 -0700
Subject: [PATCH] [Android][Autofill] Suppress datalist popup when field is outside visible bounds

AutofillProvider displays a native datalist popup for <input list>
fields via showDatalistPopup(). Previously, the renderer-supplied field
bounds were used directly to position the native popup anchor without
verifying that the field is currently within the container view's
visible bounds.

If a page places a datalist input entirely outside the visible viewport
(e.g. styled off-screen) and focuses it with preventScroll, the
unclamped anchor causes Android's popup layer to pin the dropdown to the
top of the window frame, potentially overlaying the browser toolbar or
embedder UI.

This CL checks whether the transformed field bounds intersect the
visible container screen bounds via clampToVisibleBounds(). If the field
is outside the visible area:
- Any existing datalist popup is dismissed.
- The new popup is suppressed before acquiring an anchor view.

Note that we deliberately clamp the field's anchor bounds rather than
FocusField.absBound because the anchor rect itself is the source of
truth passed to setAnchorViewRect().

Screenshot: http://screen/1g78qg20v5q9g
Kill-switch: AndroidAutofillSuppressOffscreenDatalist
Fixed: 562236428
Change-Id: I5d4448cf93a2cb34233d69e2d7930422d84c8239
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8459967
Commit-Queue: Friedrich Hauser <friedrichh@chromium.org>
Reviewed-by: Timofey Chudakov <tchudakov@google.com>
Cr-Commit-Position: refs/heads/main@{#1705969}
---
diff --git a/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java b/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java
index a96bebe4..20cc290 100644
--- a/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java
+++ b/android_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.java
@@ -64,9 +64,7 @@
      * updating the "LoginCustomFlags" field in tools/metrics/histograms/enums.xml.
      */
     public static final Flag[] sFlagList = {
-        Flag.baseFeature(
-                "useicu4xnormalizer",
-                "Auto-generated flag for useicu4xnormalizer."),
+        Flag.baseFeature("useicu4xnormalizer", "Auto-generated flag for useicu4xnormalizer."),
         Flag.baseFeature("AwMetricsLogTrimming", "Auto-generated flag for AwMetricsLogTrimming."),
         Flag.commandLine(
                 AwSwitches.HIGHLIGHT_ALL_WEBVIEWS,
@@ -255,6 +253,10 @@
                 AndroidAutofillFeatures.ANDROID_AUTOFILL_FIELDS_UPDATED_ON_SELECT_NAME,
                 "Enable updating autofill field origin on select control change."),
         Flag.baseFeature(
+                AndroidAutofillFeatures.ANDROID_AUTOFILL_SUPPRESS_OFFSCREEN_DATALIST_NAME,
+                "When enabled, datalist popups are suppressed when the field is outside the"
+                        + " container view's visible bounds."),
+        Flag.baseFeature(
                 AutofillFeatures.AUTOFILL_ACCEPT_DOM_MUTATION_AFTER_AUTOFILL_SUBMISSION,
                 "Accepts DOM_MUTATION_AFTER_AUTOFILL submissions detected on password forms."),
         Flag.baseFeature(
diff --git a/components/android_autofill/browser/android_autofill_features.cc b/components/android_autofill/browser/android_autofill_features.cc
index 763e647b..18d1a4e8 100644
--- a/components/android_autofill/browser/android_autofill_features.cc
+++ b/components/android_autofill/browser/android_autofill_features.cc
@@ -22,6 +22,7 @@
     &kAndroidAutofillLazyFrameworkWrapper,
     &kAndroidAutofillImprovedVisibilityDetection,
     &kAndroidAutofillFieldsUpdatedOnSelect,
+    &kAndroidAutofillSuppressOffscreenDatalist,
 };
 
 }  // namespace
@@ -49,6 +50,11 @@
 BASE_FEATURE(kAndroidAutofillFieldsUpdatedOnSelect,
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+// If enabled, datalist popups are suppressed when the field is outside the
+// container view's visible bounds.
+BASE_FEATURE(kAndroidAutofillSuppressOffscreenDatalist,
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 static int64_t JNI_AndroidAutofillFeatures_GetFeature(JNIEnv* env,
                                                       int32_t ordinal) {
   return reinterpret_cast<int64_t>(kFeaturesExposedToJava[ordinal]);
diff --git a/components/android_autofill/browser/android_autofill_features.h b/components/android_autofill/browser/android_autofill_features.h
index 116208bf..5a1784b2 100644
--- a/components/android_autofill/browser/android_autofill_features.h
+++ b/components/android_autofill/browser/android_autofill_features.h
@@ -16,6 +16,8 @@
 BASE_DECLARE_FEATURE(kAndroidAutofillUpdateContextForWebContents);
 
 BASE_DECLARE_FEATURE(kAndroidAutofillFieldsUpdatedOnSelect);
+
+BASE_DECLARE_FEATURE(kAndroidAutofillSuppressOffscreenDatalist);
 }  // namespace autofill::features
 
 #endif  // COMPONENTS_ANDROID_AUTOFILL_BROWSER_ANDROID_AUTOFILL_FEATURES_H_
diff --git a/components/android_autofill/browser/java/src/org/chromium/components/autofill/AndroidAutofillFeatures.java b/components/android_autofill/browser/java/src/org/chromium/components/autofill/AndroidAutofillFeatures.java
index 1c1b90a..52766ca 100644
--- a/components/android_autofill/browser/java/src/org/chromium/components/autofill/AndroidAutofillFeatures.java
+++ b/components/android_autofill/browser/java/src/org/chromium/components/autofill/AndroidAutofillFeatures.java
@@ -25,6 +25,8 @@
             "AndroidAutofillImprovedVisibilityDetection";
     public static final String ANDROID_AUTOFILL_FIELDS_UPDATED_ON_SELECT_NAME =
             "AndroidAutofillFieldsUpdatedOnSelect";
+    public static final String ANDROID_AUTOFILL_SUPPRESS_OFFSCREEN_DATALIST_NAME =
+            "AndroidAutofillSuppressOffscreenDatalist";
 
     public static final AndroidAutofillFeatures ANDROID_AUTOFILL_LAZY_FRAMEWORK_WRAPPER =
             new AndroidAutofillFeatures(0, ANDROID_AUTOFILL_LAZY_FRAMEWORK_WRAPPER_NAME);
@@ -32,6 +34,8 @@
             new AndroidAutofillFeatures(1, ANDROID_AUTOFILL_IMPROVED_VISIBILITY_DETECTION_NAME);
     public static final AndroidAutofillFeatures ANDROID_AUTOFILL_FIELDS_UPDATED_ON_SELECT =
             new AndroidAutofillFeatures(2, ANDROID_AUTOFILL_FIELDS_UPDATED_ON_SELECT_NAME);
+    public static final AndroidAutofillFeatures ANDROID_AUTOFILL_SUPPRESS_OFFSCREEN_DATALIST =
+            new AndroidAutofillFeatures(3, ANDROID_AUTOFILL_SUPPRESS_OFFSCREEN_DATALIST_NAME);
 
     private final int mOrdinal;
 
diff --git a/components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java b/components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java
index 51cc103..afa1f8e 100644
--- a/components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java
+++ b/components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java
@@ -667,12 +667,23 @@
     }
 
     /**
-     * Display the simplest popup for the datalist. This is same as WebView's datalist popup in
+     * Displays the simplest popup for the datalist. This is same as WebView's datalist popup in
      * Android pre-o. No suggestion from the autofill service will be presented, No advance features
-     * of AutofillPopup are used.
+     * of AutofillPopup are used. Does nothing if {@code bounds} lie entirely outside the container
+     * view's visible area.
      */
     private void showDatalistPopup(
             String[] datalistValues, String[] datalistLabels, RectF bounds, boolean isRtl) {
+        // Suppress the popup if the field is entirely outside the visible bounds of the container
+        // view; renderer-supplied bounds could otherwise place the dropdown over browser or
+        // embedder UI (crbug.com/562236428). We clamp the anchor bounds themselves rather than
+        // FocusField.absBound.
+        @Nullable Rect clampedBounds = clampToVisibleBounds(transformToWindowBounds(bounds));
+        if (AndroidAutofillFeatures.ANDROID_AUTOFILL_SUPPRESS_OFFSCREEN_DATALIST.isEnabled()
+                && (clampedBounds == null || clampedBounds.isEmpty())) {
+            hideDatalistPopup();
+            return;
+        }
         mDatalistSuggestions = new AutofillDropdownItem[datalistValues.length];
         for (int i = 0; i < mDatalistSuggestions.length; i++) {
             mDatalistSuggestions[i] =
@@ -805,6 +816,10 @@
         return mDatalistPopup;
     }
 
+    void setDatalistPopupForTesting(AutofillPopup datalistPopup) {
+        mDatalistPopup = datalistPopup;
+    }
+
     private int getToolbarOffset() {
         return mWebContents != null
                 ? RenderCoordinates.fromWebContents(mWebContents).getContentOffsetYPixInt()
diff --git a/components/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.java b/components/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.java
index 489c6990..9249e46 100644
--- a/components/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.java
+++ b/components/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.java
@@ -12,6 +12,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
@@ -48,6 +49,7 @@
 import org.chromium.content.browser.RenderCoordinatesImpl;
 import org.chromium.content_public.browser.WebContents;
 import org.chromium.ui.base.ImmutableWeakReference;
+import org.chromium.ui.base.ViewAndroidDelegate;
 import org.chromium.ui.base.WindowAndroid;
 import org.chromium.ui.display.DisplayAndroid;
 
@@ -58,7 +60,8 @@
 /** The unit tests for AutofillProvider. */
 @RunWith(BaseRobolectricTestRunner.class)
 @Features.EnableFeatures({
-    AndroidAutofillFeatures.ANDROID_AUTOFILL_IMPROVED_VISIBILITY_DETECTION_NAME
+    AndroidAutofillFeatures.ANDROID_AUTOFILL_IMPROVED_VISIBILITY_DETECTION_NAME,
+    AndroidAutofillFeatures.ANDROID_AUTOFILL_SUPPRESS_OFFSCREEN_DATALIST_NAME,
 })
 public class AutofillProviderTest {
     private static final float EXPECTED_DIP_SCALE = 2;
@@ -87,6 +90,8 @@
     @Mock private AutofillProvider.Natives mNativeMock;
     @Mock private RenderCoordinatesImpl mRenderCoordinates;
     @Mock private AutofillManager mAutofillManager;
+    @Mock private ViewAndroidDelegate mViewAndroidDelegate;
+    @Mock private View mAnchorView;
 
     /** AutofillManagerWrapper which keeps track of the virtual id of the field with focus. */
     private class TestAutofillManagerWrapper extends AutofillManagerWrapper {
@@ -123,6 +128,37 @@
         }
     }
 
+    private void setContainerScreenGeometry(Rect visibleRect, int screenX, int screenY) {
+        doAnswer(
+                        invocation -> {
+                            Rect rect = invocation.getArgument(0);
+                            rect.set(visibleRect);
+                            return true;
+                        })
+                .when(mContainerView)
+                .getGlobalVisibleRect(any(Rect.class));
+
+        doAnswer(
+                        invocation -> {
+                            int[] location = invocation.getArgument(0);
+                            location[0] = screenX;
+                            location[1] = screenY;
+                            return null;
+                        })
+                .when(mContainerView)
+                .getLocationOnScreen(any());
+
+        doAnswer(
+                        invocation -> {
+                            int[] location = invocation.getArgument(0);
+                            location[0] = screenX;
+                            location[1] = screenY;
+                            return null;
+                        })
+                .when(mContainerView)
+                .getLocationInWindow(any());
+    }
+
     @Before
     public void setUp() {
         mContext = Mockito.mock(Context.class);
@@ -153,34 +189,10 @@
         when(mDisplayAndroid.getDipScale()).thenReturn(EXPECTED_DIP_SCALE);
         when(mContainerView.getScrollX()).thenReturn(SCROLL_X);
         when(mContainerView.getScrollY()).thenReturn(SCROLL_Y);
-        doAnswer(
-                        invocation -> {
-                            int[] location = invocation.getArgument(0);
-                            location[0] = LOCATION_X;
-                            location[1] = LOCATION_Y;
-                            return null;
-                        })
-                .when(mContainerView)
-                .getLocationOnScreen(any());
+        when(mWebContents.getViewAndroidDelegate()).thenReturn(mViewAndroidDelegate);
+        when(mViewAndroidDelegate.acquireView()).thenReturn(mAnchorView);
 
-        doAnswer(
-                        invocation -> {
-                            Rect rect = invocation.getArgument(0);
-                            rect.set(0, 0, 2000, 2000);
-                            return true;
-                        })
-                .when(mContainerView)
-                .getGlobalVisibleRect(any(Rect.class));
-
-        doAnswer(
-                        invocation -> {
-                            int[] location = invocation.getArgument(0);
-                            location[0] = LOCATION_X;
-                            location[1] = LOCATION_Y;
-                            return null;
-                        })
-                .when(mContainerView)
-                .getLocationInWindow(any());
+        setContainerScreenGeometry(new Rect(0, 0, 2000, 2000), LOCATION_X, LOCATION_Y);
 
         RenderCoordinatesImpl.setInstanceForTesting(mRenderCoordinates);
         when(mRenderCoordinates.getContentOffsetYPixInt()).thenReturn(0);
@@ -457,35 +469,7 @@
 
     @Test
     public void testSuppressNotificationOutsideBounds() {
-        // Mock container view to be at [0, 0, 100, 100] on screen
-        doAnswer(
-                        invocation -> {
-                            Rect rect = invocation.getArgument(0);
-                            rect.set(0, 0, 100, 100);
-                            return true;
-                        })
-                .when(mContainerView)
-                .getGlobalVisibleRect(any(Rect.class));
-
-        doAnswer(
-                        invocation -> {
-                            int[] location = invocation.getArgument(0);
-                            location[0] = 0;
-                            location[1] = 0;
-                            return null;
-                        })
-                .when(mContainerView)
-                .getLocationOnScreen(any());
-
-        doAnswer(
-                        invocation -> {
-                            int[] location = invocation.getArgument(0);
-                            location[0] = 0;
-                            location[1] = 0;
-                            return null;
-                        })
-                .when(mContainerView)
-                .getLocationInWindow(any());
+        setContainerScreenGeometry(new Rect(0, 0, 100, 100), 0, 0);
 
         // Reset mFocusVirtualId and mFocusBounds
         mFocusVirtualId = 0;
@@ -536,35 +520,7 @@
 
     @Test
     public void testSuppressNotificationOutsideBoundsWithOffsetY() {
-        // Mock container view to be at [0, 0, 100, 100] on screen.
-        doAnswer(
-                        invocation -> {
-                            Rect rect = invocation.getArgument(0);
-                            rect.set(0, 0, 100, 100);
-                            return true;
-                        })
-                .when(mContainerView)
-                .getGlobalVisibleRect(any(Rect.class));
-
-        doAnswer(
-                        invocation -> {
-                            int[] location = invocation.getArgument(0);
-                            location[0] = 0;
-                            location[1] = 0;
-                            return null;
-                        })
-                .when(mContainerView)
-                .getLocationOnScreen(any());
-
-        doAnswer(
-                        invocation -> {
-                            int[] location = invocation.getArgument(0);
-                            location[0] = 0;
-                            location[1] = 0;
-                            return null;
-                        })
-                .when(mContainerView)
-                .getLocationInWindow(any());
+        setContainerScreenGeometry(new Rect(0, 0, 100, 100), 0, 0);
 
         // Set offset Y to 40. Effective visible bounds top should be 40.
         when(mRenderCoordinates.getContentOffsetYPixInt()).thenReturn(40);
@@ -629,6 +585,215 @@
     }
 
     @Test
+    public void testShowDatalistPopup_suppressedWhenOutsideVisibleBounds() {
+        setContainerScreenGeometry(new Rect(0, 0, 100, 100), 0, 0);
+
+        Activity activity = Mockito.mock(Activity.class);
+        mAutofillProvider.switchToContext(new WeakReference<>(activity));
+
+        // Field is entirely outside the visible container bounds (e.g. top: -2000px).
+        // Datalist anchor uses field bounds: [0, -2000, 100, -1900] * dipScale 2 ->
+        // [0, -4000, 200, -3800], outside [0, 0, 100, 100].
+        RectF offscreenBounds = new RectF(0, -2000, 100, -1900);
+        FormFieldDataBuilder fieldBuilder = new FormFieldDataBuilder();
+        fieldBuilder.mBounds = offscreenBounds;
+
+        FormData form =
+                new FormData(
+                        /* sessionId= */ 123,
+                        /* name= */ null,
+                        /* host= */ null,
+                        Collections.singletonList(fieldBuilder.build()));
+
+        mAutofillProvider.startAutofillSession(
+                form,
+                /* focus= */ 0,
+                offscreenBounds.left,
+                offscreenBounds.top,
+                offscreenBounds.width(),
+                offscreenBounds.height(),
+                /* hasServerPrediction= */ false);
+
+        mAutofillProvider.showDatalistPopup(
+                new String[] {"Value 1"}, new String[] {"Label 1"}, /* isRtl= */ false);
+
+        // When bounds are completely outside visible bounds, popup anchor must not be set
+        // and datalist popup must not be created.
+        verify(mViewAndroidDelegate, never()).acquireView();
+        verify(mNativeMock, never())
+                .setAnchorViewRect(
+                        anyLong(), any(), anyFloat(), anyFloat(), anyFloat(), anyFloat());
+        assertNull(mAutofillProvider.getDatalistPopupForTesting());
+    }
+
+    @Test
+    @Features.DisableFeatures({
+        AndroidAutofillFeatures.ANDROID_AUTOFILL_SUPPRESS_OFFSCREEN_DATALIST_NAME
+    })
... (file diff truncated)
Loading diff…

Original Bug Report

reported by vm...@google.com

UI Spoofing via Unclamped Android Datalist AutofillPopup Overlaying Omnibox

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: On Android, the native dropdown shown for <datalist> fields (AutofillPopup) is anchored at raw renderer-supplied field bounds, and every clamp in the popup-positioning chain is relative to the application window — whose top band is the omnibox/toolbar — not to the WebContents content area. A page with an off-screen datalist input (position:fixed; top:-2000px) plus one ordinary tap gets a persistent, attacker-text-filled native dropdown pinned over Chrome’s omnibox (third-party-autofill configuration) or over a WebView embedder’s own top-of-window UI (default WebView configuration). The overlay can display a fake origin while the user is on an attacker origin, and taps aimed at the covered omnibox are consumed by the dropdown, so the tap-the-URL-bar verification gesture never reaches Chrome. Demonstrated end-to-end on a real Android arm64 device with unmodified builds of this tree (155.0.8055.0), in both configurations.

Affected files:

Estimated timestamp from git blame: 2020-06-15

Summary

AutofillProvider.showDatalistPopup() anchors the Android datalist dropdown at FormFieldData bounds computed by the renderer from page layout, with no intersection against the visible content area anywhere in the chain. The sibling paths that hand these same bounds to the platform AutofillManager were retrofitted with clampToVisibleBounds() — whose comment names the hazard verbatim: “In Chrome, the omnibox toolbar sits on top of mContainerView, obscuring the top content” (AutofillProvider.java:838-843) — but the datalist popup, the only consumer that draws Chromium’s own interactive UI, was left out. Desktop hides Autofill popups whose anchor leaves the content area (chrome/browser/ui/views/autofill/popup/popup_base_view.cc:556-584, SuggestionHidingReason::kElementOutsideOfContentArea); no equivalent exists on Android.

Preconditions:

The trigger is one ordinary tap anywhere on the page (transient activation) followed by JS focus() on the off-screen field. Platform-autofill configurations set FocusRequiresScroll(false) (components/autofill/content/renderer/autofill_agent.cc:314-323), and ShowSuggestions has no visibility or in-viewport check (autofill_agent.cc:1608-1705). The shown popup does not dismiss on outside taps (AutofillPopup.java:63 → setDismissOnTouchInteraction(false)); it is torn down only by blur/navigation/visibility events, which the page controls while the user stays on it.


Root Cause Analysis

  1. Renderer computes unclamped bounds from layout: components/autofill/content/renderer/form_autofill_util.cc:2009-2014 — field->set_bounds(...) from element.BoundsInWidget() with no viewport intersection, so position:fixed; top:-2000px yields a large negative y. Datalist strings are copied with only length/count caps, no content restrictions.
  2. Browser forwards without validation: components/autofill/content/browser/content_autofill_driver.cc:89-95,120 only re-origins the rect into root coordinates; android_autofill_provider.cc:214-261 then calls ShowDatalistPopup() whenever field.datalist_options() is non-empty — no bounds validation, no further gesture check.
  3. Popup anchored at the raw bounds, bypassing the existing clamp helper:
    // [`components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java:615-625`](https://source.chromium.org/chromium/chromium/src/+/main:components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java;l=615)
    @CalledByNative
    protected void showDatalistPopup(
            String[] datalistValues, String[] datalistLabels, boolean isRtl) {
        if (mRequest == null) return;
        FocusField focusField = mRequest.getFocusField();
        if (focusField != null) {
            showDatalistPopup(
                    datalistValues,
                    datalistLabels,
                    mRequest.getField(focusField.fieldIndex).getBounds(),  // <-- raw renderer bounds
                    isRtl);
    
    The private overload feeds them to setAnchorViewRect(bounds) (AutofillProvider.java:689); clampToVisibleBounds() (:860-870) is never called on this path, and datalist fields are excluded from the clamped AutofillManager notification paths, so no clamped path ever gates this popup.
  4. Negative margin applied verbatim to the anchor View: ui/android/view_android.cc:261 computes top_margin with no clamp; ViewAndroidDelegate.java:270 assigns mlp.topMargin = topMargin, with only a right-edge width clip (:263-265). The anchor View is laid out far above the container.
  5. Popup layer pins the anchor to the window origin — the toolbar’s corner:
    // [`ui/android/java/src/org/chromium/ui/widget/ViewRectUpdater.java:71-74`](https://source.chromium.org/chromium/chromium/src/+/main:ui/android/java/src/org/chromium/ui/widget/ViewRectUpdater.java;l=71)
    mView.getLocationInWindow(mCachedWindowCoordinates);
    mCachedWindowCoordinates[0] = Math.max(mCachedWindowCoordinates[0], 0);  // <-- window origin,
    mCachedWindowCoordinates[1] = Math.max(mCachedWindowCoordinates[1], 0);  //     not content area
    
  6. Dropdown y is never clamped and the popup is a window above the toolbar:
    // [`ui/android/java/src/org/chromium/ui/widget/AnchoredPopupWindowUtils.java:107-120`](https://source.chromium.org/chromium/chromium/src/+/main:ui/android/java/src/org/chromium/ui/widget/AnchoredPopupWindowUtils.java;l=107)
    if (positionBelow) {
        y = overlapAnchor ? anchorRect.top : anchorRect.bottom;
    } ...
    if (overlapAnchor) {
        return clamp(y, windowRect.top + marginPx, windowRect.bottom - popupHeight - marginPx);
    }
    return y;  // <-- dropdown (non-overlap) case: unclamped
    
    DropdownPopupWindowImpl uses the non-overlap branch, the positioning viewport is the whole app window, and AnchoredPopupWindow.java:1364-1386 shows the popup via showAtLocation() — a separate Android window above every view of the activity, so it renders on top of the omnibox/toolbar band. A TODO in PopupSpecCalculator.java:236-239 acknowledges the popup “can bleed onto the decorations”.
  7. Overlay persists and swallows verification taps: outside-tap dismissal is disabled (AutofillPopup.java:63 → DropdownPopupWindowImpl.java:204-206), and taps on the covered omnibox band land inside the popup’s list — accepting a row merely fills the hidden field (android_autofill_provider.cc:158-212 for the page-controlled teardown paths).

Reproduction

  1. Serve a page (secure context) containing a full-screen decoy, <input id=f list=d autocomplete=off inputmode=none style="position:fixed;top:-2000px;left:0;width:100vw;height:1px">, and a <datalist id=d> whose option labels/values render a fake origin line (e.g. “https://accounts.google.com” with padlock-like Unicode plus “Verified · Google LLC” filler rows).
  2. Open it (a) in any Android WebView app (default configuration), or (b) in Chrome for Android with a third-party autofill service selected.
  3. On the first tap anywhere, call f.focus() (with mousedown.preventDefault() so the tap does not re-focus the decoy and blur the field).
  4. Expected: no popup for an element outside the visible content area — matching the sibling clampToVisibleBounds() paths and desktop’s kElementOutsideOfContentArea suppression.

Observed (executed on a real Android arm64 device, unmodified builds, both configurations): AskForValuesToFill carries the unclamped bounds (0,-2000 411x1), and the native dropdown renders pinned to the top of the app window — fully covering Chrome’s omnibox (window-manager evidence: PopupWindow child of the Chrome activity with frame [0,42][720,332], exactly the toolbar band) and, in the WebView case, the shell’s title/URL bar. The overlay persists across further taps on the page, and a tap on the covered omnibox band is consumed by the popup’s ListView — a row is accepted into the hidden field and the real omnibox is never focused.

Severity

medium — Web content escapes the content area and places arbitrary attacker text over the browser’s primary security surface with one ordinary tap, matching the security FAQ’s first worked example of a reportable spoof (origin confusion, docs/security/faq.md:362-380), and it additionally intercepts the user’s tap-to-verify gesture. It stays below High’s “complete control over the apparent origin in the omnibox” (docs/security/severity-guidelines.md:108) because the overlay is dropdown-styled rather than altering the real omnibox contents or security state, and the Chrome target requires the shipped third-party-autofill setting (the always-on WebView target overlays embedder UI instead); per the S2 preamble those are mitigating factors on a higher-rated shape, not grounds for Low. Direct triage precedent on the same component/platform/effect is Medium: CVE-2023-1231 (“spoof the contents of the omnibox” via Autofill on Android, crbug.com/1274887), CVE-2023-3738, CVE-2024-11111.

Suggested Fix Direction

Clamp the datalist anchor bounds with the existing clampToVisibleBounds()/transformToWindowBounds() helpers and suppress (or dismiss) the popup when the field has no intersection with the visible content area, mirroring the sibling AutofillManager retrofit and desktop’s SuggestionHidingReason::kElementOutsideOfContentArea. This shape of fix was verified on-device to eliminate the overlay for the off-screen field while leaving visible datalist fields working normally.


POC: Android datalist AutofillPopup anchored at unclamped renderer bounds draws attacker-text native dropdown over the omnibox / WebView embedder UI

Strategy: web-content Bug Type: UI spoofing (CWE-451) — web content escapes the content area and overlays browser/embedder security UI

A web page containing a <datalist> input positioned at position:fixed; top:-2000px gets, after a single ordinary tap anywhere on the page, a native Android dropdown filled with attacker-controlled text pinned over the top band of the app window:

  • Chrome for Android (user has selected a third-party autofill service — shipped stable setting): the dropdown completely covers the omnibox. Where the user expects the URL bar they instead read 🔒 https://accounts.google.com — Verified · Google LLC · Secure connection while the page is localhost:8000 (any attacker origin). Tapping the fake “omnibox” to verify the URL is swallowed by the popup’s ListView (it merely accepts a suggestion into the hidden field) — the verification gesture never reaches Chrome’s toolbar.
  • Android WebView (default configuration, every WebView app): the dropdown covers the embedding app’s own top-of-window UI (title/URL bar of the WebView shell).

Root cause: AutofillProvider.showDatalistPopup() anchors the popup at the raw renderer-supplied FormFieldData bounds; every clamp in the popup chain is window-relative (ViewRectUpdater pins the anchor to the window origin, AnchoredPopupWindowUtils.getPopupY() returns unclamped y in the dropdown case), and the sibling clampToVisibleBounds() retrofit skipped this path.

Reproduced on

  • Real Android 17 arm64 device (Cuttlefish), unmodified Chromium builds from this tree (155.0.8055.0): ChromePublic.apk and SystemWebView64.apk + preinstalled org.chromium.webview_shell. Zero source modifications.

Files

File Purpose
poc.html The attack page (decoy + off-screen datalist input + fake-origin options)
server.py Serves the page; combine with adb reverse tcp:8000 tcp:8000
device-serve.sh Alternative delivery: canned on-device localhost:8000 responder (used in this lab because adb reverse terminates on a remote adb-server host; page bytes are identical, and http://localhost:8000 is a secure context in both flows)
screenshot-webview-1-before-tap.png WebView shell: URL bar visible, page loaded
screenshot-webview-2-overlay.png After one tap: dropdown covers the shell’s title + URL bar
screenshot-webview-3-persists-after-tap.png Second tap elsewhere: overlay persists (setDismissOnTouchInteraction(false))
screenshot-chrome-1-before-tap.png Chrome: omnibox shows localhost:8000/poc.html
screenshot-chrome-2-overlay.png After one tap: omnibox fully covered by fake accounts.google.com rows
screenshot-chrome-3-after-tapping-fake-omnibox.png Tap on the covered omnibox band: consumed by the popup (row accepted, popup closed, omnibox never focused)
window-manager-evidence.txt dumpsys window proof: PopupWindow child of the Chrome/WebView activity, Chrome frame [0,42][720,332] covering the toolbar band
bandaid-fix.patch Minimal fix, verified on device (see below)

Prerequisites

  • Chromium Android build: autoninja -C out/android-arm64 chrome_public_apk system_webview_64_apk
  • An Android device (tested: Android 17 arm64, userdebug).
  • For the Chrome variant: any third-party autofill service selected on the device. The in-tree example service works: autoninja -C out/android-arm64 inline_autofill_service_example_apk.

Reproduction

A. WebView variant (default config — no settings at all)

export ANDROID_SERIAL=<serial>
# Install this tree's WebView as provider (userdebug: remove preinstalled first)
adb root && adb remount   # reboot once if remount asks for it
adb shell rm -rf /product/app/webview && adb reboot && adb wait-for-device
out/android-arm64/bin/system_webview_64_apk install
out/android-arm64/bin/system_webview_64_apk set-webview-provider

# Serve the page (either flow makes http://localhost:8000 = secure context)
python3 server.py &  adb reverse tcp:8000 tcp:8000     # standard
#   -- or --
./device-serve.sh "$ANDROID_SERIAL" &                  # lab fallback

adb shell am start -n org.chromium.webview_shell/.WebViewBrowserActivity \
    -d "http://localhost:8000/poc.html"
sleep 5
adb shell input tap 360 800          # one ordinary tap anywhere on the page
sleep 2
adb exec-out screencap -p > overlay.png

B. Chrome variant (omnibox overlay)

adb install -r -t out/android-arm64/apks/ChromePublic.apk
adb install -r -t out/android-arm64/apks/ChromiumExampleAutofillService.apk
# Select the 3P service (equivalent of the user picking it in Android Settings)
adb shell settings put secure autofill_service \
  org.chromium.example.autofill_service/org.chromium.example.autofill_service.fill_service.InlineFillService

# Enable Chrome's shipped "Autofill using another service" setting
# (equivalent of the user toggling chrome://settings > Autofill services).
# With the device rooted, set the profile prefs while Chrome is stopped:
adb shell am start -n org.chromium.chrome/com.google.android.apps.chrome.Main -d about:blank
sleep 10; adb shell am force-stop org.chromium.chrome
adb root
adb pull /data/data/org.chromium.chrome/app_chrome/Default/Preferences /tmp/p.json
python3 - <<'EOF'
import json; p=json.load(open('/tmp/p.json')); a=p.setdefault('autofill',{})
a['using_virtual_view_structure']=True
a['third_party_package_used_for_platform_autofill']='org.chromium.example.autofill_service'
json.dump(p,open('/tmp/p.json','w'))
EOF
adb push /tmp/p.json /data/data/org.chromium.chrome/app_chrome/Default/Preferences

adb shell am start -n org.chromium.chrome/com.google.android.apps.chrome.Main \
    -d "http://localhost:8000/poc.html"
sleep 10                              # dismiss any first-run promo sheet if shown
adb shell input tap 360 800           # one ordinary tap anywhere on the page
sleep 2
adb exec-out screencap -p > overlay.png
adb shell dumpsys window windows | grep -A8 PopupWindow

Expected Result

After the single tap, a native dropdown whose rows read 🔒 https://accounts.google.com / Verified · Google LLC · Secure connection (etc.) is drawn pinned to the top of the app window:

  • Chrome: it fully covers the omnibox; dumpsys window shows a PopupWindow child of the Chrome activity with frame [0,42][720,332] (720x1348 display), i.e. exactly the toolbar band. Tapping that band interacts with the popup, not with Chrome.
  • WebView shell: it fully covers the shell’s title bar and URL EditText.
  • The overlay survives further taps on the page (outside-tap dismissal is disabled for this popup) and is torn down only by field blur/navigation-level events, which the page controls.

Output

From the Chrome run (window-manager-evidence.txt, captured while the overlay covered the omnibox; complete file in this directory):

Window #6 Window{97376a0 u0 PopupWindow:a5df16e}:
  mOwnerUid=10125 showForAllUsers=false package=org.chromium.chrome
  mAttrs={(0,11)(720x290) gr=TOP START CENTER ... ty=APPLICATION_PANEL fmt=TRANSLUCENT
    fl=NOT_FOCUSABLE SPLIT_TOUCH HARDWARE_ACCELERATED FLAG_LAYOUT_ATTACHED_IN_DECOR
  Requested w=720 h=290 mLayoutSeq=131
  mParentWindow=Window{e8956c8 u0 org.chromium.chrome/com.google.android.apps.chrome.Main}
  mBaseLayer=21000 mSubLayer=1
  Frames: parent=[0,0][720,1348] display=[0,42][720,1264] frame=[0,42][720,332]

The PopupWindow (sub-layer 1, above all views of the activity window) spans [0,42][720,332] — the omnibox strip occupies roughly y=95..160 in the “before” screenshot, so it is entirely beneath the attacker-controlled surface.

Screenshots: compare screenshot-chrome-1-before-tap.png (omnibox: localhost:8000/poc.html) with screenshot-chrome-2-overlay.png (same screen, one tap later: top band reads 🔒 https://accounts.google.com).

Notes on the trigger

  • One tap anywhere provides transient user activation; the page then calls f.focus(). With uses_platform_autofill=true the renderer config is FocusRequiresScroll(false), so the focus triggers AskForValuesToFill with the field’s unclamped bounds (0,-2000 411x1 observed), and the browser shows the datalist popup unconditionally.
  • mousedown.preventDefault() in the page keeps the tap from re-focusing the tapped element afterwards (that would send HidePopup); inputmode=none keeps the soft keyboard away. Both are ordinary, attacker-controlled page behavior.
  • http://localhost is used because the platform-autofill config sets SecureContextRequired(true); any HTTPS attacker origin is equivalent.

Bandaid fix (verified)

bandaid-fix.patch suppresses the datalist popup when the field’s bounds fall entirely outside the visible bounds of the container view, reusing the same clampToVisibleBounds()/transformToWindowBounds() helpers the platform-AutofillManager notification paths already use (and matching desktop’s SuggestionHidingReason::kElementOutsideOfContentArea behavior).

Verified on-device with the patched ChromePublic.apk:

  • attack page: autofill session still starts, but no popup window is created;
  • legitimate visible datalist field: popup shows normally, anchored at the field, inside the content area.

Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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.

View on issue tracker
Links in the report