CVE-2026-113170
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcomponents/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java |
modified |
Files Changed
components/android_autofill/browser/android_autofill_features.cccomponents/android_autofill/browser/java/src/org/chromium/components/autofill/AndroidAutofillFeatures.javaandroid_webview/java/src/org/chromium/android_webview/common/ProductionSupportedFlagList.javacomponents/android_autofill/browser/android_autofill_features.hcomponents/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.javacomponents/android_autofill/browser/junit/src/org/chromium/components/autofill/AutofillProviderTest.javacomponents/android_autofill/browser/junit/BUILD.gn
Patch
From 9b2d1724885538396738e52ca2e5c9eadd067c2d Mon Sep 17 00:00:00 2001 From: Friedrich Horschig <[email protected]> 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 <[email protected]> Reviewed-by: Timofey Chudakov <[email protected]> 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)
Original Bug Report
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:
components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java:615-627— datalist popup anchored at raw renderer bounds; theclampToVisibleBounds()retrofit (:275,:507,:860-870) skipped this pathcomponents/android_autofill/browser/android_autofill_provider.cc:214-261— shows the popup whenever datalist options are present; no bounds validationui/android/view_android.cc:248-266— anchortop_margincomputed with no clampui/android/java/src/org/chromium/ui/base/ViewAndroidDelegate.java:241-272— arbitrary negativetopMarginapplied verbatim; only the right edge is clippedui/android/java/src/org/chromium/ui/widget/ViewRectUpdater.java:71-74— anchor clamped to the window origin, not the content areaui/android/java/src/org/chromium/ui/widget/AnchoredPopupWindowUtils.java:100-121— dropdown-case y returned unclampedui/android/java/src/org/chromium/ui/widget/AnchoredPopupWindow.java:1364-1386— popup shown viaPopupWindow.showAtLocation(), a separate window layered above all activity views including the toolbar
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:
- Android WebView: none —
AwContentscreates anAutofillProviderfor every WebView (android_webview/java/src/org/chromium/android_webview/AwContents.java:1769-1777), and the datalist popup works with no autofill service selected. Web content overlays the embedding app’s own top-of-window UI. - Chrome on Android: the user has selected a third-party autofill service (“Autofill using another service” — a shipped, flag-free stable setting;
chrome/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/AutofillClientProviderUtils.java:84-127). The overlay then covers Chrome’s omnibox.
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
- Renderer computes unclamped bounds from layout:
components/autofill/content/renderer/form_autofill_util.cc:2009-2014—field->set_bounds(...)fromelement.BoundsInWidget()with no viewport intersection, soposition:fixed; top:-2000pxyields a large negative y. Datalist strings are copied with only length/count caps, no content restrictions. - Browser forwards without validation:
components/autofill/content/browser/content_autofill_driver.cc:89-95,120only re-origins the rect into root coordinates;android_autofill_provider.cc:214-261then callsShowDatalistPopup()wheneverfield.datalist_options()is non-empty — no bounds validation, no further gesture check. - Popup anchored at the raw bounds, bypassing the existing clamp helper:
The private overload feeds them to
// [`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);setAnchorViewRect(bounds)(AutofillProvider.java:689);clampToVisibleBounds()(:860-870) is never called on this path, and datalist fields are excluded from the clampedAutofillManagernotification paths, so no clamped path ever gates this popup. - Negative margin applied verbatim to the anchor View:
ui/android/view_android.cc:261computestop_marginwith no clamp;ViewAndroidDelegate.java:270assignsmlp.topMargin = topMargin, with only a right-edge width clip (:263-265). The anchor View is laid out far above the container. - 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 - 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: unclampedDropdownPopupWindowImpluses the non-overlap branch, the positioning viewport is the whole app window, andAnchoredPopupWindow.java:1364-1386shows the popup viashowAtLocation()— a separate Android window above every view of the activity, so it renders on top of the omnibox/toolbar band. A TODO inPopupSpecCalculator.java:236-239acknowledges the popup “can bleed onto the decorations”. - 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-212for the page-controlled teardown paths).
Reproduction
- 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). - Open it (a) in any Android WebView app (default configuration), or (b) in Chrome for Android with a third-party autofill service selected.
- On the first tap anywhere, call
f.focus()(withmousedown.preventDefault()so the tap does not re-focus the decoy and blur the field). - Expected: no popup for an element outside the visible content area — matching the sibling
clampToVisibleBounds()paths and desktop’skElementOutsideOfContentAreasuppression.
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 connectionwhile the page islocalhost: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.apkandSystemWebView64.apk+ preinstalledorg.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 windowshows aPopupWindowchild 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(). Withuses_platform_autofill=truethe renderer config isFocusRequiresScroll(false), so the focus triggersAskForValuesToFillwith the field’s unclamped bounds (0,-2000 411x1observed), 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 sendHidePopup);inputmode=nonekeeps the soft keyboard away. Both are ordinary, attacker-controlled page behavior.http://localhostis used because the platform-autofill config setsSecureContextRequired(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.
- http://localhost:8000
- http://localhost:8000/poc.html
- https://accounts.google.com
- https://crbug.com/1274887
- https://source.chromium.org/chromium/chromium/src/+/main:android_webview/java/src/org/chromium/android_webview/AwContents.java;l=1769
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/AutofillClientProviderUtils.java;l=84
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/autofill/popup/popup_base_view.cc;l=556
- https://source.chromium.org/chromium/chromium/src/+/main:components/android_autofill/browser/android_autofill_provider.cc;l=214
- https://source.chromium.org/chromium/chromium/src/+/main:components/android_autofill/browser/java/src/org/chromium/components/autofill/AutofillProvider.java;l=615
- https://source.chromium.org/chromium/chromium/src/+/main:components/autofill/content/browser/content_autofill_driver.cc;l=89
- https://source.chromium.org/chromium/chromium/src/+/main:components/autofill/content/renderer/autofill_agent.cc;l=314
- https://source.chromium.org/chromium/chromium/src/+/main:components/autofill/content/renderer/form_autofill_util.cc;l=2009
- https://source.chromium.org/chromium/chromium/src/+/main:ui/android/java/src/org/chromium/ui/base/ViewAndroidDelegate.java;l=241
- https://source.chromium.org/chromium/chromium/src/+/main:ui/android/java/src/org/chromium/ui/widget/AnchoredPopupWindow.java;l=1364
- https://source.chromium.org/chromium/chromium/src/+/main:ui/android/java/src/org/chromium/ui/widget/AnchoredPopupWindowUtils.java;l=100
- https://source.chromium.org/chromium/chromium/src/+/main:ui/android/java/src/org/chromium/ui/widget/AnchoredPopupWindowUtils.java;l=107
- https://source.chromium.org/chromium/chromium/src/+/main:ui/android/java/src/org/chromium/ui/widget/ViewRectUpdater.java;l=71
- https://source.chromium.org/chromium/chromium/src/+/main:ui/android/view_android.cc;l=248
- https://source.chromium.org/chromium/chromium/src/+/main:ui/android/view_android.cc;l=261