Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in WebApp Installs
DescriptionInappropriate implementation in WebApp Installs
ComponentWebApp Installs
Sub-componentClank-q4-fixit
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.
Tracker351564774
Fix commit83df5710daca (chromium/src) +15/-70
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L
CISA KEVNot listed
CreditedBharat (mrnoob)
Reported2024-07-07 · bounty $500 (per vendor advisory)
Fix landed2024-12-19 (public git)
Disclosed2025-03-04
Patch gap75 days — fix public in source before the advisory

Background

Bottom sheet
The Android UI surface implemented by BottomSheet.java that slides up from the bottom of the screen to host content such as the WebApp (PWA) install prompt.
`SheetState.SCROLLING`
The transient SheetState enum value indicating the sheet is mid-animation between resting positions (e.g. settling open or closed).
`TouchRestrictingFrameLayout`
The removed custom FrameLayout subclass whose stated job was to “ignore all user input based on the state of the bottom sheet,” delegating the gating decision to the BottomSheet it held via setBottomSheet.
Touch event dispatch
Android routes pointer input through onInterceptTouchEvent (a parent’s chance to claim the gesture) and then onTouchEvent (where the claiming view consumes it), so a view must decide there whether input is allowed.

Root Cause Analysis

While the bottom sheet was animating between states, its position on screen was changing under the user’s finger, yet the input pipeline still forwarded touches to mGestureDetector and, through it, to whatever content the sheet was displaying. The invariant that “a control should not accept an activating tap while it is sliding into a new position” was not enforced in BottomSheet.onInterceptTouchEvent or BottomSheet.onTouchEvent; the pre-patch gating logic in TouchRestrictingFrameLayout only suppressed input in coarse cases and did not cover the SheetState.SCROLLING animation window.

As a result, a tap aimed at one part of the UI could land on a different, unexpected target (such as an install-confirmation affordance) once the animation shifted the layout — a classic tapjacking / input-timing race.

The fix adds an explicit early return in both onInterceptTouchEvent and onTouchEvent that swallows all touches whenever getSheetState() equals SheetState.SCROLLING, so no gesture reaches the detector or content while the sheet is in motion. Because the gating now lives directly in BottomSheet at the true dispatch boundary, the special-purpose TouchRestrictingFrameLayout is deleted and its holders are replaced with plain FrameLayout instances.

Key insight
The core mistake was gating input on coarse touch-enabled state while leaving the animation (SheetState.SCROLLING) window unguarded, allowing taps to be consumed against a surface whose position was still moving; the fix closes the window by returning early from both touch entry points whenever the sheet is scrolling.

Attack Path

  1. Trigger the sheet A malicious or misleading page/flow causes the WebApp install bottom sheet (or another bottom-sheet UI) to begin animating open or settling into a new SheetState.
  2. Enter the animation window During the transition the sheet reports SheetState.SCROLLING, and its content moves under the pointer while touch dispatch remains active.
  3. Land a mistimed tap The user taps where they believe a benign element sits, but the shifting layout places an actionable control (e.g. an install/confirm button) under the finger at the moment onTouchEvent consumes it.
  4. Confirm an unintended action The gesture is delivered to the now-repositioned target, completing an action the user did not intend, such as accepting a WebApp install prompt.

Impact Assessment

An attacker gains the ability to induce an unintended confirmation (inappropriate WebApp install or similar sheet action) in the Android browser UI context by exploiting the animation timing race, with no memory corruption. The precondition is that the user must interact with a bottom sheet while it is animating, so exploitation relies on UI timing and user misdirection rather than direct code control. The severity is limited to logic-level UI spoofing/tapjacking of the affected sheet-hosted confirmations.

Changed Functions

FunctionChangeNotes
TouchRestrictingFrameLayout
components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/TouchRestrictingFrameLayout.java
modified

Files Changed

  • chrome/android/expectations/lint-baseline.xml
  • components/browser_ui/bottomsheet/android/internal/BUILD.gn
  • components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheet.java
  • components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/TouchRestrictingFrameLayout.java
  • components/browser_ui/bottomsheet/android/java/res/layout/bottom_sheet.xml

Audit Directions

  • Animation-window input gating
    Audit other views and sheets that reposition via animators for touch handlers that consume input without checking whether the view is mid-transition (e.g. missing a SheetState.SCROLLING-equivalent guard).
  • Split gating responsibility
    Look for input suppression logic that lives in a wrapper/FrameLayout rather than at the true onInterceptTouchEvent/onTouchEvent dispatch boundary, since indirection tends to leave states (like animation) unhandled.
  • State-machine coverage
    Review touch/input decisions that enumerate only some values of a UI state enum, checking that every transient state (settling, hiding, scrolling) is explicitly accounted for before input is accepted.
