Medium watch CVSS 6.5 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Messages
DescriptionInappropriate implementation in Messages
ComponentMessages
Sub-componentMobile>Intents
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker356658477
Fix commit95729cec0565 (chromium/src) +169/-1
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedMohit Raj (shadow2639)
Reported2024-07-31 · bounty $1000 (per vendor advisory)
Fix landed2025-04-03 (public git)
Disclosed2025-05-27
Patch gap54 days — fix public in source before the advisory

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.

Key insight
The single mistake was making the message’s primary action clickable at the very instant the banner animated in, with no debounce; the fix introduces a bounded tap-protection interval keyed off the animation start so taps arriving during the appearance are ignored.

Attack Path

  1. 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.
  2. 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 by message_primary_button.
  3. Trigger the action unintentionally The user’s tap invokes ON_PRIMARY_ACTION before 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

An attacker can cause a user to unintentionally activate a message banner’s primary action by exploiting the absence of a tap-protection interval during the banner’s enter animation, an “inappropriate implementation” (LogicError) in the Messages component on Chrome for Android. The gain is limited to whatever the surfaced message’s primary action performs; no memory-safety corruption is demonstrated by the patch, and any escalation beyond triggering the action is (inferred). The advisory does not indicate in-the-wild exploitation.

Files Changed

  • chrome/android/javatests/BUILD.gn
  • chrome/android/javatests/src/org/chromium/chrome/browser/messages/MessageTest.java
  • chrome/android/javatests/src/org/chromium/chrome/browser/messages/OWNERS
  • components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerMediator.java

Audit Directions

  • Newly-appearing actionable UI
    Review 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 ordering
    Look 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 mechanisms
    Audit features guarded by ...ForTesting overrides (like sTapProtectionDurationMsForTesting) to ensure the protection is actually active in production builds and not inadvertently bypassed.
From 95729cec0565596f159af487d0bceeb82a7755b8 Mon Sep 17 00:00:00 2001
From: Lijin Shen <[email protected]>
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 <[email protected]>
Commit-Queue: Lijin Shen <[email protected]>
Reviewed-by: Theresa Sullivan <[email protected]>
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(
Loading diff…

Regression Test / PoC

shipped with the fix
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);
Loading diff…

Original Bug Report

reported by [email protected]

Android Chrome External Navigation Bubble Tapjacking

Steps to reproduce the problem

  1. 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)
  2. Open Chrome and visit - http://bugtest.unaux.com/bubble.html
  3. Click on the red box 3 times
  4. 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

View on issue tracker