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

Overview

Medium
Severity
4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactClickjacking in TrustedWebActivities
DescriptionClickjacking in TrustedWebActivities
ComponentTrustedWebActivities
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.
Tracker514017067
Fix commit6233a9f1b482 (chromium/src) +109/-1
CWECWE-1021
CVSS vectorCVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-07-27 (public git)
Disclosed2026-09-08
Patch gap43 days — fix public in source before the advisory

Background

Trusted Web Activity (TWA)
an Android mechanism that lets a verified web origin render full-screen inside a Chrome-backed activity without browser chrome, so its UI occupies the entire screen.
Splash screen
the placeholder image a TWA shows on launch, configured through intent extras and hidden with a fade-out animation whose duration is supplied by the app via SplashScreenParamKey.KEY_FADE_OUT_DURATION_MS.
`org.chromium.base.MathUtils.clamp`
a utility that constrains a value to an inclusive [min, max] range, used here to bound the fade-out duration.
Clickjacking / UI spoofing
tricking a user into acting on a UI surface that is not what it appears to be, e.g. an overlay that stays on screen long enough to intercept or misdirect input.

Root Cause Analysis

The TWA splash-screen setup read the fade-out animation duration directly from an intent extra (SplashScreenParamKey.KEY_FADE_OUT_DURATION_MS, defaulting to 0) into splashHideAnimationDurationMs and passed it unbounded to mSplashController.setConfigAndShowSplash(this, splashHideAnimationDurationMs). Because this value is fully attacker-controlled by the app launching the TWA and was never validated, an arbitrarily large duration could keep the splash screen (and its fade animation) on screen far longer than intended, and a negative value was also possible. The invariant that the splash overlay should disappear within a short, bounded interval was therefore violated.

The fix computes clampedHideAnimationDurationMs = org.chromium.base.MathUtils.clamp(splashHideAnimationDurationMs, 0, 1000) and passes the clamped value instead, forcing the duration into [0, 1000] ms. The accompanying tests confirm the intended behavior: default 0, an in-range 500 passes through, 5000 is capped to 1000, and -500 is raised to 0.

The patch only adds a bounds check; it does not change any other splash logic.

Key insight
The single mistake was trusting an app-supplied animation duration without bounding it, letting the splash overlay persist for an attacker-chosen time; the fix clamps that duration to [0, 1000] ms so the overlay can no longer be held on screen abnormally long (or driven negative).

Attack Path

  1. Craft a malicious TWA intent An attacker-controlled Android app launches a Trusted Web Activity and sets the splash-screen params bundle with SplashScreenParamKey.KEY_FADE_OUT_DURATION_MS to an abnormally large value (e.g. far above 1000).
  2. Reach the unclamped assignment On splash setup the value is read into splashHideAnimationDurationMs and, pre-patch, handed straight to mSplashController.setConfigAndShowSplash(this, splashHideAnimationDurationMs) with no bounds check.
  3. Hold a spoofed overlay on screen The oversized fade-out duration keeps the full-screen splash/fade animation visible longer than a legitimate transition, giving a window in which the displayed surface does not match the underlying interactive content. (inferred: the exact UI a victim is tricked into acting on is not shown by the diff.)
  4. Capture or misdirect user interaction During that window the user may act on the spoofed surface, achieving the clickjacking / UI-spoofing outcome named in the advisory. (inferred)

Impact Assessment

Per the advisory this is clickjacking / UI spoofing in Trusted Web Activities, a LogicError, exercisable by an app that can launch a TWA and control its splash-screen intent extras. The gain is the ability to keep a full-screen splash overlay displayed for an attacker-chosen duration, which can be used to spoof UI and misdirect user input on the device (client/browser UI context). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.0. The advisory does not state exploited-in-the-wild status or a severity rating here.

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashController.java
  • chrome/android/junit/BUILD.gn
  • chrome/android/junit/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/trustedwebactivity/TwaSplashControllerTest.java

Audit Directions

  • Unbounded intent/IPC-supplied timing and animation values
    Search for durations, timeouts, delays, or animation lengths read from Intent extras or Bundles (e.g. other SplashScreenParamKey fields or IntentUtils.safeGet* reads) that flow into UI/show/hide calls without a clamp.
  • Overlay and full-screen surface lifetimes
    Review code that controls how long splash screens, overlays, or full-screen surfaces stay visible, ensuring their display/fade durations are bounded to prevent UI-spoofing windows.
  • Missing range validation on external inputs
    Look for values passed straight from an untrusted app to a controller/setter (like setConfigAndShowSplash) where a MathUtils.clamp or explicit range check is absent, especially where negative or very large values are meaningful.
From 6233a9f1b482fdfc5ac777ba8a23f04eb6af6f4c Mon Sep 17 00:00:00 2001
From: Vincent Scheib <[email protected]>
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 <[email protected]>
Commit-Queue: Vincent Scheib <[email protected]>
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/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());
+    }
+}
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",
Loading diff…

Original Bug Report

reported by [email protected]

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.java
  • chrome/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

  1. Occlusion: The splash screen ImageView is added to android.R.id.content and re-added as the last child during startup. This positioning allows it to occlude the Chrome toolbar (address bar) and the web content area.
  2. Touch Pass-Through: By default, the ImageView used 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.
  3. 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.

  1. From a malicious Android app, create a CustomTabsService session.
  2. Provide a splash image via session.receiveFile that depicts a spoofed Chrome UI and address bar (e.g., mimicking https://accounts.google.com).
  3. Launch a TWA intent targeting an attacker-controlled origin, setting KEY_FADE_OUT_DURATION_MS to Integer.MAX_VALUE in the EXTRA_SPLASH_SCREEN_PARAMS bundle.
  4. 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.

View on issue tracker
Links in the report