Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactClickjacking in Messages
DescriptionClickjacking in Messages
ComponentMessages
Sub-componentMessages
Bug ClassLogic Error
Tracker518108937
Fix commit0b986c551c86 (chromium/src) +284/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerView.java
modified
if
components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerViewTest.java
modified

Files Changed

  • components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerViewTest.java
  • components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerView.java
From 0b986c551c86c252ea7d985aa8a3315058239dcc Mon Sep 17 00:00:00 2001
From: Aishwarya Rajesh <[email protected]>
Date: Wed, 02 Sep 2026 12:56:25 -0700
Subject: [PATCH] [Messages] Arm key events and click handlers with tap protection

Tap protection on MessageBannerView was previously enforced solely
via touch event interception. This allowed hardware keyboard, D-pad,
and programmatic activations to bypass the protection window.

* Block non-system key events in MessageBannerView during the tap
  protection period while allowing system keys to pass through.
* Route all banner click listeners through a protected helper to
  ignore clicks executed during tap protection.

Bug: 518108937
Change-Id: Id9ad626db86e56ff5697598245d8ead8d121610c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8333047
Reviewed-by: Charles Hager <[email protected]>
Commit-Queue: Aishwarya Rajesh <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1691010}
---
diff --git a/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerView.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerView.java
index 978a1a0e..e428a5b 100644
--- a/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerView.java
+++ b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerView.java
@@ -16,6 +16,7 @@
 import android.graphics.drawable.Drawable;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -99,7 +100,8 @@
         mSecondaryButton = findViewById(R.id.message_secondary_button);
         mCloseButton = findViewById(R.id.message_close_button);
         mDivider = findViewById(R.id.message_divider);
-        mSecondaryButton.setOnClickListener(
+        setProtectedOnClickListener(
+                mSecondaryButton,
                 (View v) -> {
                     handleSecondaryButtonClick();
                 });
@@ -229,8 +231,13 @@
         mPrimaryButton.setVisibility(showButton ? VISIBLE : GONE);
     }
 