From 83df5710daca3a1fbdde22c0b9e5fde6cf8db81c Mon Sep 17 00:00:00 2001
From: Lijin Shen <[email protected]>
Date: Thu, 19 Dec 2024 13:16:42 -0800
Subject: [PATCH] [clank-q4-fixit] Disable touch when bottom sheet is animating

Bug: 351564774
Change-Id: I7f7b94f3b82945361bff8703d3e6fc5aa2cf9234
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6102411
Reviewed-by: Matthew Jones <[email protected]>
Commit-Queue: Lijin Shen <[email protected]>
Code-Coverage: [email protected] <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1398821}
---

diff --git a/chrome/android/expectations/lint-baseline.xml b/chrome/android/expectations/lint-baseline.xml
index a71f1c6..4c67eb1 100644
--- a/chrome/android/expectations/lint-baseline.xml
+++ b/chrome/android/expectations/lint-baseline.xml
@@ -2499,17 +2499,6 @@
     </issue>
 
     <issue
-        id="ClickableViewAccessibility"
-        message="Custom view `TouchRestrictingFrameLayout` overrides `onTouchEvent` but not `performClick`"
-        errorLine1="    public boolean onTouchEvent(MotionEvent event) {"
-        errorLine2="                   ~~~~~~~~~~~~">
-        <location
-            file="../../components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/TouchRestrictingFrameLayout.java"
-            line="42"
-            column="20"/>
-    </issue>
-
-    <issue
         id="AccessibilityWindowStateChangedEvent"
         message="Manually populating or sending TYPE_WINDOW_STATE_CHANGED events should be avoided. They may be ignored on certain versions of Android. Prefer setting UI metadata using `View.onInitializeAccessibilityNodeInfo`, `Activity.setTitle`, `ViewCompat.setAccessibilityPaneTitle`, etc. to inform users of crucial changes to the UI."
         errorLine1="            mListView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);"
diff --git a/components/browser_ui/bottomsheet/android/internal/BUILD.gn b/components/browser_ui/bottomsheet/android/internal/BUILD.gn
index 0b68022..35122f5 100644
--- a/components/browser_ui/bottomsheet/android/internal/BUILD.gn
+++ b/components/browser_ui/bottomsheet/android/internal/BUILD.gn
@@ -16,7 +16,6 @@
     "java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheetControllerFactory.java",
     "java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheetControllerImpl.java",
     "java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheetSwipeDetector.java",
-    "java/src/org/chromium/components/browser_ui/bottomsheet/TouchRestrictingFrameLayout.java",
   ]
 
   deps = [
diff --git a/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheet.java b/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheet.java
index 1c02a9a..00f5ee7 100644
--- a/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheet.java
+++ b/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/BottomSheet.java
@@ -130,7 +130,7 @@
     @Nullable protected BottomSheetContent mSheetContent;
 
     /** A handle to the FrameLayout that holds the content of the bottom sheet. */
-    private TouchRestrictingFrameLayout mBottomSheetContentContainer;
+    private FrameLayout mBottomSheetContentContainer;
 
     /**
      * The last offset ratio sent to observers of onSheetOffsetChanged(). This is used to ensure the
@@ -139,7 +139,7 @@
     private float mLastOffsetRatioSent;
 
     /** The FrameLayout used to hold the bottom sheet toolbar. */
-    private TouchRestrictingFrameLayout mToolbarHolder;
+    private FrameLayout mToolbarHolder;
 
     /** Whether the {@link BottomSheet} and its children should react to touch events. */
     private boolean mIsTouchEnabled;
@@ -247,7 +247,9 @@
         mSettleAnimator = null;
     }
 
-    /** @return Whether the sheet is in the process of hiding. */
+    /**
+     * @return Whether the sheet is in the process of hiding.
+     */
     boolean isHiding() {
         return mSettleAnimator != null && mTargetState == SheetState.HIDDEN;
     }
@@ -264,6 +266,9 @@
 
         if (isHiding()) return false;
 
+        // No interaction when sheet is animating.
+        if (getSheetState() == SheetState.SCROLLING) return true;
+
         return mGestureDetector.onInterceptTouchEvent(e);
     }
 
