CVE-2026-113171
Overview
Files Changed
chrome/android/junit/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialogTest.javachrome/android/java/src/org/chromium/chrome/browser/permissions/PermissionBlockedDialog.java
Patch
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)); + } }
Original Bug Report
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:
- The attacker’s webpage requests
NotificationsorGeolocationin a context where a quiet prompt is triggered (e.g., without a user gesture). - The quiet permission prompt banner (Messages 2.0 UI) appears.
- A co-installed malicious app with
SYSTEM_ALERT_WINDOWpermission places a non-touchable visual overlay over the “Manage” gear/chip in the quiet UI, tricking the user into clicking it to openPermissionBlockedDialog. - As soon as
PermissionBlockedDialogopens, 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). - 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.