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

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Custom Tabs
DescriptionInappropriate implementation in Custom Tabs
ComponentCustom Tabs
Sub-componentUI>Browser>Mobile
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.
Tracker376491759
Fix commita9a7be5f5c0a (chromium/src) +29/-0
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedPhilipp Beer (TU Wien)
Reported2024-10-31 · bounty $10000 (per vendor advisory)
Fix landed2025-02-11 (public git)
Disclosed2025-04-01
Patch gap49 days — fix public in source before the advisory

Background

Custom Tab (CCT)
A browser Activity that an Android app can launch to show web content in-app; the loaded page is web-controlled content rendered on top of the host app’s task.
Enter animation
The window transition that plays as the CCT Activity opens; while it runs the Activity can appear transparent or not-yet-visible even though it is already live and dispatching input.
`dispatchTouchEvent`
The Activity entry point that receives every MotionEvent before it is routed to views/web content; returning true consumes the event so it is not delivered.
`onEnterAnimationComplete`
The Android Activity callback fired once the enter animation finishes, used here to mark the point after which touches are safe to deliver.

Root Cause Analysis

Before the fix, CustomTabActivity.dispatchTouchEvent delivered touch events immediately, gated only by the unrelated sPreventTouches/shouldPreventTouch(ev) overlay check, so events that arrived while the enter animation was still running were routed straight to the web content. Because an enter animation can make the Activity “appear” transparent for a noticeable interval, a user could see and touch UI beneath the CCT (or the page could be positioned so a tap lands on an element the user did not intend to interact with) while the CCT is effectively invisible — a tap-through / input-timing weakness. The invariant that should hold is that no user touch reaches the web page until the CCT is actually presented, which the old code did not enforce.

The fix adds a boolean mIsEnterAnimationCompleted, set to true only in the newly overridden onEnterAnimationComplete, and makes dispatchTouchEvent return true (consuming and discarding the event) whenever the sBlockTouchesDuringEnterAnimation feature is enabled and the animation has not yet completed. This guard runs first, before the existing overlay check, so all touches during the vulnerable window are swallowed and only post-animation touches proceed.

Key insight
The core mistake was dispatching touch events to web content during the enter animation, when the CCT can appear invisible/transparent; the fix consumes every touch until onEnterAnimationComplete sets mIsEnterAnimationCompleted.

Attack Path

  1. Launch a CCT with a slow/transparent enter animation A malicious or cooperating app opens a Custom Tab such that the enter animation leaves the Activity appearing transparent or not visibly present for an extended interval.
  2. Load attacker-controlled content underneath During that window the CCT loads a web page whose interactive elements are positioned where the user is likely to tap.
  3. Induce a tap in the invisible window The user (believing they are interacting with the underlying app/screen) touches the display while the CCT is still animating in.
  4. Touch reaches web content Pre-patch, dispatchTouchEvent forwards the MotionEvent to the invisible CCT’s page, causing an unintended interaction with attacker-influenced UI (e.g. a click on a page element). The concrete consequence of that click beyond an unintended tap is (inferred).

Impact Assessment

An attacker gains the ability to have a user’s touch land on web content inside a Custom Tab while that tab is visually not yet presented, on Android in the browser/app UI context, requiring that a CCT be launched and that the user touch the screen during the enter animation. The advisory describes this as an inappropriate implementation (logic error) in Custom Tabs and does not report exploitation in the wild; any escalation beyond an unintended UI interaction is (inferred). The fix is gated behind the CCTBlockTouchesDuringEnterAnimation feature, enabled by default.

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java
  • chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsFilterTouchUnitTest.java
  • chrome/browser/flags/android/chrome_feature_list.cc
  • chrome/browser/flags/android/chrome_feature_list.h
  • chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java

Audit Directions

  • Input delivered before presentation
    Audit Activity/window input entry points (dispatchTouchEvent, onTouchEvent) that route events to web or sensitive content without first confirming the surface is fully visible and the enter/transition animation has completed.
  • Transparent or animating overlays
    Review UI surfaces that can render transparent or partially off-screen during transitions and still accept clicks, a classic tap-through / clickjacking vector on both Android and desktop.
  • State flags set by lifecycle callbacks
    Check that guards depending on lifecycle signals (animation-complete, visible, resumed) default to the safe/blocked state and are only relaxed once the corresponding callback (onEnterAnimationComplete) has actually fired.
