Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Prompts
DescriptionMemory corruption in Prompts
ComponentPrompts
Sub-componentUI>Browser>Permissions>Prompts
Bug ClassMemory Corruption
Tracker517477395
Fix commit655721cad4ac (chromium/src) +151/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Files Changed

  • chrome/android/junit/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialogTest.java
  • chrome/android/java/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialog.java
From 655721cad4ac2ba83b2dcadd650c06bd0a9f649a Mon Sep 17 00:00:00 2001
From: Thomas Nguyen <[email protected]>
Date: Wed, 16 Sep 2026 02:05:51 -0700
Subject: [PATCH] Enable tapjacking protection on PermissionBlockedDialog

Set FILTER_TOUCH_FOR_SECURITY and BUTTON_TAP_PROTECTION_PERIOD_MS in
PermissionBlockedDialog.show() to discard obscured touches and ignore
taps for UiUtils.PROMPT_INPUT_PROTECTION_SHORT_DELAY_MS after the
dialog is displayed.

Fixed: 517477395
Change-Id: Icca01b3d1d7cf07f6646dfaa03b0e2ae255965fc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8409808
Reviewed-by: Antonio Sartori <[email protected]>
Commit-Queue: Thomas Nguyen <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1698763}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialog.java b/chrome/android/java/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialog.java
index 52e98e6..d2e1b7b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialog.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialog.java
@@ -41,6 +41,7 @@
 import org.chromium.components.content_settings.ContentSettingsType;
 import org.chromium.components.page_info.PageInfoController;
 import org.chromium.content_public.browser.WebContents;
+import org.chromium.ui.UiUtils;
 import org.chromium.ui.base.WindowAndroid;
 import org.chromium.ui.modaldialog.DialogDismissalCause;
 import org.chromium.ui.modaldialog.ModalDialogManager;
@@ -115,6 +116,12 @@
                         .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, positiveButtonLabel)
                         .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, negativeButtonLabel)
                         .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, true)
+                        // Either button may grant the permission, so protect against tapjacking
+                        // in the same way as the regular permission prompt.
+                        .with(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY, true)
+                        .with(
+                                ModalDialogProperties.BUTTON_TAP_PROTECTION_PERIOD_MS,
+                                UiUtils.PROMPT_INPUT_PROTECTION_SHORT_DELAY_MS)
                         .build();
         mModalDialogManager.showDialog(mPropertyModel, ModalDialogType.APP);
     }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialogTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialogTest.java
index 9c493eb..b4d09f2 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialogTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialogTest.java
@@ -6,16 +6,27 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
 import android.text.Spanned;
 import android.text.style.ClickableSpan;
+import android.view.ContextThemeWrapper;
+import android.view.InputDevice;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
 import android.widget.TextView;
 
+import androidx.annotation.IdRes;
+
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -26,8 +37,15 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 import org.robolectric.Robolectric;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowSystemClock;
 
 import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.R;
+import org.chromium.components.browser_ui.modaldialog.ModalDialogView;
+import org.chromium.components.browser_ui.modaldialog.ModalDialogViewBinder;
+import org.chromium.ui.UiUtils;
+import org.chromium.ui.base.TestActivity;
 import org.chromium.ui.base.WindowAndroid;
 import org.chromium.ui.modaldialog.DialogDismissalCause;
 import org.chromium.ui.modaldialog.ModalDialogManager;
@@ -35,8 +53,10 @@
 import org.chromium.ui.modaldialog.ModalDialogProperties;
 import org.chromium.ui.modaldialog.ModalDialogProperties.ButtonType;
 import org.chromium.ui.modelutil.PropertyModel;
+import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
 
 import java.lang.ref.WeakReference;
+import java.time.Duration;
 
 /** Unit tests for {@link PermissionBlockedDialog}. */
 @RunWith(BaseRobolectricTestRunner.class)
@@ -48,6 +68,7 @@
 
     @Captor private ArgumentCaptor<PropertyModel> mPropertyModelCaptor;
 
+    private ActivityController<TestActivity> mActivityController;
     private Activity mActivity;
     private PermissionBlockedDialog mDialog;
     private static final long NATIVE_CONTROLLER = 12345L;
@@ -56,13 +77,19 @@
     public void setUp() {
         PermissionBlockedDialogJni.setInstanceForTesting(mNativeMock);
 
-        mActivity = Robolectric.buildActivity(Activity.class).setup().get();
+        mActivityController = Robolectric.buildActivity(TestActivity.class).setup();
+        mActivity = mActivityController.get();
         when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<>(mActivity));
         when(mWindowAndroid.getModalDialogManager()).thenReturn(mModalDialogManager);
 
         mDialog = new PermissionBlockedDialog(NATIVE_CONTROLLER, mWindowAndroid);
     }
 
