CVE-2026-113055
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.java |
modified | |
forcontent/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.java |
modified | |
testRestrictInteractionsInSwipeRegion_stickyImmersiveWithoutFullscreenTab_deferredcontent/public/android/junit/src/org/chromium/content/browser/input/ImeAdapterImplTest.java |
modified |
Files Changed
ui/android/javatests/src/org/chromium/ui/base/EventForwarderDeviceTest.javaui/android/java/src/org/chromium/ui/base/EventForwarder.javaui/android/junit/src/org/chromium/ui/base/EventForwarderTest.javacontent/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.javaui/android/java/src/org/chromium/ui/base/ViewUtils.javacontent/public/android/junit/src/org/chromium/content/browser/input/ImeAdapterImplTest.java
Patch
From 1f0419c1e4ec5069b0e02647fa072c098df794b8 Mon Sep 17 00:00:00 2001 From: Charles Hager <[email protected]> Date: Fri, 18 Sep 2026 15:58:09 -0700 Subject: [PATCH] Restrict IME in fullscreen when initiated in system gesture region This change adds new logic to EventForwarder tracking touch sequences, specifically when a touch down occurs, when a touch move exceeds the "slop" threshold to be considered a "swipe, and when a touch up ends a touch sequence. The term "slop" is used here to match the Android API - ViewConfiguration#getScaledTouchSlop(). This logic can be used by observers to track a touch sequence so that they can react to taps occurring in sensitive screen regions, like the system gesture insets, and potentially delay reactions to taps until it is confirmed that a tap is not a swipe. This CL also includes a change to ImeAdapterImpl using this new logic to delay showing the keyboard for "touchstart" events when in fullscreen. A webpage could exploit showing the keyboard for "touchstart" to intercept and cancel gesture swipes like the back gesture, or the gesture to exit an app and move it to Recents, which is adversarial to the user. With this change, the keyboard can no longer be exploited this way as it will not be shown until a tap has been confirmed not to be a swipe. Before: https://screencast.googleplex.com/cast/NTQxODI0OTk3Mjg3NTI2NHwzMWUzYTRiZS0wZQ Fix: https://screencast.googleplex.com/cast/NjUwMTc1MjQ0NTU5OTc0NHwxYjFmNmNkMS05NA Bug: 340578708 Change-Id: I7d69a312066a28462a08cb8057bfdaf6c5e41cea Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8414393 Reviewed-by: Jinsuk Kim <[email protected]> Commit-Queue: Charles Hager <[email protected]> Cr-Commit-Position: refs/heads/main@{#1701177} --- diff --git a/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.java b/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.java index d726091..eacbb82 100644 --- a/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.java +++ b/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapterImpl.java @@ -89,6 +89,8 @@ import org.chromium.mojo.system.MessagePipeHandle; import org.chromium.mojo.system.MojoException; import org.chromium.mojo.system.impl.CoreImpl; +import org.chromium.ui.base.EventForwarder; +import org.chromium.ui.base.EventForwarder.TouchSequenceObserver; import org.chromium.ui.base.ViewUtils; import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.base.ime.TextInputAction; @@ -131,7 +133,8 @@ WindowEventObserver, UserData, InputMethodManagerWrapper.Delegate, - AutocorrectManager.Delegate { + AutocorrectManager.Delegate, + TouchSequenceObserver { private static final String TAG = "Ime"; private static final boolean DEBUG_LOGS = false; @@ -206,6 +209,8 @@ // keyboard from Direct writing toolbar. private boolean mForceShowKeyboardDuringStylusWriting; + private boolean mPendingShowKeyboardOnTouchUp; + private final ImeKeyEventReplayer mImeKeyEventReplayer = new ImeKeyEventReplayer(this::sendReplayedKeyEvent); @@ -905,9 +910,10 @@ } if (DEBUG_LOGS) Log.i(TAG, "showSoftKeyboard"); View containerView = getContainerView(); + EventForwarder forwarder = mWebContents.getEventForwarder(); // Block showing soft keyboard during stylus handwriting. - int lastToolType = mWebContents.getEventForwarder().getLastToolType(); + int lastToolType = forwarder.getLastToolType(); if (mWebContents.getStylusWritingHandler() != null && !mWebContents.getStylusWritingHandler().canShowSoftKeyboard() && (lastToolType == MotionEvent.TOOL_TYPE_STYLUS @@ -918,6 +924,20 @@ return; } + if (isRestrictInteractionsInSwipeRegionEnabled() + && isFullscreenOrImmersive(mWebContents, containerView) + && forwarder.hasTouchOriginatingInGestureInsets(containerView)) { + if (DEBUG_LOGS) { + Log.i( + TAG, + "showSoftKeyboard: deferred or suppressed due to touch in gesture insets"); + } + if (!forwarder.hasCurrentTouchExceededTouchSlop()) { + setPendingShowKeyboardOnTouchUp(true); + } + return; + } + mInputMethodManagerWrapper.showSoftInput(containerView, 0, getNewShowKeyboardReceiver()); if (containerView.getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS) { @@ -925,6 +945,39 @@ } } + @Override + public void onTouchSequenceEnded(boolean hasExceededTouchSlop) { + if (!mPendingShowKeyboardOnTouchUp) return; + + setPendingShowKeyboardOnTouchUp(false); + if (!hasExceededTouchSlop && focusedNodeAllowsSoftKeyboard()) { + showSoftKeyboard(); + } + } + + /** + * Updates whether a soft keyboard show request is pending touch release, and manages {@link + * TouchSequenceObserver} registration accordingly. + * + * <p>The observer is registered lazily only while a deferred request is pending rather than in + * the constructor. {@link EventForwarder} instances are retained in the static {@code + * EventForwarder.sEventForwarders} map until native teardown, whereas {@link ImeAdapterImpl} is + * owned by {@code UserDataHost} (referenced weakly from {@code sNativeHelperMap}) and holds + * strong references back to {@code AwContents} via {@link ImeEventObserver}. Holding a + * permanent strong reference from {@link EventForwarder} to {@link ImeAdapterImpl} would bypass + * {@code AwContents}'s weak internals holder and prevent {@code AwContents} from being + * garbage-collected before native cleanup. + */ + private void setPendingShowKeyboardOnTouchUp(boolean pending) { + if (mPendingShowKeyboardOnTouchUp == pending) return; + mPendingShowKeyboardOnTouchUp = pending; + if (pending) { + mWebContents.getEventForwarder().addTouchSequenceObserver(this); + } else { + mWebContents.getEventForwarder().removeTouchSequenceObserver(this); + } + } + private void updateNodeAttributes(boolean isEditable, boolean isPassword) { if (mNodeEditable != isEditable || mNodePassword != isPassword) { for (ImeEventObserver observer : mEventObservers) { @@ -975,6 +1028,7 @@ /** Hide soft keyboard. */ private void hideKeyboard() { + setPendingShowKeyboardOnTouchUp(false); if (!isValid()) return; if (DEBUG_LOGS) Log.i(TAG, "hideKeyboard"); View view = getContainerView(); @@ -1101,6 +1155,16 @@ return type != TextInputType.NONE && !InputDialogContainer.isDialogInputType(type); } + private static boolean isFullscreenOrImmersive(WebContents webContents, View containerView) { + return webContents.isFullscreenForCurrentTab() + || ViewUtils.isStickyImmersiveMode(containerView); + } + + private static boolean isRestrictInteractionsInSwipeRegionEnabled() { + return ContentFeatureMap.isEnabled( + ContentFeatures.RESTRICT_INTERACTIONS_IN_SWIPE_REGION_ON_FULLSCREEN); + } + /** See {@link View#dispatchKeyEvent(KeyEvent)} */ public boolean dispatchKeyEvent(KeyEvent event) { if (DEBUG_LOGS) { @@ -1115,9 +1179,11 @@ } @CalledByNative - private void destroyFromNative() { + @VisibleForTesting + void destroyFromNative() { if (mNativeImeAdapterAndroid == 0) return; + setPendingShowKeyboardOnTouchUp(false); resetAndHideKeyboard(); mIsConnected = false; if (mCursorAnchorInfoController != null) { diff --git a/content/public/android/junit/src/org/chromium/content/browser/input/ImeAdapterImplTest.java b/content/public/android/junit/src/org/chromium/content/browser/input/ImeAdapterImplTest.java index a448bce..ea21978 100644 --- a/content/public/android/junit/src/org/chromium/content/browser/input/ImeAdapterImplTest.java +++ b/content/public/android/junit/src/org/chromium/content/browser/input/ImeAdapterImplTest.java @@ -4,6 +4,8 @@ package org.chromium.content.browser.input; +import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; + import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; @@ -21,12 +23,14 @@ import static org.mockito.Mockito.when; import android.graphics.Rect; +import android.os.Build; import android.os.SystemClock; import android.text.SpannableString; import android.text.Spanned; import android.text.style.SuggestionSpan; import android.view.KeyEvent; import android.view.ViewGroup; +import android.view.WindowInsetsController; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; @@ -42,6 +46,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; +import org.robolectric.annotation.Config; import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.base.test.util.Features.DisableFeatures; @@ -60,6 +65,7 @@ import org.chromium.content_public.common.ContentFeatures; import org.chromium.ui.accessibility.AccessibilityFeatures; import org.chromium.ui.base.DeviceInput; +import org.chromium.ui.base.EventForwarder; import org.chromium.ui.base.ime.TextInputType; import org.chromium.ui.mojom.ImeTextSpanType; import org.chromium.ui.test.util.TestViewAndroidDelegate; @@ -84,6 +90,7 @@ @Mock private CorrectionInfo mCorrectionInfo; @Mock private AutocorrectManager mAutocorrectManager; @Mock private InputMethodManagerWrapper mInputMethodManagerWrapper; + @Mock private EventForwarder mEventForwarder; @Before public void setUp() { @@ -103,6 +110,7 @@ .thenReturn(ApplicationProvider.getApplicationContext().getResources()); when(mWebContentsImpl.getViewAndroidDelegate()) .thenReturn(new TestViewAndroidDelegate(mContainerView)); + when(mWebContentsImpl.getEventForwarder()).thenReturn(mEventForwarder); } @Test @@ -152,6 +160,26 @@ /* imeTextSpans= */ null); } + private void updateTextInputTypeWithShow( + ImeAdapterImpl adapter, @TextInputType int textInputType) { + adapter.updateState( + /* textInputType= */ textInputType, + /* textInputFlags= */ 0, + /* textInputMode= */ 0, + /* textInputAction= */ 0, + /* showIfNeeded= */ true, + /* alwaysHide= */ false, + /* text= */ "", + /* selectionStart= */ 0, + /* selectionEnd= */ 0, + /* compositionStart= */ 0, + /* compositionEnd= */ 0, + /* replyToRequest= */ false, + /* lastVkVisibilityRequest= */ 0, + /* vkPolicy= */ 0, + /* imeTextSpans= */ null); + } + @Test @EnableFeatures(ContentFeatureList.ANDROID_FORCE_TEXT_INPUT_STATE_UPDATE_UPON_FOCUS) public void testOnViewFocusChangedGainedCallsRequestTextInputStateUpdate() { @@ -915,4 +943,162 @@ DeviceInput.setSupportsKeyboardForTesting(false); doUpdateCursorAnchorInfoTest(/* shouldExpectRequestRectangleOnScreen= */ false); } + + @Test + @EnableFeatures(ContentFeatures.RESTRICT_INTERACTIONS_IN_SWIPE_REGION_ON_FULLSCREEN) + public void testRestrictInteractionsInSwipeRegion_fullscreen_touchInInsets_deferredUntilTap() { + ImeAdapterImpl adapter = new ImeAdapterImpl(mWebContentsImpl); + adapter.setInputMethodManagerWrapper(mInputMethodManagerWrapper); + adapter.onConnectedToRenderProcess(); + + when(mWebContentsImpl.isFullscreenForCurrentTab()).thenReturn(true); + + // Simulate touch down inside gesture insets (rawX = 20 < 50) + when(mEventForwarder.hasTouchOriginatingInGestureInsets(mContainerView)).thenReturn(true); + + updateTextInputTypeWithShow(adapter, TextInputType.TEXT); + + // Verify IME show is suppressed while touch is down in gesture insets and observer added + verify(mEventForwarder).addTouchSequenceObserver(adapter); + verify(mInputMethodManagerWrapper, never()).showSoftInput(any(), anyInt(), any()); + + // Simulate touch release as a tap (not exceeding touch slop) + when(mEventForwarder.hasTouchOriginatingInGestureInsets(mContainerView)).thenReturn(false); + adapter.onTouchSequenceEnded(/* hasExceededTouchSlop= */ false); + + // Verify deferred IME show executes upon tap release and observer is removed + verify(mEventForwarder).removeTouchSequenceObserver(adapter); + verify(mInputMethodManagerWrapper, times(1)).showSoftInput(any(), anyInt(), any()); + } + + @Test + @EnableFeatures(ContentFeatures.RESTRICT_INTERACTIONS_IN_SWIPE_REGION_ON_FULLSCREEN) + public void testRestrictInteractionsInSwipeRegion_fullscreen_touchInInsets_swipeDiscarded() { + ImeAdapterImpl adapter = new ImeAdapterImpl(mWebContentsImpl); + adapter.setInputMethodManagerWrapper(mInputMethodManagerWrapper); + adapter.onConnectedToRenderProcess(); + + when(mWebContentsImpl.isFullscreenForCurrentTab()).thenReturn(true); + + // Simulate touch down inside gesture insets + when(mEventForwarder.hasTouchOriginatingInGestureInsets(mContainerView)).thenReturn(true); + + updateTextInputTypeWithShow(adapter, TextInputType.TEXT); + + // Verify IME show suppressed + verify(mInputMethodManagerWrapper, never()).showSoftInput(any(), anyInt(), any()); + + // Simulate swipe exceeding touch slop + adapter.onTouchSequenceEnded(/* hasExceededTouchSlop= */ true); + + // Verify discarded (never shown) + verify(mInputMethodManagerWrapper, never()).showSoftInput(any(), anyInt(), any()); + } + + @Test + @EnableFeatures(ContentFeatures.RESTRICT_INTERACTIONS_IN_SWIPE_REGION_ON_FULLSCREEN) + public void testRestrictInteractionsInSwipeRegion_fullscreen_touchCenter_immediate() { + ImeAdapterImpl adapter = new ImeAdapterImpl(mWebContentsImpl); + adapter.setInputMethodManagerWrapper(mInputMethodManagerWrapper); + adapter.onConnectedToRenderProcess(); + + when(mWebContentsImpl.isFullscreenForCurrentTab()).thenReturn(true); + + // Simulate touch down outside gesture insets + when(mEventForwarder.hasTouchOriginatingInGestureInsets(mContainerView)).thenReturn(false); + + updateTextInputTypeWithShow(adapter, TextInputType.TEXT); + + // Verify IME shows immediately + verify(mInputMethodManagerWrapper, times(1)).showSoftInput(any(), anyInt(), any()); + + // Touch release does not trigger another show + adapter.onTouchSequenceEnded(/* hasExceededTouchSlop= */ false); + verify(mInputMethodManagerWrapper, times(1)).showSoftInput(any(), anyInt(), any()); + } + + @Test + @EnableFeatures(ContentFeatures.RESTRICT_INTERACTIONS_IN_SWIPE_REGION_ON_FULLSCREEN) + public void testRestrictInteractionsInSwipeRegion_notFullscreen_immediate() { + ImeAdapterImpl adapter = new ImeAdapterImpl(mWebContentsImpl); + adapter.setInputMethodManagerWrapper(mInputMethodManagerWrapper); + adapter.onConnectedToRenderProcess(); + + when(mWebContentsImpl.isFullscreenForCurrentTab()).thenReturn(false); + + // Simulate touch down in gesture insets (rawX = 20) + when(mEventForwarder.hasTouchOriginatingInGestureInsets(mContainerView)).thenReturn(true); + + updateTextInputTypeWithShow(adapter, TextInputType.TEXT); + + // Not in fullscreen -> immediately shown + verify(mInputMethodManagerWrapper, times(1)).showSoftInput(any(), anyInt(), any()); + } + + @Test + @Config(sdk = Build.VERSION_CODES.R) + @EnableFeatures(ContentFeatures.RESTRICT_INTERACTIONS_IN_SWIPE_REGION_ON_FULLSCREEN) + public void + testRestrictInteractionsInSwipeRegion_stickyImmersiveWithoutFullscreenTab_deferred() { + ImeAdapterImpl adapter = new ImeAdapterImpl(mWebContentsImpl); + adapter.setInputMethodManagerWrapper(mInputMethodManagerWrapper); + adapter.onConnectedToRenderProcess(); + + when(mWebContentsImpl.isFullscreenForCurrentTab()).thenReturn(false); + + WindowInsetsController insetsController = mock(WindowInsetsController.class); + when(insetsController.getSystemBarsBehavior()) + .thenReturn(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + when(mContainerView.getWindowInsetsController()).thenReturn(insetsController); + + // Simulate touch down inside gesture insets + when(mEventForwarder.hasTouchOriginatingInGestureInsets(mContainerView)).thenReturn(true); + + updateTextInputTypeWithShow(adapter, TextInputType.TEXT); + + // Verify IME show is suppressed because sticky immersive mode is active on container view + verify(mInputMethodManagerWrapper, never()).showSoftInput(any(), anyInt(), any()); + + // Simulate touch release as tap + when(mEventForwarder.hasTouchOriginatingInGestureInsets(mContainerView)).thenReturn(false); + adapter.onTouchSequenceEnded(/* hasExceededTouchSlop= */ false); + verify(mInputMethodManagerWrapper, times(1)).showSoftInput(any(), anyInt(), any()); + } + + @Test + @EnableFeatures(ContentFeatures.RESTRICT_INTERACTIONS_IN_SWIPE_REGION_ON_FULLSCREEN) + public void testRestrictInteractionsInSwipeRegion_hideKeyboardCancelsPendingShow() { + ImeAdapterImpl adapter = new ImeAdapterImpl(mWebContentsImpl); + adapter.setInputMethodManagerWrapper(mInputMethodManagerWrapper); + adapter.onConnectedToRenderProcess(); + ... (file diff truncated)
Original Bug Report
Fullscreen navigation gestures can be intercepted by the keyboard activated via the 'touchstart' event in Chrome for Android.
VULNERABILITY DETAILS
In Chrome, after version 59.0.3054.0 on Android versions 14 and lower (except Android 9,10,11), gestures to exit fullscreen mode might be intercepted by the keyboard.
The poc.html file contains a script that focuses the input element on the “touchstart” event and removes focus from the input element on the “touchend” event to hide the keyboard.
This allows the page to absorb the user’s gestures by bringing up the on-screen keyboard, preventing the user from exiting fullscreen mode.
At the same time, the interactivity of the page is preserved, for example, the script does not interfere with the functionality of buttons and input fields.
Also, when switching to full-screen mode, the script sets the background to a screenshot of the google.com web page with omnibox and android navigation panel, demonstrating the ability to spoof the security UI, potentially tricking the user into mistaking the page’s rendered interface for a non-fullscreen browser interface with android navigation bar.
VERSION
The bug was successfully reproduced on the following android/chrome versions:
- Android 14 Samsung Galaxy S23 Chrome 124.0.6367.114
- Android 13 Samsung Galaxy S20 Chrome 124.0.6367.159, Beta 125.0.6422.34, Dev 126.0.6465.0, Canary 126.0.6468.0 Attached POC Videos: Screen_Recording_Gestures.mp4, Screen_Recording_NavigationBar.mp4
- Android 12 x86_64 Emulator Chrome 91.0.4472.114, Chrome 124.0.6367.171
The bug could NOT be reproduced on the following android/chrome versions:
- Android 11 Samsung Galaxy A50 Chrome 124.0.6367.159
- Android 10 x86 Emulator Chrome 74.0.3729.185, Chrome 124.0.6367.171
- Android 9.0 x86 Emulator Chrome 69.0.3497.100, Chrome 124.0.6367.179
Interestingly, the bug can be successfully reproduced on older versions of Android, such as:
- Android 8.1 x86 Emulator Chrome 61.0.3163.98, Chrome 124.0.6367.179
- Android 8.0 x86 Emulator Chrome 124.0.6367.171
- Android 7.1.1 x86 Emulator Chrome 119.0.6045.193
REPRODUCTION CASE
- Ensure you have both the “poc.html” file and “poc_background.jpg” image accessible in the same directory
- Open the “poc.html” file.
- Touch the screen to enter fullscreen mode.
- Try to exit fullscreen mode following the instructions provided by the application: “Drag from top and touch the back button to exit fullscreen”
Expected Result: When swiping from the top, a navbar or gesture hint appears and the user can exit full-screen mode by clicking on the “Back” button or by swiping from the side of the screen (depending on preferred navigation type).
Actual Result: When swiping from any side of the screen, the keyboard that appears intercepts the user’s gesture
BISECT
I identified the commit that is most likely responsible for this behavior:
https://chromium.googlesource.com/chromium/src/+/af0bd0b92ac7f779d51ae482d5bfc23e7ee6c5b5
chrome/android: Remove EdgeSwipeEventHandler. Cr-Commit-Position: refs/heads/master@{#459942}
Closest snapshot after this commit: 459946
Closest snapshot before this commit: 459931
Chromium version: 59.0.3054.0
As you can see in the attached videos Screen_Recording_459931.mp4, Screen_Recording_459946.mp4 the bug is not present in snapshot 459931 but it is present in snapshot 459946.
Restoring or recreating the removed functionality will likely resolve the vulnerability.
CREDIT INFORMATION
Reporter credit: Kirill Baev