From a9a7be5f5c0a41b054d18e1e95c26ba1ab15f0b4 Mon Sep 17 00:00:00 2001
From: Sinan Sahin <sinansahin@google.com>
Date: Mon, 10 Feb 2025 17:34:45 -0800
Subject: [PATCH] Block touches in CCT until enter animation is completed

Bug: 376491759
Change-Id: I989b1bcb3aee842a0080e01603c5717771368cab
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6244912
Reviewed-by: Jinsuk Kim <jinsukkim@chromium.org>
Commit-Queue: Sinan Sahin <sinansahin@google.com>
Cr-Commit-Position: refs/heads/main@{#1418416}
---

diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java
index 5dc71c9..19e3714 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java
@@ -85,6 +85,10 @@
      */
     private MotionEvent mBlockedEvent;
 
+    private static final boolean sBlockTouchesDuringEnterAnimation =
+            ChromeFeatureList.sCctBlockTouchesDuringEnterAnimation.isEnabled();
+    private boolean mIsEnterAnimationCompleted;
+
     private CustomTabActivityTabProvider.Observer mTabChangeObserver =
             new CustomTabActivityTabProvider.Observer() {
                 @Override
@@ -315,6 +319,12 @@
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
+        // We should block touches while the enter animation is still running. An enter animation
+        // that makes the Activity "appear" transparent for a long time may lead users to touch
+        // elements on the webpage that's loaded within a currently invisible CCT.
+        if (sBlockTouchesDuringEnterAnimation && !mIsEnterAnimationCompleted) {
+            return true;
+        }
         if (sPreventTouches && shouldPreventTouch(ev)) {
             // Discard the events which may be trickling down from an overlay activity above.
             return true;
@@ -451,4 +461,11 @@
             getCustomTabActivityTabProvider().getTab().loadUrl(params);
         }
     }
+
+    @Override
+    public void onEnterAnimationComplete() {
+        super.onEnterAnimationComplete();
+
+        mIsEnterAnimationCompleted = true;
+    }
 }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsFilterTouchUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsFilterTouchUnitTest.java