+    @After
+    public void tearDown() {
+        mActivityController.destroy();
+    }
+
     @Test
     public void testShow() {
         String title = "Title";
@@ -84,6 +111,12 @@
         View customView = model.get(ModalDialogProperties.CUSTOM_VIEW);
         assertTrue(customView instanceof TextView);
         assertEquals(content, ((TextView) customView).getText().toString());
+
+        // The dialog can grant a permission, so it must be protected against tapjacking.
+        assertTrue(model.get(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY));
+        assertEquals(
+                UiUtils.PROMPT_INPUT_PROTECTION_SHORT_DELAY_MS,
+                model.get(ModalDialogProperties.BUTTON_TAP_PROTECTION_PERIOD_MS));
     }
 
     @Test
@@ -143,4 +176,114 @@
         spans[0].onClick(message);
         verify(mNativeMock).onLearnMoreClicked(NATIVE_CONTROLLER);
     }
+
+    @Test
+    public void testButtonTapProtection_disablesClickDuringProtectionPeriod_positiveButton() {
+        checkButtonTapProtection(
+                R.id.positive_button,
+                () -> verify(mNativeMock).onPrimaryButtonClicked(NATIVE_CONTROLLER),
+                DialogDismissalCause.POSITIVE_BUTTON_CLICKED);
+    }
+
+    @Test
+    public void testButtonTapProtection_disablesClickDuringProtectionPeriod_negativeButton() {
+        checkButtonTapProtection(
+                R.id.negative_button,
+                () -> verify(mNativeMock).onNegativeButtonClicked(NATIVE_CONTROLLER),
+                DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
+    }
+
+    private void checkButtonTapProtection(
+            @IdRes int buttonId, Runnable verifyAcceptedClick, int expectedDismissalCause) {
+        mDialog.show("Title", "Content", "Allow", "Deny", "");
+        verify(mModalDialogManager)
+                .showDialog(mPropertyModelCaptor.capture(), eq(ModalDialogType.APP));
+        PropertyModel model = mPropertyModelCaptor.getValue();
+
+        ModalDialogView dialogView =
+                (ModalDialogView)
+                        LayoutInflater.from(
+                                        new ContextThemeWrapper(
+                                                mActivity,
+                                                R.style
+                                                        .ThemeOverlay_BrowserUI_ModalDialog_TextPrimaryButton))
+                                .inflate(R.layout.modal_dialog_view, null);
+        PropertyModelChangeProcessor.create(model, dialogView, new ModalDialogViewBinder());
+
+        // Attach to window to initialize the protection period timestamp.
+        mActivity.setContentView(dialogView);
+
+        View button = dialogView.findViewById(buttonId);
+
+        // Click immediately while within protection period - should be ignored.
+        button.performClick();
+        verify(mNativeMock, never()).onPrimaryButtonClicked(anyLong());
+        verify(mNativeMock, never()).onNegativeButtonClicked(anyLong());
+        verify(mModalDialogManager, never()).dismissDialog(any(), anyInt());
+
+        // Advance time by less than the protection period - should still be ignored.
+        ShadowSystemClock.advanceBy(
+                Duration.ofMillis(UiUtils.PROMPT_INPUT_PROTECTION_SHORT_DELAY_MS / 2));
+        button.performClick();
+        verify(mNativeMock, never()).onPrimaryButtonClicked(anyLong());
+        verify(mNativeMock, never()).onNegativeButtonClicked(anyLong());
+        verify(mModalDialogManager, never()).dismissDialog(any(), anyInt());
+
+        // Advance time past the protection period.
+        ShadowSystemClock.advanceBy(
+                Duration.ofMillis(UiUtils.PROMPT_INPUT_PROTECTION_SHORT_DELAY_MS + 100));
+
+        // Click should now be accepted and invoke the controller callback.
+        button.performClick();
+        verifyAcceptedClick.run();
+        verify(mModalDialogManager).dismissDialog(model, expectedDismissalCause);
+    }
+
+    @Test
+    public void testTouchFilterForSecurity() {
+        mDialog.show("Title", "Content", "Allow", "Deny", "");
+        verify(mModalDialogManager)
+                .showDialog(mPropertyModelCaptor.capture(), eq(ModalDialogType.APP));
+        PropertyModel model = mPropertyModelCaptor.getValue();
+
+        ModalDialogView dialogView =
+                (ModalDialogView)
+                        LayoutInflater.from(
+                                        new ContextThemeWrapper(
+                                                mActivity,
+                                                R.style
+                                                        .ThemeOverlay_BrowserUI_ModalDialog_TextPrimaryButton))
+                                .inflate(R.layout.modal_dialog_view, null);
+        PropertyModelChangeProcessor.create(model, dialogView, new ModalDialogViewBinder());
+
+        View positiveButton = dialogView.findViewById(R.id.positive_button);
+        View negativeButton = dialogView.findViewById(R.id.negative_button);
+
+        // Verify touch filtering is enabled on both buttons.
+        assertTrue(positiveButton.getFilterTouchesWhenObscured());
+        assertTrue(negativeButton.getFilterTouchesWhenObscured());
+
+        // Dispatch an obscured touch event to the buttons - they should be consumed/blocked.
+        MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
+        props.id = 0;
+        MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+        MotionEvent obscuredEvent =
+                MotionEvent.obtain(
+                        /* downTime= */ 0,
+                        /* eventTime= */ 0,
+                        /* action= */ MotionEvent.ACTION_DOWN,
+                        /* pointerCount= */ 1,
+                        new MotionEvent.PointerProperties[] {props},
+                        new MotionEvent.PointerCoords[] {coords},
+                        /* metaState= */ 0,
+                        /* buttonState= */ 0,
+                        /* xPrecision= */ 1.0f,
+                        /* yPrecision= */ 1.0f,
+                        /* deviceId= */ 0,
+                        /* edgeFlags= */ 0,
+                        /* source= */ InputDevice.SOURCE_CLASS_POINTER,
+                        /* flags= */ MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED);
+        assertTrue(positiveButton.dispatchTouchEvent(obscuredEvent));
+        assertTrue(negativeButton.dispatchTouchEvent(obscuredEvent));
+    }
 }