-    void setPrimaryButtonClickListener(OnClickListener listener) {
-        mPrimaryButton.setOnClickListener(
+    void setPrimaryButtonClickListener(@Nullable OnClickListener listener) {
+        if (listener == null) {
+            setProtectedOnClickListener(mPrimaryButton, null);
+            return;
+        }
+        setProtectedOnClickListener(
+                mPrimaryButton,
                 (view) -> {
                     // Ignore click events if a progress bar is showing.
                     if (mPrimaryWidgetAppearance == PrimaryWidgetAppearance.BUTTON_IF_TEXT_IS_SET
@@ -244,8 +251,8 @@
         mEnableCloseButton = enable;
     }
 
-    void setCloseButtonClickListener(OnClickListener listener) {
-        mCloseButton.setOnClickListener(listener);
+    void setCloseButtonClickListener(@Nullable OnClickListener listener) {
+        setProtectedOnClickListener(mCloseButton, listener);
     }
 
     void setSecondaryIcon(Drawable icon) {
@@ -498,6 +505,28 @@
         return super.requestFocus(direction, previouslyFocusedRect);
     }
 
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        // Consume events within the protection period, but do not block system keys (e.g. back,
+        // volume, power).
+        if (isWithinTapProtectionPeriod() && !event.isSystem()) {
+            return true;
+        }
+        return super.dispatchKeyEvent(event);
+    }
+
+    private void setProtectedOnClickListener(View view, @Nullable OnClickListener listener) {
+        if (listener == null) {
+            view.setOnClickListener(null);
+            return;
+        }
+        view.setOnClickListener(
+                (v) -> {
+                    if (isWithinTapProtectionPeriod()) return;
+                    listener.onClick(v);
+                });
+    }
+
     private boolean isWithinTapProtectionPeriod() {
         return mIsWithinTapProtectionPeriodSupplier != null
                 && Boolean.TRUE.equals(mIsWithinTapProtectionPeriodSupplier.get());
diff --git a/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerViewTest.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerViewTest.java
index 05fb2d6..5afc569 100644
--- a/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerViewTest.java
+++ b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerViewTest.java
@@ -19,6 +19,7 @@
 import android.content.res.Resources;
 import android.os.SystemClock;
 import android.view.InputDevice;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -59,6 +60,7 @@
 
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /** Instrumentation tests for MessageBannerView. */
 @RunWith(BaseJUnit4ClassRunner.class)
@@ -945,6 +947,254 @@
                 });
     }
 
+    /**
+     * Tests that dispatchKeyEvent consumes and blocks non-system key events during the tap
+     * protection period while allowing system key events to pass through.
+     */
+    @Test
+    @MediumTest
+    public void testDispatchKeyEvent_BlocksNonSystemKeysDuringTapProtection() {
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    PropertyModel propertyModel =
+                            new PropertyModel.Builder(MessageBannerProperties.ALL_KEYS)
+                                    .with(
+                                            MessageBannerProperties.MESSAGE_IDENTIFIER,
+                                            MessageIdentifier.TEST_MESSAGE)
+                                    .with(MessageBannerProperties.TITLE, "test")
+                                    .with(
+                                            MessageBannerProperties
+                                                    .IS_WITHIN_TAP_PROTECTION_PERIOD_SUPPLIER,
+                                            () -> true)
+                                    .build();
+                    PropertyModelChangeProcessor.create(
+                            propertyModel, mMessageBannerView, MessageBannerViewBinder::bind);
+                });
+
+        KeyEvent enterEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER);
+        KeyEvent dpadCenterEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER);
+        KeyEvent backEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
+
+        boolean enterHandled =
+                ThreadUtils.runOnUiThreadBlocking(
+                        () -> mMessageBannerView.dispatchKeyEvent(enterEvent));
+        boolean dpadCenterHandled =
+                ThreadUtils.runOnUiThreadBlocking(
+                        () -> mMessageBannerView.dispatchKeyEvent(dpadCenterEvent));
+        boolean backHandled =
+                ThreadUtils.runOnUiThreadBlocking(
+                        () -> mMessageBannerView.dispatchKeyEvent(backEvent));
+
+        Assert.assertTrue(
+                "Enter key event should be consumed and blocked during tap protection.",
+                enterHandled);
+        Assert.assertTrue(
+                "D-pad center key event should be consumed and blocked during tap protection.",
+                dpadCenterHandled);
+        Assert.assertFalse(
+                "System key (Back) should not be consumed by tap protection.", backHandled);
+    }
+
+    /**
+     * Tests that dispatchKeyEvent allows key events to be dispatched normally after the tap
+     * protection period expires.
+     */
+    @Test
+    @MediumTest
+    public void testDispatchKeyEvent_AllowsKeysAfterTapProtectionExpires() {
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    PropertyModel propertyModel =
+                            new PropertyModel.Builder(MessageBannerProperties.ALL_KEYS)
+                                    .with(
+                                            MessageBannerProperties.MESSAGE_IDENTIFIER,
+                                            MessageIdentifier.TEST_MESSAGE)
+                                    .with(MessageBannerProperties.TITLE, "test")
+                                    .with(
+                                            MessageBannerProperties
+                                                    .IS_WITHIN_TAP_PROTECTION_PERIOD_SUPPLIER,
+                                            () -> false)
+                                    .build();
+                    PropertyModelChangeProcessor.create(
+                            propertyModel, mMessageBannerView, MessageBannerViewBinder::bind);
+                });
+
+        KeyEvent enterEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER);
+        boolean enterHandled =
+                ThreadUtils.runOnUiThreadBlocking(
+                        () -> mMessageBannerView.dispatchKeyEvent(enterEvent));
+        Assert.assertFalse(
+                "Key event should not be unconditionally blocked after tap protection expires.",
+                enterHandled);
+    }
+
+    /** Tests that primary button clicks are ignored during the tap protection period. */
+    @Test
+    @MediumTest
+    public void testPrimaryButtonClick_BlockedDuringTapProtection() {
+        final AtomicBoolean called = new AtomicBoolean();
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    PropertyModel propertyModel =
+                            new PropertyModel.Builder(MessageBannerProperties.ALL_KEYS)
+                                    .with(
+                                            MessageBannerProperties.MESSAGE_IDENTIFIER,
+                                            MessageIdentifier.TEST_MESSAGE)
+                                    .with(
+                                            MessageBannerProperties.PRIMARY_WIDGET_APPEARANCE,
+                                            PrimaryWidgetAppearance.BUTTON_IF_TEXT_IS_SET)
+                                    .with(
+                                            MessageBannerProperties.PRIMARY_BUTTON_TEXT,
+                                            PRIMARY_BUTTON_TEXT)
+                                    .with(
+                                            MessageBannerProperties.PRIMARY_BUTTON_CLICK_LISTENER,
+                                            (v) -> called.set(true))
+                                    .with(
+                                            MessageBannerProperties
+                                                    .IS_WITHIN_TAP_PROTECTION_PERIOD_SUPPLIER,
+                                            () -> true)
+                                    .build();
+                    PropertyModelChangeProcessor.create(
+                            propertyModel, mMessageBannerView, MessageBannerViewBinder::bind);
+                });
+
+        ThreadUtils.runOnUiThreadBlocking(() -> mPrimaryButton.performClick());
+        Assert.assertFalse(
+                "Primary button click should be ignored during tap protection.", called.get());
+    }
+
+    /** Tests that secondary button clicks are ignored during the tap protection period. */
+    @Test
+    @MediumTest
+    public void testSecondaryButtonClick_BlockedDuringTapProtection() {
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    PropertyModel propertyModel =
+                            new PropertyModel.Builder(MessageBannerProperties.ALL_KEYS)
+                                    .with(
+                                            MessageBannerProperties.MESSAGE_IDENTIFIER,
+                                            MessageIdentifier.TEST_MESSAGE)
+                                    .with(
+                                            MessageBannerProperties.SECONDARY_ICON_RESOURCE_ID,
+                                            android.R.drawable.ic_menu_add)
+                                    .with(
+                                            MessageBannerProperties.ON_SECONDARY_BUTTON_CLICK,
+                                            mSecondaryActionCallback)
+                                    .with(
+                                            MessageBannerProperties
+                                                    .IS_WITHIN_TAP_PROTECTION_PERIOD_SUPPLIER,
+                                            () -> true)
+                                    .build();
+                    PropertyModelChangeProcessor.create(
+                            propertyModel, mMessageBannerView, MessageBannerViewBinder::bind);
+                });
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () ->
+                        mMessageBannerView
+                                .findViewById(R.id.message_secondary_button)
+                                .performClick());
+        Mockito.verify(mSecondaryActionCallback, Mockito.never()).run();
+    }
+
+    /** Tests that close button clicks are ignored during the tap protection period. */
+    @Test
+    @MediumTest
+    public void testCloseButtonClick_BlockedDuringTapProtection() {
+        final AtomicBoolean called = new AtomicBoolean();
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    PropertyModel propertyModel =
+                            new PropertyModel.Builder(MessageBannerProperties.ALL_KEYS)
+                                    .with(
+                                            MessageBannerProperties.MESSAGE_IDENTIFIER,
+                                            MessageIdentifier.TEST_MESSAGE)
+                                    .with(
+                                            MessageBannerProperties.CLOSE_BUTTON_CLICK_LISTENER,
+                                            (v) -> called.set(true))
+                                    .with(
+                                            MessageBannerProperties
+                                                    .IS_WITHIN_TAP_PROTECTION_PERIOD_SUPPLIER,
+                                            () -> true)
+                                    .build();
+                    PropertyModelChangeProcessor.create(
+                            propertyModel, mMessageBannerView, MessageBannerViewBinder::bind);
+                });
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> mMessageBannerView.findViewById(R.id.message_close_button).performClick());
+        Assert.assertFalse(
+                "Close button click should be ignored during tap protection.", called.get());
+    }
+
+    /**
+     * Tests that all clickable elements across the entire MessageBannerView hierarchy are blocked
+     * from executing click actions during the tap protection period.
+     */
+    @Test
+    @MediumTest
+    public void testAllClickableElements_BlockedDuringTapProtection() {
+        final AtomicInteger clickCount = new AtomicInteger();
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    PropertyModel propertyModel =
+                            new PropertyModel.Builder(MessageBannerProperties.ALL_KEYS)
+                                    .with(
+                                            MessageBannerProperties.MESSAGE_IDENTIFIER,
+                                            MessageIdentifier.TEST_MESSAGE)
+                                    .with(MessageBannerProperties.TITLE, "test")
+                                    .with(
+                                            MessageBannerProperties.PRIMARY_WIDGET_APPEARANCE,
+                                            PrimaryWidgetAppearance.BUTTON_IF_TEXT_IS_SET)
+                                    .with(
+                                            MessageBannerProperties.PRIMARY_BUTTON_TEXT,
+                                            PRIMARY_BUTTON_TEXT)
+                                    .with(
+                                            MessageBannerProperties.PRIMARY_BUTTON_CLICK_LISTENER,
+                                            (v) -> clickCount.incrementAndGet())
+                                    .with(
+                                            MessageBannerProperties.SECONDARY_ICON_RESOURCE_ID,
+                                            android.R.drawable.ic_menu_add)
+                                    .with(
+                                            MessageBannerProperties.ON_SECONDARY_BUTTON_CLICK,
+                                            clickCount::incrementAndGet)
+                                    .with(MessageBannerProperties.ENABLE_CLOSE_BUTTON, true)
+                                    .with(
+                                            MessageBannerProperties.CLOSE_BUTTON_CLICK_LISTENER,
+                                            (v) -> clickCount.incrementAndGet())
+                                    .with(
+                                            MessageBannerProperties
+                                                    .IS_WITHIN_TAP_PROTECTION_PERIOD_SUPPLIER,
+                                            () -> true)
+                                    .build();
+                    PropertyModelChangeProcessor.create(
+                            propertyModel, mMessageBannerView, MessageBannerViewBinder::bind);
+                });
+
+        ThreadUtils.runOnUiThreadBlocking(
+                () ->
+                        assertNoClickListenersFiredDuringTapProtection(
+                                mMessageBannerView, clickCount));
+    }
+
+    private void assertNoClickListenersFiredDuringTapProtection(
+            View root, AtomicInteger clickCount) {
+        if (root.hasOnClickListeners()) {
+            root.performClick();
+            Assert.assertEquals(
+                    "Click on view with id "
+                            + root.getId()
+                            + " should be blocked during tap protection.",
+                    0,
+                    clickCount.get());
+        }
+        if (root instanceof ViewGroup group) {
+            for (int i = 0; i < group.getChildCount(); i++) {
+                assertNoClickListenersFiredDuringTapProtection(group.getChildAt(i), clickCount);
+            }
+        }
+    }
+
     private void assertIsLoading(boolean isLoading) {
         Assert.assertEquals(isLoading ? View.GONE : View.VISIBLE, mPrimaryButton.getVisibility());
         Assert.assertEquals(isLoading ? View.VISIBLE : View.GONE, mLoadingSpinner.getVisibility());
Loading diff…

Original Bug Report

reported by [email protected]

Tap-protection bypass on MessageBannerView via hardware keyboard/D-pad key events

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The clickjacking and tap-protection mitigation in MessageBannerView is only enforced on the touch event dispatch path. If a user interacts with the banner using a physical keyboard or D-pad, key events such as ENTER or SPACE bypass these checks entirely. This potentially allows timed banner prompts to be clicked immediately upon appearing, bypassing the safety window.

Affected files:

  • components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerView.java

Estimated timestamp from git blame: 2025-04-03

Summary

MessageBannerView implements a clickjacking/tap-protection mitigation designed to swallow inputs for a short period (~775 ms plus enter animation time) after a banner becomes visible to prevent accidental or clickjacked clicks. However, this protection is exclusively implemented on the touch-event dispatch path (onInterceptTouchEvent and onTouchEvent). When a user interacts with Chrome using a physical keyboard, Chromebook keyboard, or D-pad, key events (such as pressing ENTER or SPACE) bypass these touch handlers and trigger button actions instantly, bypassing the safety window.

Potential Root Cause

In components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerView.java, the tap-protection period check is only performed in touch event intercept and touch event handlers:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return isWithinTapProtectionPeriod() || super.onInterceptTouchEvent(ev);
}