index 54f7bfb88..f98cdd47 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsFilterTouchUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsFilterTouchUnitTest.java
@@ -67,6 +67,7 @@
     @Test
     @SmallTest
     public void testInjectMissingEventInMultiWindowMode() {
+        mActivity.onEnterAnimationComplete();
         ApplicationStatus.onStateChangeForTesting(mActivity, ActivityState.PAUSED);
         assertTrue("Events should be consumed", mActivity.dispatchTouchEvent(mMotionEvent));
 
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
index 0cab131d..6658f08 100644
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -213,6 +213,7 @@
     &kCCTAuthTabDisableAllExternalIntents,
     &kCCTAuthTabEnableHttpsRedirects,
     &kCCTBeforeUnload,
+    &kCCTBlockTouchesDuringEnterAnimation,
     &kCCTClientDataHeader,
     &kCCTEarlyNav,
     &kCCTExtendTrustedCdnPublisher,
@@ -611,6 +612,10 @@
              "CCTBeforeUnload",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+BASE_FEATURE(kCCTBlockTouchesDuringEnterAnimation,
+             "CCTBlockTouchesDuringEnterAnimation",
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 BASE_FEATURE(kCCTClientDataHeader,
              "CCTClientDataHeader",
              base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/chrome/browser/flags/android/chrome_feature_list.h b/chrome/browser/flags/android/chrome_feature_list.h
index 0724e645..69bcf76 100644
--- a/chrome/browser/flags/android/chrome_feature_list.h
+++ b/chrome/browser/flags/android/chrome_feature_list.h
@@ -55,6 +55,7 @@
 BASE_DECLARE_FEATURE(kCCTAuthTabDisableAllExternalIntents);
 BASE_DECLARE_FEATURE(kCCTAuthTabEnableHttpsRedirects);
 BASE_DECLARE_FEATURE(kCCTBeforeUnload);
+BASE_DECLARE_FEATURE(kCCTBlockTouchesDuringEnterAnimation);
 BASE_DECLARE_FEATURE(kCCTClientDataHeader);
 BASE_DECLARE_FEATURE(kCCTEarlyNav);
 BASE_DECLARE_FEATURE(kCCTEphemeralMediaViewerExperiment);
diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
index ca7bd0f..bd0b5bd 100644
--- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
+++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
@@ -249,6 +249,8 @@
             "CCTAuthTabEnableHttpsRedirects";
     public static final String CCT_AUTO_TRANSLATE = "CCTAutoTranslate";
     public static final String CCT_BEFORE_UNLOAD = "CCTBeforeUnload";
+    public static final String CCT_BLOCK_TOUCHES_DURING_ENTER_ANIMATION =
+            "CCTBlockTouchesDuringEnterAnimation";
     public static final String CCT_CLIENT_DATA_HEADER = "CCTClientDataHeader";
     public static final String CCT_EARLY_NAV = "CCTEarlyNav";
     public static final String CCT_EPHEMERAL_MEDIA_VIEWER_EXPERIMENT =
@@ -627,6 +629,8 @@
     public static final CachedFlag sCctAuthTabEnableHttpsRedirects =
             newCachedFlag(CCT_AUTH_TAB_ENABLE_HTTPS_REDIRECTS, true);
     public static final CachedFlag sCctAutoTranslate = newCachedFlag(CCT_AUTO_TRANSLATE, true);
+    public static final CachedFlag sCctBlockTouchesDuringEnterAnimation =
+            newCachedFlag(CCT_BLOCK_TOUCHES_DURING_ENTER_ANIMATION, true);
     public static final CachedFlag sCCTEphemeralMediaViewerExperiment =
             newCachedFlag(
                     CCT_EPHEMERAL_MEDIA_VIEWER_EXPERIMENT,
@@ -856,6 +860,7 @@
                     sCctAuthTabDisableAllExternalIntents,
                     sCctAuthTabEnableHttpsRedirects,
                     sCctAutoTranslate,
+                    sCctBlockTouchesDuringEnterAnimation,
                     sCCTEphemeralMediaViewerExperiment,
                     sCctEphemeralMode,
                     sCctFreInSameTask,
Loading diff…

Original Bug Report

reported by ph...@gmail.com

Tapjacking on Custom Tabs using animations

VULNERABILITY DETAILS

Short Summary

An application without any permissions can exploit animations and launch a Chrome Custom Tab (CCT) to bypass permission checks on websites and perform clickjacking attacks against arbitrary websites.

Primitives

Android applications can use Chrome Custom Tabs (CCT) to open web content with minimal context switching, as Custom Tab Activities are launched within the same Task as the requesting application. Through the ActivityOptions.makeCustomAnimation method, Android also allows applications to specify custom enter and exit animations for cross-activity and same-task transitions, such as the transition between an app and a CCT.

An application installed on the user’s device can exploit this functionality by launching a Custom Tab and setting an enter animation with a long fade-in duration, set to low opacity. This transition brings the Custom Tab to the foreground yet keeps it invisible to the user while the animation completes since the CCT is basically transparent (e.g., opacity=0.0005). As the CCT Activity is on top of the stack, and Chrome does not wait for animations to be finished before handling touches, screen interactions are handled by the browser.

The following minimum working example demonstrates this exploit, assuming Chrome is the default browser:

MainActivity.kt

val builder = CustomTabsIntent.Builder()
// setStartAnimation internally uses ActivityOptions#makeCustomAnimation
builder.setStartAnimations(this, R.anim.fade_in, R.anim.fade_out) 
val customTabsIntent = builder.build()
customTabsIntent.launchUrl(this, victimUrl)

res/anim/fade_in.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- Long-running animation with low opacity -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="0.0"
    android:toAlpha="0.0005"
    android:duration="20000"/>

res/anim/fade_out.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- This boils down to no animation, as duration = 0 -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="1.0"
    android:toAlpha="1.0"
    android:duration="0" />

Chrome is not vulnerable when opening a website via a non-CCT Intent, as the FLAG_ACTIVITY_NEW_TASK flag is set on the Intent when launched (see relevant Chromium source code). This flag causes Chrome to start in a new task, which prevents the use of custom animations.

This report is part of a coordinated responsible disclosure on the underlying animation-based tapjacking vulnerability we discovered on Android.

Security Implications

The vulnerability opens doors for the following Chrome/Web-specific issues:

  1. Permission Bypass

By launching a CCT with this technique, an application can open an attacker-controlled website that requests sensitive permissions, such as microphone or camera access. By luring the user to tap a button that is positioned at the same location as the allow button in the permission prompt, the permission is granted to the website without user awareness. Since CCT permissions are shared with Chrome, any permissions granted in this way go beyond the CCT, granting persistence to the attacker in the victim’s browser. This vulnerability also allows attackers to install PWAs on the user’s device.

  1. Web Clickjacking Attacks

An app can secretly request a website, i.e., a checkout page of an online store, and lure the user into performing critical actions, such as clicking the ‘buy’ button. Existing standard web-based clickjacking prevention, such as the Content Security Policy frame-ancestors directive and the X-Frame-Options headers, do not apply here and do not mitigate these attacks. Note also that SameSite Lax cookies are attached to CCT-initiated navigations.

The implications of this clickjacking attack differ significantly from clickjacking in Android WebViews. Unlike WebViews, which do not share state with the browser, Custom Tabs do. That means that sessions established by the user on any websites in Chrome are available in the CCT.

Existing and Proposed Mitigations

Existing efforts made by Chrome to mitigate tapjacking vulnerabilities on Android, such as using View.setFilterTouchesWhenObscured and checks for the FLAG_WINDOW_IS_PARTIALLY_OBSCURED flag in modals (see relevant source code here) only mitigate attacks from malicious overlays on top of Chrome activities, not animations. Similarly, system level-protections built into Android 12+ only target overlays.

The source of this vulnerability is twofold: (1) Chrome does not wait for animations to be finished until it handles touches, and (2) Android allows long-running animations with low opacity.

To offer mitigations to protect users until (2) is fixed on the system level, we propose to also fix (1) and mitigate this issue at the browser level. Chrome can ignore all touch events until the onEnterAnimationComplete lifecycle method is called. This method is triggered once the enter animations are complete, thereby protecting against animation-based tapjacking.

Practicality of the Attack

A slow fade-in transition would reveal the Chrome window after a few seconds, but an app can re-launch its own activity before the animation ends to effectively hide the Custom Tab. According to our experiments, a delay of 2500ms between starting the CCT and re-launching the app’s activity is effective. This means that the permission prompt is clickable for 2.5 seconds. Although this is a relatively short time frame for user interaction, attack scenarios like games, where users are prompted to tap quickly, make this attack practical. An app can further optimize load times by pre-warming the browser with the warmup method and pre-fetching the target page using mayLaunchUrl.

VERSION

Chrome Version: 130.0.6723.73 stable Operating System: Android 15, Pixel 6a

REPRODUCTION CASE

The Proof of Concept application and the attached video demonstrate bypassing the Chrome permission prompt via tapjacking (source code can be found here). Notice that the button positioning is manually adjusted for a Pixel 6a and might require some tweaking to generalize to different devices. The application opens https://webrtc.github.io/test-pages/src/iframe-video/, which immediately requests camera permission on load.

To replicate the permission tapjacking, follow these steps:

  • Grant Chrome permission to access the user’s camera
  • Install the PoC application (source code and APK available here)
  • Click ‘Choose browser’ and choose Chrome
  • Click ‘open com.android.chrome’
  • Click on the red button when it appears
  • The site is now allowed to access the camera

CREDIT INFORMATION

Reporter credit: Philipp Beer, TU Wien (@beerphilipp)

View on issue tracker
Uploaded PoC files
Links in the report