Chrome · WebAppInstalls › Android
CVE-2026-113169
Memory Corruption in WebAppInstalls (Android)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.javachrome/android/junit/BUILD.gnchrome/android/junit/src/org/chromium/chrome/browser/webapps/WebappFilterTouchUnitTest.javachrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java
Patch
From d5ca2e16e08aca9487cf5a077f87c29b2a73b06d Mon Sep 17 00:00:00 2001 From: Glenn Hartmann <[email protected]> Date: Tue, 22 Sep 2026 06:51:14 -0700 Subject: [PATCH] [Android] Hoist anti-tapjacking touch guard to BaseCustomTabActivity CustomTabActivity discards touch events that arrive while the activity is not RESUMED on Android T-, guarding against taps routed through an overlay activity owned by another app (crbug.com/40063907). The sibling activities in the same BaseCustomTabActivity family that host installed PWAs/WebAPKs - WebappActivity and SameTaskWebApkActivity - overrode no input-dispatch method, and neither an ancestor nor a view-layer filter compensated, so they accepted those events. These windows render without a URL bar and typically host a long-lived logged-in session. Move the guard from CustomTabActivity up into BaseCustomTabActivity so the whole family is covered uniformly: CustomTabActivity, TranslucentCustomTabActivity, WebappActivity, SameTaskWebApkActivity and DevToolsActivity. CustomTabActivity retains only the CCT-specific, flag-gated enter-animation block and delegates to super. The order of the two checks is unchanged, so CCT behavior is unaffected. On T+ the platform's ActivityRecordInputSink already drops these events, which is why the guard stays gated on SDK_INT < TIRAMISU. The enter-animation block is intentionally left CCT-only. SameTaskWebApkActivity is launched with FLAG_ACTIVITY_NO_ANIMATION, so an unconditional gate risks never unblocking, and a no-animation fallback would disable it precisely where it would be needed. The remaining gap - SameTaskWebApkActivity being translucent on SDK < S and accepting touches from its first frame while RESUMED - needs a different signal and is not addressed here. TAG=agy CONV=75134412-8ef9-4231-a294-f035431fec9b Bug: 562955271 Test: run_chrome_junit_tests --gtest_filter=*WebappFilterTouchUnitTest*:*CustomTabsFilterTouchUnitTest* Change-Id: Idec17a44937afdb772952a2bfcbaaa636a6a6964 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8427844 Reviewed-by: Peter Conn <[email protected]> Commit-Queue: Glenn Hartmann <[email protected]> Cr-Commit-Position: refs/heads/main@{#1702568} --- diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java index 2c2acee..256b749b 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java @@ -19,6 +19,7 @@ import android.text.format.DateUtils; import android.util.Pair; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; @@ -30,9 +31,12 @@ import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.TrustedWebUtils; +import org.chromium.base.ActivityState; +import org.chromium.base.ApplicationStatus; import org.chromium.base.DeviceInfo; import org.chromium.base.IntentUtils; import org.chromium.base.ResettersForTesting; @@ -152,6 +156,16 @@ * CustomTabActivity}. */ public abstract class BaseCustomTabActivity extends ChromeActivity { + /** + * Prevents Tapjacking on T-. See crbug.com/40063907. + * + * <p>On Android T+ the platform's own {@code ActivityRecordInputSink} drops touches routed + * through an overlay owned by another app, so the activity-level guard is only needed below + * that. + */ + private static final boolean sPreventTouches = + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU; + private static Integer sOverrideCoreCountForTesting; private final CipherFactory mCipherFactory = new CipherFactory(); @@ -1326,6 +1340,29 @@ mBaseCustomTabRootUiCoordinator.onDeferredStartup(); } + /** + * Discards touch events that arrive while this activity is not RESUMED, i.e. events which may + * be trickling down from an overlay activity above. See crbug.com/40063907. + * + * <p>This lives on {@link BaseCustomTabActivity} rather than on an individual subclass so that + * every activity in this family hosting web content is covered - notably the PWA/WebAPK windows + * ({@code WebappActivity}, {@code SameTaskWebApkActivity}), which render without a URL bar and + * typically host a logged-in session. + */ + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (sPreventTouches && shouldPreventTouch()) { + return true; + } + return super.dispatchTouchEvent(ev); + } + + @VisibleForTesting + public boolean shouldPreventTouch() { + if (ApplicationStatus.getStateForActivity(this) == ActivityState.RESUMED) return false; + return true; + } + @Override public boolean dispatchKeyEvent(KeyEvent event) { Boolean result = diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java index 8d98298..4e77327 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java @@ -4,7 +4,6 @@ package org.chromium.chrome.browser.customtabs; -import static androidx.annotation.VisibleForTesting.PRIVATE; import static androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK; import static androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT; @@ -28,11 +27,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.browser.customtabs.CustomTabsIntent; -import org.chromium.base.ActivityState; -import org.chromium.base.ApplicationStatus; import org.chromium.base.IntentUtils; import org.chromium.base.Log; import org.chromium.base.ResettersForTesting; @@ -91,10 +87,6 @@ private final CustomTabsConnection mConnection = CustomTabsConnection.getInstance(); private int mNumOmniboxNavigationEventsPerSession; - /** Prevents Tapjacking on T-. See crbug.com/40063907 */ - private static final boolean sPreventTouches = - Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU; - private CustomTabsOpenTimeRecorder mOpenTimeRecorder; private static final boolean sBlockTouchesDuringEnterAnimation = @@ -510,10 +502,8 @@ if (sBlockTouchesDuringEnterAnimation && !mIsEnterAnimationCompleted) { return true; } - if (sPreventTouches && shouldPreventTouch()) { - // Discard the events which may be trickling down from an overlay activity above. - return true; - } + // The overlay/tapjacking guard lives in BaseCustomTabActivity so the whole family + // (including PWA/WebAPK windows) shares it. See crbug.com/40063907. return super.dispatchTouchEvent(ev); } @@ -531,12 +521,6 @@ super.finish(); } - @VisibleForTesting(otherwise = PRIVATE) - boolean shouldPreventTouch() { - if (ApplicationStatus.getStateForActivity(this) == ActivityState.RESUMED) return false; - return true; - } - /** * Show the web page with CustomTabActivity, without any navigation control. Used in showing the * terms of services page or help pages for Chrome. diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebappFilterTouchUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebappFilterTouchUnitTest.java new file mode 100644 index 0000000..53557e60 --- /dev/null +++ b/chrome/android/junit/src/org/chromium/chrome/browser/webapps/WebappFilterTouchUnitTest.java @@ -0,0 +1,98 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chrome.browser.webapps; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.Build; +import android.view.MotionEvent; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import org.chromium.base.ActivityState; +import org.chromium.base.ApplicationStatus; +import org.chromium.base.test.BaseRobolectricTestRunner; +import org.chromium.chrome.browser.customtabs.BaseCustomTabActivity; + +/** + * Tests that the PWA/WebAPK windows filter touch events coming from an overlay activity, the same + * way {@link org.chromium.chrome.browser.customtabs.CustomTabActivity} does. + * + * <p>These activities host installed web apps with no URL bar, so an overlay-routed tap would + * otherwise reach an authenticated origin. See crbug.com/40063907 for the guard's origin. + */ +@RunWith(BaseRobolectricTestRunner.class) +public class WebappFilterTouchUnitTest { + private static MotionEvent createTouchDown() { + return MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 0, + MotionEvent.ACTION_DOWN, + /* x= */ 0f, + /* y= */ 0f, + /* metaState= */ 0); + } + + private static void setUpTracking(BaseCustomTabActivity activity) { + // ApplicationStatus only starts tracking an activity on the CREATED transition. + ApplicationStatus.onStateChangeForTesting(activity, ActivityState.CREATED); + } + + private static void assertFiltersTouchesWhileNotResumed(BaseCustomTabActivity activity) { + setUpTracking(activity); + + ApplicationStatus.onStateChangeForTesting(activity, ActivityState.RESUMED); + assertFalse("Events should be accepted.", activity.shouldPreventTouch()); + ApplicationStatus.onStateChangeForTesting(activity, ActivityState.PAUSED); + assertTrue("Events should be discarded.", activity.shouldPreventTouch()); + + ApplicationStatus.onStateChangeForTesting(activity, ActivityState.DESTROYED); + } + + /** + * Asserts that a touch arriving while the activity is PAUSED is consumed rather than forwarded + * into the window, i.e. that {@code dispatchTouchEvent} is actually wired up to the guard. + */ + private static void assertDiscardsTouchWhilePaused(BaseCustomTabActivity activity) { + setUpTracking(activity); + ApplicationStatus.onStateChangeForTesting(activity, ActivityState.PAUSED); + + MotionEvent event = createTouchDown(); + try { + assertTrue( + "Touch delivered while PAUSED should be discarded.", + activity.dispatchTouchEvent(event)); + } finally { + event.recycle(); + } + + ApplicationStatus.onStateChangeForTesting(activity, ActivityState.DESTROYED); + } + + @Test + public void testWebappActivityShouldPreventTouch() { + assertFiltersTouchesWhileNotResumed(new WebappActivity()); + } + + @Test + public void testSameTaskWebApkActivityShouldPreventTouch() { + assertFiltersTouchesWhileNotResumed(new SameTaskWebApkActivity()); + } + + @Test + @Config(sdk = Build.VERSION_CODES.S_V2) + public void testWebappActivityDiscardsTouchWhilePaused_preT() { + assertDiscardsTouchWhilePaused(new WebappActivity()); + } + + @Test + @Config(sdk = Build.VERSION_CODES.S_V2) + public void testSameTaskWebApkActivityDiscardsTouchWhilePaused_preT() { + assertDiscardsTouchWhilePaused(new SameTaskWebApkActivity()); + } +} diff --git a/chrome/android/junit/BUILD.gn b/chrome/android/junit/BUILD.gn index ad9edbcb..6c85f34e 100644 --- a/chrome/android/junit/BUILD.gn +++ b/chrome/android/junit/BUILD.gn @@ -1601,6 +1601,7 @@ "src/org/chromium/chrome/browser/webapps/WebappAuthenticatorTest.java", "src/org/chromium/chrome/browser/webapps/WebappDataStorageTest.java", "src/org/chromium/chrome/browser/webapps/WebappDirectoryManagerTest.java", + "src/org/chromium/chrome/browser/webapps/WebappFilterTouchUnitTest.java", "src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderTest.java", "src/org/chromium/chrome/browser/webapps/WebappLauncherActivityTest.java", "src/org/chromium/chrome/browser/webapps/WebappRegistryTest.java",
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page