@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
    if (isWithinTapProtectionPeriod()) return true;
    ...
}

When a hardware key event (such as KeyEvent.KEYCODE_ENTER or KeyEvent.KEYCODE_SPACE) is dispatched to a focused button like the primary action button, Android’s view framework processes the key event and triggers performClick() directly on the button without invoking onInterceptTouchEvent or onTouchEvent on the parent layout.

At the same time, the click listener registered on the primary button does not check if the banner is currently within the tap-protection window:

void setPrimaryButtonClickListener(OnClickListener listener) {
    mPrimaryButton.setOnClickListener(
            (view) -> {
                // Ignore click events if a progress bar is showing.
                if (mPrimaryWidgetAppearance == PrimaryWidgetAppearance.BUTTON_IF_TEXT_IS_SET
                        && !TextUtils.isEmpty(mPrimaryButtonText)) {
                    listener.onClick(view); // <-- Does not check isWithinTapProtectionPeriod()
                }
            });
}

Thus, any key-driven activation is executed instantly, completely ignoring the mIsWithinTapProtectionPeriodSupplier timer.

Potential Attack Vector

  1. An attacker-controlled page prompts a user for repetitive key inputs (e.g., a simple keypress game or a form requiring multiple rapid ENTER or SPACE keypresses).
  2. Timed with the user’s expected keypress, the attacker triggers a feature that presents a Messages-backed prompt (such as a Framebust Blocked permission or Popup Blocked action).
  3. When the MessageBannerView is shown, focus transitions or default focus lands on message_primary_button (which is focusable and allows descendant focus).
  4. The next ENTER or SPACE key event is dispatched to the focused button, immediately executing the action inside the protected window.

Note: These are potential steps deduced from a source-code review; our analysis was performed via static analysis without running functional exploit code.

Suggested Fix

To secure MessageBannerView against this bypass, follow the secure patterns implemented in other components (such as ModalDialogView):

  1. Modify MessageBannerView’s click listeners (both for primary and secondary actions) to explicitly verify !isWithinTapProtectionPeriod() before executing the underlying callbacks.
  2. Override dispatchKeyEvent in MessageBannerView to block keyboard key events during the active tap-protection window (except system-critical keys), ensuring that key actions cannot trigger button clicks while tap-protection is active.

Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker