CVE-2025-3067
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.javachrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsFilterTouchUnitTest.javachrome/browser/flags/android/chrome_feature_list.ccchrome/browser/flags/android/chrome_feature_list.hchrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
Patch
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,
Original Bug Report
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:
- 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.
- 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)
- http://com.android.chrome
- http://schemas.android.com/apk/res/android
- https://developer.android.com/privacy-and-security/risks/tapjacking
- https://gitfront.io/r/philippbeer/PZo34NhDiR1c/tapjacking-browser-poc/
- https://source.chromium.org/chromium/chromium/src/ /main:chrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.java;drc=7804fc4cc9837d70a6f52f7617612884cac261a5;bpv=1;bpt=1;l=497?gsn=setFlags
- https://source.chromium.org/chromium/chromium/src/ /main:components/browser_ui/modaldialog/android/java/src/org/chromium/components/browser_ui/modaldialog/ModalDialogView.java?q=symbol:\borg.chromium.components.browser_ui.modaldialog.ModalDialogView.setupFilterTouchForView\b case:yes
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.java;drc=7804fc4cc9837d70a6f52f7617612884cac261a5;bpv=1;bpt=1;l=497?gsn=setFlags&gs=KYTHE%3A%2F%2Fkythe%3A%2F%2Fchromium.googlesource.com%2Fcodesearch%2Fchromium%2Fsrc%2F%2Fmain%3Flang%3Djava%3Fpath%3Dandroid.content.Intent%23dad7272f323513f01d4f2511a91d49e1f9ea1cfd40fd26ca2b36d7b2181493bb
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/android/java/src/org/chromium/chrome/browser/LaunchIntentDispatcher.java;drc=7804fc4cc9837d70a6f52f7617612884cac261a5;bpv=1;bpt=1;l=497?gsn=setFlags&gs=KYTHE://kythe://chromium.googlesource.com/codesearch/chromium/src//main?lang=java?path=android.content.Intent#dad7272f323513f01d4f2511a91d49e1f9ea1cfd40fd26ca2b36d7b2181493bb
- https://source.chromium.org/chromium/chromium/src/+/main:components/browser_ui/modaldialog/android/java/src/org/chromium/components/browser_ui/modaldialog/ModalDialogView.java?q=symbol%3A%5Cborg.chromium.components.browser_ui.modaldialog.ModalDialogView.setupFilterTouchForView%5Cb%20case%3Ayes
- https://source.chromium.org/chromium/chromium/src/+/main:components/browser_ui/modaldialog/android/java/src/org/chromium/components/browser_ui/modaldialog/ModalDialogView.java?q=symbol:\borg.chromium.components.browser_ui.modaldialog.ModalDialogView.setupFilterTouchForView\b case:yes
- https://webrtc.github.io/test-pages/src/iframe-video/