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 <hartmanng@chromium.org>
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 <peconn@chromium.org>
Commit-Queue: Glenn Hartmann <hartmanng@chromium.org>
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