CVE-2026-87486
Overview
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashController.javachrome/android/junit/BUILD.gnchrome/android/junit/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashControllerTest.java
Patch
From 6233a9f1b482fdfc5ac777ba8a23f04eb6af6f4c Mon Sep 17 00:00:00 2001
From: Vincent Scheib <scheib@chromium.org>
Date: Mon, 27 Jul 2026 15:11:29 -0700
Subject: [PATCH] Clamp splash screen fade-out duration in TWA
Ensure splash screen hide animation duration is clamped between 0 and 1000 ms to prevent UI spoofing.
TAG=agy
CONV=16f1091a-fe0a-4abe-8845-20bc2e3f8828
Bug: 514017067
Change-Id: Ic1c22e3e1719ffc4091312dbfbaff213ae3fa235
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8141662
Reviewed-by: Peter Conn <peconn@chromium.org>
Commit-Queue: Vincent Scheib <scheib@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1669027}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashController.java b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashController.java
index a794ca1..b4ae683 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashController.java
@@ -85,7 +85,9 @@
assertNonNull(getSplashScreenParamsFromIntent()),
SplashScreenParamKey.KEY_FADE_OUT_DURATION_MS,
0);
- mSplashController.setConfigAndShowSplash(this, splashHideAnimationDurationMs);
+ long clampedHideAnimationDurationMs =
+ org.chromium.base.MathUtils.clamp(splashHideAnimationDurationMs, 0, 1000);
+ mSplashController.setConfigAndShowSplash(this, clampedHideAnimationDurationMs);
}
@Override
diff --git a/chrome/android/junit/BUILD.gn b/chrome/android/junit/BUILD.gn
index 39a6a322..eb94731 100644
--- a/chrome/android/junit/BUILD.gn
+++ b/chrome/android/junit/BUILD.gn
@@ -610,6 +610,7 @@
"src/org/chromium/chrome/browser/browserservices/ui/controller/trustedwebactivity/TrustedWebActivityOpenTimeRecorderTest.java",
"src/org/chromium/chrome/browser/browserservices/ui/controller/trustedwebactivity/TwaVerifierTest.java",
"src/org/chromium/chrome/browser/browserservices/ui/controller/webapps/WebappDisclosureControllerTest.java",
+ "src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashControllerTest.java",
"src/org/chromium/chrome/browser/browserservices/ui/trustedwebactivity/DisclosureAcceptanceBroadcastReceiverTest.java",
"src/org/chromium/chrome/browser/browserservices/ui/trustedwebactivity/DisclosureUiPickerTest.java",
"src/org/chromium/chrome/browser/browserservices/ui/view/DisclosureNotificationTest.java",
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashControllerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashControllerTest.java
new file mode 100644
index 0000000..507a6c6
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashControllerTest.java
@@ -0,0 +1,105 @@
+// Copyright 2026 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.chrome.browser.browserservices.ui.splashscreen.trustedwebactivity;
+
+import static androidx.browser.trusted.TrustedWebActivityIntentBuilder.EXTRA_SPLASH_SCREEN_PARAMS;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.browser.trusted.splashscreens.SplashScreenParamKey;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.base.test.util.Feature;
+import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
+import org.chromium.chrome.browser.browserservices.ui.splashscreen.SplashController;
+
+import java.util.function.Supplier;
+
+/** Tests for {@link TwaSplashController}. */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class TwaSplashControllerTest {
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock public Activity mActivity;
+ @Mock public SplashController mSplashController;
+ @Mock public Supplier<SplashController> mSplashControllerSupplier;
+ @Mock public BrowserServicesIntentDataProvider mIntentDataProvider;
+
+ private Intent mIntent;
+ private Bundle mSplashParams;
+
+ @Captor public ArgumentCaptor<Long> mDurationCaptor;
+
+ @Before
+ public void setUp() {
+ doReturn(mSplashController).when(mSplashControllerSupplier).get();
+
+ mIntent = new Intent();
+ mSplashParams = new Bundle();
+ mIntent.putExtra(EXTRA_SPLASH_SCREEN_PARAMS, mSplashParams);
+ doReturn(mIntent).when(mIntentDataProvider).getIntent();
+ }
+
+ private void createController() {
+ new TwaSplashController(
+ mActivity,
+ mSplashControllerSupplier,
+ mIntentDataProvider);
+ }
+
+ @Test
+ @Feature({"TrustedWebActivities"})
+ public void testDefaultDuration() {
+ createController();
+ verify(mSplashController).setConfigAndShowSplash(any(), mDurationCaptor.capture());
+ assertEquals(0L, mDurationCaptor.getValue().longValue());
+ }
+
+ @Test
+ @Feature({"TrustedWebActivities"})
+ public void testDurationWithinRange() {
+ mSplashParams.putInt(SplashScreenParamKey.KEY_FADE_OUT_DURATION_MS, 500);
+ createController();
+ verify(mSplashController).setConfigAndShowSplash(any(), mDurationCaptor.capture());
+ assertEquals(500L, mDurationCaptor.getValue().longValue());
+ }
+
+ @Test
+ @Feature({"TrustedWebActivities"})
+ public void testDurationTooLarge() {
+ mSplashParams.putInt(SplashScreenParamKey.KEY_FADE_OUT_DURATION_MS, 5000);
+ createController();
+ verify(mSplashController).setConfigAndShowSplash(any(), mDurationCaptor.capture());
+ assertEquals(1000L, mDurationCaptor.getValue().longValue());
+ }
+
+ @Test
+ @Feature({"TrustedWebActivities"})
+ public void testDurationNegative() {
+ mSplashParams.putInt(SplashScreenParamKey.KEY_FADE_OUT_DURATION_MS, -500);
+ createController();
+ verify(mSplashController).setConfigAndShowSplash(any(), mDurationCaptor.capture());
+ assertEquals(0L, mDurationCaptor.getValue().longValue());
+ }
+}
Original Bug Report
Potential UI spoofing in TWAs via unclamped splash screen fade-out duration
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: A missing validation of the splash screen animation duration parameter in Trusted Web Activities allows a malicious app to overlay a persistent fake UI on top of Chrome. By providing an extremely large duration, an attacker can keep a spoofed address bar visible while interactions are passed to a malicious website.
Affected files:
chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashController.javachrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/SplashController.java
Estimated timestamp from git blame: 2019-04-07
Summary
A potential vulnerability in Chrome for Android’s Trusted Web Activity (TWA) implementation allows a malicious local application to overlay a static bitmap over Chrome’s UI for an extended duration. This enables address bar spoofing and tapjacking by exploiting an unclamped animation duration parameter.
Root Cause Analysis
In TwaSplashController.java, the splash screen fade-out duration is retrieved directly from an attacker-supplied Intent bundle via SplashScreenParamKey.KEY_FADE_OUT_DURATION_MS. This value is read using IntentUtils.safeGetInt() but is never validated or clamped to a reasonable maximum.
// chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashController.java
long splashHideAnimationDurationMs =
IntentUtils.safeGetInt(
assertNonNull(getSplashScreenParamsFromIntent()),
SplashScreenParamKey.KEY_FADE_OUT_DURATION_MS,
0);
mSplashController.setConfigAndShowSplash(this, splashHideAnimationDurationMs);
This value is subsequently passed to ViewPropertyAnimator.setDuration() in SplashController.java. If an attacker provides a value like Integer.MAX_VALUE (approximately 24.8 days), the splash screen—which is a MATCH_PARENT ImageView containing an attacker-provided bitmap—will remain visible and nearly opaque for the duration of the user’s session.
Vulnerability Mechanics
- Occlusion: The splash screen
ImageViewis added toandroid.R.id.contentand re-added as the last child during startup. This positioning allows it to occlude the Chrome toolbar (address bar) and the web content area. - Touch Pass-Through: By default, the
ImageViewused for the splash screen is non-clickable. In Android’s view dispatch logic, touch events that are not consumed by the top-level view fall through to the underlying views. Consequently, touches are routed to the attacker-controlled web content beneath the splash screen. - Persistence: Digital Asset Link (DAL) verification failure (which normally triggers a UI downgrade to standard Custom Tab mode with a visible toolbar) does not remove the splash screen. The overlay persists because it is rendered at a higher Z-order than the toolbar, and no logic in Chrome explicitly removes it upon verification failure.
Potential Reproduction Steps
Note: These steps are based on code analysis and have not been verified with a live proof of concept.
- From a malicious Android app, create a
CustomTabsServicesession. - Provide a splash image via
session.receiveFilethat depicts a spoofed Chrome UI and address bar (e.g., mimickinghttps://accounts.google.com). - Launch a TWA intent targeting an attacker-controlled origin, setting
KEY_FADE_OUT_DURATION_MStoInteger.MAX_VALUEin theEXTRA_SPLASH_SCREEN_PARAMSbundle. - Verify that the spoofed bitmap covers the entire view, including the real toolbar, while interactions are delivered to the underlying malicious site.
Suggested Fix
Apply a reasonable upper bound to the splash screen fade-out duration in TwaSplashController.java (e.g., clamp the value to 1000ms).
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.