Loading diff…

Original Bug Report

reported by [email protected]

Overlay tapjacking in PermissionBlockedDialog allows unauthorized permission grants

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 PermissionBlockedDialog on Android does not enable touch filtering or button tap protection in its PropertyModel. This potentially allows a co-installed malicious app with screen overlay permissions to perform a tapjacking attack on the dialog. Consequently, a malicious web origin could obtain sensitive permissions like notifications or geolocation without the user’s informed consent.

Affected files:

  • chrome/android/java/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialog.java

Estimated timestamp from git blame: 2022-01-15

Root Cause Analysis

In Chrome on Android, PermissionBlockedDialog is the modal dialog shown when a user manages quiet permission prompts for notifications and geolocation. The PropertyModel builder for this dialog does not configure either touch filtering or button tap protection:

// chrome/android/java/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialog.java
mPropertyModel =
        new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                .with(ModalDialogProperties.CONTROLLER, this)
                .with(ModalDialogProperties.TITLE, title)
                .with(ModalDialogProperties.CUSTOM_VIEW, message)
                .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, positiveButtonLabel)
                .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, negativeButtonLabel)
                .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, true)
                .build();
mModalDialogManager.showDialog(mPropertyModel, ModalDialogType.APP);

Both parameters default to disabled (FILTER_TOUCH_FOR_SECURITY is false and BUTTON_TAP_PROTECTION_PERIOD_MS is 0). Consequently, the dialog’s buttons are immediately active upon display and obscured touch events are accepted and processed instead of being discarded.

By contrast, standard permission prompts configured via PermissionDialogModelFactory.java explicitly enable these protections to prevent tapjacking:

// components/permissions/android/java/src/org/chromium/components/permissions/PermissionDialogModelFactory.java
.with(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY, true)
.with(ModalDialogProperties.TOUCH_FILTERED_CALLBACK, touchFilteredCallback)
.with(ModalDialogProperties.BUTTON_TAP_PROTECTION_PERIOD_MS,
      UiUtils.PROMPT_INPUT_PROTECTION_SHORT_DELAY_MS)

Potential Exploit Scenario

An attacker could potentially orchestrate a tapjacking attack using a co-installed malicious app on Android and a malicious webpage:

  1. The attacker’s webpage requests Notifications or Geolocation in a context where a quiet prompt is triggered (e.g., without a user gesture).
  2. The quiet permission prompt banner (Messages 2.0 UI) appears.
  3. A co-installed malicious app with SYSTEM_ALERT_WINDOW permission places a non-touchable visual overlay over the “Manage” gear/chip in the quiet UI, tricking the user into clicking it to open PermissionBlockedDialog.
  4. As soon as PermissionBlockedDialog opens, the overlay application repositions the overlay to align with the “Allow” button (the positive or negative button, depending on whether the quiet UI reason is flagged as disruptive).
  5. Since the dialog does not configure touch filtering or button tap protection, the obscured tap is processed instantly, and the origin is granted the requested permission without informed user consent.

Disclaimer: These are potential steps. Our static analysis tooling is unable to run dynamic proofs of concept.


Suggested Fix

Modify PermissionBlockedDialog.java to enable FILTER_TOUCH_FOR_SECURITY and configure a standard BUTTON_TAP_PROTECTION_PERIOD_MS inside the PropertyModel builder:

mPropertyModel =
        new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
                .with(ModalDialogProperties.CONTROLLER, this)
                .with(ModalDialogProperties.TITLE, title)
                .with(ModalDialogProperties.CUSTOM_VIEW, message)
                .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, positiveButtonLabel)
                .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, negativeButtonLabel)
                .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, true)
                .with(ModalDialogProperties.FILTER_TOUCH_FOR_SECURITY, true)
                .with(
                        ModalDialogProperties.BUTTON_TAP_PROTECTION_PERIOD_MS,
                        org.chromium.ui.UiUtils.PROMPT_INPUT_PROTECTION_SHORT_DELAY_MS)
                .build();

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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