@@ -277,6 +282,9 @@
         // anything with them.
         if (!mIsTouchEnabled) return true;
 
+        // No interaction when sheet is animating.
+        if (getSheetState() == SheetState.SCROLLING) return true;
+
         mGestureDetector.onTouchEvent(e);
 
         return true;
@@ -314,12 +322,9 @@
         onAppHeaderHeightChanged(appHeaderHeight);
         setBottomMargin(bottomMargin);
 
-        mToolbarHolder =
-                (TouchRestrictingFrameLayout) findViewById(R.id.bottom_sheet_toolbar_container);
+        mToolbarHolder = (FrameLayout) findViewById(R.id.bottom_sheet_toolbar_container);
 
-        mBottomSheetContentContainer =
-                (TouchRestrictingFrameLayout) findViewById(R.id.bottom_sheet_content);
-        mBottomSheetContentContainer.setBottomSheet(this);
+        mBottomSheetContentContainer = (FrameLayout) findViewById(R.id.bottom_sheet_content);
 
         mContainerWidth = mSheetContainer.getWidth();
         mContainerHeight = mSheetContainer.getHeight();
diff --git a/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/TouchRestrictingFrameLayout.java b/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/TouchRestrictingFrameLayout.java
deleted file mode 100644
index bf114fe9..0000000
--- a/components/browser_ui/bottomsheet/android/internal/java/src/org/chromium/components/browser_ui/bottomsheet/TouchRestrictingFrameLayout.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2018 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.components.browser_ui.bottomsheet;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.widget.FrameLayout;
-
-/**
- * A specialized FrameLayout that is capable of ignoring all user input based on the state of
- * the bottom sheet.
- */
-class TouchRestrictingFrameLayout extends FrameLayout {
-    /** A handle to the bottom sheet. */
-    private BottomSheet mBottomSheet;
-
-    public TouchRestrictingFrameLayout(Context context, AttributeSet atts) {
-        super(context, atts);
-    }
-
-    /** @param sheet The bottom sheet. */
-    public void setBottomSheet(BottomSheet sheet) {
-        mBottomSheet = sheet;
-    }
-
-    /** @return Whether touch is enabled. */
-    private boolean isTouchDisabled() {
-        return mBottomSheet == null
-                || mBottomSheet.getSheetState() == BottomSheetController.SheetState.SCROLLING;
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent event) {
-        if (isTouchDisabled()) return false;
-        return super.onInterceptTouchEvent(event);
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        if (isTouchDisabled()) return false;
-        return super.onTouchEvent(event);
-    }
-}
diff --git a/components/browser_ui/bottomsheet/android/java/res/layout/bottom_sheet.xml b/components/browser_ui/bottomsheet/android/java/res/layout/bottom_sheet.xml
index b10b6ce4..04ce93e 100644
--- a/components/browser_ui/bottomsheet/android/java/res/layout/bottom_sheet.xml
+++ b/components/browser_ui/bottomsheet/android/java/res/layout/bottom_sheet.xml
@@ -6,7 +6,6 @@
 -->
 <org.chromium.components.browser_ui.bottomsheet.BottomSheet
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/bottom_sheet"
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
@@ -25,7 +24,7 @@
         android:layout_height="match_parent"
         android:background="@drawable/bottom_sheet_background" />
 
-    <org.chromium.components.browser_ui.bottomsheet.TouchRestrictingFrameLayout
+    <FrameLayout
         android:importantForAccessibility="yes"
         android:id="@+id/bottom_sheet_content"
         android:layout_width="match_parent"
@@ -38,8 +37,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content" >
 
-        <view
-            class="org.chromium.components.browser_ui.bottomsheet.TouchRestrictingFrameLayout"
+        <FrameLayout
             android:id="@+id/bottom_sheet_toolbar_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content" />
Loading diff…

Original Bug Report

reported by [email protected]

The PWA's installation dialog isn't being dismissed after redirects, which allows an attacker to sho

Steps to reproduce the problem

go to mrnoob790.github.io/index.html click on add homescreen and then fastly click on install app u will installation popup come in screen and website is redirect to google.com

Problem Description

After the PWA’s installation dialog is opened by the user, it is possible to redirect the attacker’s page to another website, and given the dialog isn’t being dismissed, it will show over cross-origin websites

Summary

The PWA’s installation dialog isn’t being dismissed after redirects, which allows an attacker to sho

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A

View on issue tracker