CVE-2025-5066
Overview
Background
- Message banner
- Chrome for Android’s transient UI card (managed by
MessageBannerMediator) that slides in from the top and exposes a primary action button (R.id.message_primary_button). - Tap protection
- a short interval after the banner animates into place during which touch/click input is ignored, so a tap cannot be consumed the instant the actionable UI appears.
- `Position.FRONT`
- the model state indicating the banner has been promoted to the visible/front position, i.e. the point at which its enter animation begins.
- `sTapProtectionDurationMsForTesting`
- a test-only override (default
TAP_PROTECTION_DURATION_MS= 500 ms) that gates the protection so it can be exercised deterministically in tests.
Root Cause Analysis
Before the patch, when a message banner transitioned to Position.FRONT and started its enter animation, its primary action button was immediately clickable — no interval prevented input from being processed while the card was still animating into view. This violates the invariant that an actionable UI element should not accept a tap the same moment it becomes visible, because a user’s in-progress tap (or a tap induced against an element that just appeared under the finger) can be routed to the message’s primary action before the user can perceive it.
The fix adds a tap-protection window: on transition to Position.FRONT, the mediator records TimeUtils.elapsedRealtimeMillis() and sets the model so that input is suppressed for TAP_PROTECTION_DURATION_MS (500 ms), after which the message becomes clickable again. The new IS_WITHIN_TAP_PROTECTION_PERIOD_SUPPLIER property exposes this state to the view layer so the button rejects taps during the window. The regression test testTapProtection confirms a click within the protection period does not invoke the action (getCallCount() == 0), while a click after the window fires it.
Attack Path
- Induce a message banner A malicious or crafted page performs an action that causes Chrome to enqueue a message banner with a primary action button, timing its appearance to coincide with expected user touch input.
- Race the animation
Because the primary button is clickable the moment the banner reaches
Position.FRONT, a tap that lands as the card slides in is immediately consumed bymessage_primary_button. - Trigger the action unintentionally
The user’s tap invokes
ON_PRIMARY_ACTIONbefore they can read or react to the banner, executing the message’s action without informed consent (a tapjacking/clickjacking-style outcome). (inferred) the specific action gained depends on which message type is surfaced.
Impact Assessment
(inferred). The advisory does not indicate in-the-wild exploitation.Files Changed
chrome/android/javatests/BUILD.gnchrome/android/javatests/src/org/chromium/chrome/browser/messages/MessageTest.javachrome/android/javatests/src/org/chromium/chrome/browser/messages/OWNERScomponents/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerMediator.java
Audit Directions
- Newly-appearing actionable UIReview other overlays, banners, dialogs, and permission surfaces that become interactive during or immediately after an animation, and confirm they debounce input for a protection window before accepting taps.
- Animation-start vs. input-enable orderingLook for code that enables click/touch handling at the same point it starts an enter transition, without gating on a timestamp or “protection period” supplier.
- Test-disabled security mechanismsAudit features guarded by
...ForTestingoverrides (likesTapProtectionDurationMsForTesting) to ensure the protection is actually active in production builds and not inadvertently bypassed.
Patch
From 95729cec0565596f159af487d0bceeb82a7755b8 Mon Sep 17 00:00:00 2001
From: Lijin Shen <lazzzis@google.com>
Date: Thu, 03 Apr 2025 15:02:33 -0700
Subject: [PATCH] Add tap protection to message ui
Disallow any touch event as soon as the animation starts. The message should become clickable again once the tap protection period ends.
Bug: 356658477
Change-Id: I3522cb07e0b9a4c6fbe9f88cbe12777ffc41b5d5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6411166
Reviewed-by: Aishwarya Rajesh <aishwaryarj@google.com>
Commit-Queue: Lijin Shen <lazzzis@google.com>
Reviewed-by: Theresa Sullivan <twellington@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1442451}
---
diff --git a/chrome/android/javatests/BUILD.gn b/chrome/android/javatests/BUILD.gn
index 49c8aa56..efe5e69 100644
--- a/chrome/android/javatests/BUILD.gn
+++ b/chrome/android/javatests/BUILD.gn
@@ -1527,6 +1527,7 @@
"src/org/chromium/chrome/browser/locale/LocaleManagerReferralTest.java",
"src/org/chromium/chrome/browser/locale/LocaleManagerTest.java",
"src/org/chromium/chrome/browser/login/ChromeHttpAuthHandlerTest.java",
+ "src/org/chromium/chrome/browser/messages/MessageTest.java",
"src/org/chromium/chrome/browser/modaldialog/ChromeTabModalPresenterTest.java",
"src/org/chromium/chrome/browser/modaldialog/ModalDialogViewRenderTest.java",
"src/org/chromium/chrome/browser/multiwindow/MultiWindowIntegrationTest.java",
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/messages/MessageTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/messages/MessageTest.java
new file mode 100644
index 0000000..675a46a
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/messages/MessageTest.java
@@ -0,0 +1,105 @@
+// Copyright 2025 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.messages;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.FakeTimeTestRule;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.test.util.Batch;
+import org.chromium.base.test.util.CallbackHelper;
+import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.chrome.browser.ChromeTabbedActivity;
+import org.chromium.chrome.browser.flags.ChromeSwitches;
+import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
+import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
+import org.chromium.chrome.test.R;
+import org.chromium.components.messages.MessageBannerProperties;
+import org.chromium.components.messages.MessageDispatcher;
+import org.chromium.components.messages.MessageDispatcherProvider;
+import org.chromium.components.messages.MessagesTestHelper;
+import org.chromium.components.messages.PrimaryActionClickBehavior;
+import org.chromium.ui.modelutil.PropertyModel;
+
+import java.util.concurrent.TimeoutException;
+
+/** Integration test of Message. */
+@RunWith(ChromeJUnit4ClassRunner.class)
+@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
+@Batch(Batch.PER_CLASS)
+public class MessageTest {
+ @ClassRule
+ public static ChromeTabbedActivityTestRule sActivityTestRule =
+ new ChromeTabbedActivityTestRule();
+
+ @Rule public FakeTimeTestRule mFakeTimeTestRule = new FakeTimeTestRule();
+
+ private ChromeTabbedActivity mActivity;
+ private MessageDispatcher mMessageDispatcher;
+
+ @Before
+ public void setUp() {
+ sActivityTestRule.startMainActivityOnBlankPage();
+ mActivity = sActivityTestRule.getActivity();
+ mMessageDispatcher =
+ ThreadUtils.runOnUiThreadBlocking(
+ () -> MessageDispatcherProvider.from(mActivity.getWindowAndroid()));
+ }
+
+ @After
+ public void tearDown() {
+ MessagesTestHelper.enableTapProtectionDuration(-1);
+ }
+
+ /**
+ * Test that message is not clickable within tap protection period and becomes clickable once it
+ * ends.
+ */
+ @Test
+ @SmallTest
+ public void testTapProtection() throws TimeoutException {
+ MessagesTestHelper.enableTapProtectionDuration(500);
+ CallbackHelper helper = new CallbackHelper();
+ PropertyModel model =
+ ThreadUtils.runOnUiThreadBlocking(
+ () ->
+ new PropertyModel.Builder(MessageBannerProperties.ALL_KEYS)
+ .with(MessageBannerProperties.TITLE, "Test title")
+ .with(MessageBannerProperties.PRIMARY_BUTTON_TEXT, "Action")
+ .with(MessageBannerProperties.ON_DISMISSED, (v) -> {})
+ .with(
+ MessageBannerProperties.ON_PRIMARY_ACTION,
+ () -> {
+ helper.notifyCalled();
+ return PrimaryActionClickBehavior
+ .DISMISS_IMMEDIATELY;
+ })
+ .build());
+ ThreadUtils.runOnUiThreadBlocking(
+ () -> {
+ mMessageDispatcher.enqueueWindowScopedMessage(model, true);
+ });
+ onView(withId(R.id.message_primary_button)).check(matches(isDisplayed()));
+ onView(withId(R.id.message_primary_button)).perform(click());
+ Assert.assertEquals("Not clickable with tap protection period", 0, helper.getCallCount());
+ mFakeTimeTestRule.advanceMillis(1500);
+ onView(withId(R.id.message_primary_button)).perform(click());
+ helper.waitForNext("Should able to click when tap protection period ends");
+ }
+}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/messages/OWNERS b/chrome/android/javatests/src/org/chromium/chrome/browser/messages/OWNERS
new file mode 100644
index 0000000..12c1e52
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/messages/OWNERS
@@ -0,0 +1 @@
+file://components/messages/OWNERS
\ No newline at end of file
diff --git a/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerMediator.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerMediator.java
index 7d04e76a..dd43d31 100644
--- a/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerMediator.java
+++ b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerMediator.java
@@ -7,6 +7,7 @@
import static org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.ScrollDirection.DOWN;
import static org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.ScrollDirection.UP;
import static org.chromium.components.messages.MessageBannerProperties.CONTENT_ALPHA;
+import static org.chromium.components.messages.MessageBannerProperties.IS_WITHIN_TAP_PROTECTION_PERIOD_SUPPLIER;
import static org.chromium.components.messages.MessageBannerProperties.MARGIN_TOP;
import static org.chromium.components.messages.MessageBannerProperties.TRANSLATION_X;
import static org.chromium.components.messages.MessageBannerProperties.TRANSLATION_Y;
@@ -21,7 +22,10 @@
import androidx.annotation.IntDef;
import org.chromium.base.MathUtils;
+import org.chromium.base.ResettersForTesting;
+import org.chromium.base.TimeUtils;
import org.chromium.base.supplier.Supplier;
+import org.chromium.build.BuildConfig;
import org.chromium.build.annotations.NullMarked;
import org.chromium.build.annotations.Nullable;
import org.chromium.components.browser_ui.widget.animation.CancelAwareAnimatorListener;
@@ -55,6 +59,8 @@
int GESTURE = 3;
}
+ private static final long TAP_PROTECTION_DURATION_MS = 500;
+
private static final int ENTER_DURATION_MS = 550;
private static final int EXIT_DURATION_MS = 350;
private static final TimeInterpolator TRANSLATION_ENTER_INTERPOLATOR =
@@ -63,6 +69,8 @@
Interpolators.EMPHASIZED_DECELERATE;
private static final TimeInterpolator EXIT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE;
+ private static long sTapProtectionDurationMsForTesting;
+
private final PropertyModel mModel;
private final Supplier<Integer> mMaxTranslationYSupplier;
private final Supplier<Integer> mTopOffsetSupplier;
@@ -132,6 +140,20 @@
mModel.set(TRANSLATION_Y, mModel.get(MARGIN_TOP) - mDefaultMarginTop);
mModel.set(MARGIN_TOP, mDefaultMarginTop);
}
+ if (toIndex == Position.FRONT
+ && (!BuildConfig.IS_FOR_TEST || sTapProtectionDurationMsForTesting > 0)) {
+ long startTimestamp = TimeUtils.elapsedRealtimeMillis();
+ long protectionDuration =
+ sTapProtectionDurationMsForTesting > 0
+ ? sTapProtectionDurationMsForTesting
+ : TAP_PROTECTION_DURATION_MS;
+ mModel.set(
Regression Test / PoC
diff --git a/components/messages/android/test/java/src/org/chromium/components/messages/MessagesTestHelper.java b/components/messages/android/test/java/src/org/chromium/components/messages/MessagesTestHelper.java
index c1bbb27cb..66a5e45 100644
--- a/components/messages/android/test/java/src/org/chromium/components/messages/MessagesTestHelper.java
+++ b/components/messages/android/test/java/src/org/chromium/components/messages/MessagesTestHelper.java
@@ -150,6 +150,14 @@
return ((SingleActionMessage) messageStateHandler).getModelForTesting();
}
+ /**
+ * Enable tap protection duration in Test environment, which is disabled by default in Test
+ * environment.
+ */
+ public static void enableTapProtectionDuration(long duration) {
+ MessageBannerMediator.setTapProtectionDurationMsForTesting(duration);
+ }
+
@NativeMethods
interface Natives {
void onMessageEnqueued(long nativeMessagesTestHelper);
Original Bug Report
Android Chrome External Navigation Bubble Tapjacking
Steps to reproduce the problem
- Make sure “com.sec.android.app.sbrowser” is installed on your Android device. (https://play.google.com/store/apps/details?id=com.sec.android.app.sbrowser)
- Open Chrome and visit - http://bugtest.unaux.com/bubble.html
- Click on the red box 3 times
- Notice, that “Samsung Browser” is triggered accidentally and inside it “http://evil.com” is opened.
Problem Description
Previously I reported issue 356038470 “Tapjacking on Intent chooser dialog box” which got marked as duplicate, so I got another way to perform browser downgrade attack using the External Navigation bubble tapjacking, which prompts user’s to open any installed apps using INTENT redirects.
If an attacker tricks user’s into clicking 2-3 times on the webpage on a specific point then the user’s will accidentally trigger the INTENT redirect and end up opening any malicious INTENTS.
Summary
Android Chrome External Navigation Bubble Tapjacking
Custom Questions
Reporter credit:
Mohit Raj (shadow2639)
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A