CVE-2026-79180
Overview
Background
- Custom Tabs (CCT) theming
- CCT lets the launching app pick a toolbar color, which Chrome applies as the toolbar primary color only while
Tab#isThemingAllowed()returns true. - `isThemingAllowed()`
- a per-
Tabpredicate that disallows custom theming when the connection state should be shown neutrally (e.g. degraded/insecure SSL state), forcing the default toolbar color. - `TabObserver` SSL callback
onSSLStateUpdated(Tab)fires when a tab’s visible security/SSL state changes without a full navigation, whereasonDidChangeThemeColoronly fires when the page’s own theme color changes.- `BrandedColorScheme` / `getThemeColor`
- the toolbar’s derived color scheme and color value that the theme providers recompute in
updateTheme()/updateColor().
Root Cause Analysis
BrowserServicesThemeColorProvider and TopUiThemeColorProvider derive the toolbar color from the activity/custom theme color only while isThemingAllowed() is true, and they recompute it from their TabObserver callbacks. Neither observer implemented onSSLStateUpdated, so when a tab’s visible SSL state changed without a navigation and the tab’s own theme color was already UNSPECIFIED (meaning onDidChangeThemeColor never fired), no callback recomputed the color. As a result the previously applied activity theme color and BrandedColorScheme were left in place even after isThemingAllowed() had become false, so the toolbar continued displaying the app-supplied brand color while other connection-state UI updated.
The fix adds onSSLStateUpdated overrides to both observers that call updateTheme() / updateColor(tab, tab.getThemeColor(), false), forcing the toolbar color and color scheme to be recomputed alongside the rest of the connection-state UI.
The added tests confirm the toolbar falls back to ChromeColors.getDefaultThemeColor(...) and BrandedColorScheme.APP_DEFAULT once theming is no longer allowed.
onSSLStateUpdated handlers makes the toolbar color track isThemingAllowed() at the moment the security state changes.Attack Path
- Launch a themed Custom Tab A malicious or attacker-influenced app opens a CCT with a chosen toolbar (brand) color that is applied while theming is allowed.
- Load a page with no theme color
The page leaves its own theme color
UNSPECIFIED, so the toolbar color is sourced from the activity/custom color andonDidChangeThemeColorwill not fire on later state changes. - Change SSL state without navigating
The visible security state degrades (e.g. an in-page connection-state transition), which fires
onSSLStateUpdatedbut, pre-patch, triggers no recomputation. - Stale trusted color persists
The toolbar keeps the brand color and
BrandedColorSchemeeven thoughisThemingAllowed()is now false, misrepresenting the connection’s security to the user (UI spoofing).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProvider.java |
modified |
Files Changed
chrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProvider.javachrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProviderUnitTest.javachrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProviderUnitTest.javachrome/browser/ui/android/theme/BUILD.gnchrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProvider.java
Audit Directions
- Missing observer callbacksAudit other
TabObserverimplementations that drive security-sensitive UI (toolbar, omnibox, security chip) for missingonSSLStateUpdatedhandling where state can change without a navigation or theme-color change. - Recompute triggered by wrong signalLook for UI derived from a predicate like
isThemingAllowed()that is only refreshed on a proxy event (navigation,onDidChangeThemeColor) rather than on every input to that predicate. - `UNSPECIFIED`/default-value shortcutsReview paths where an “unspecified” page value suppresses an update callback, leaving previously computed trusted UI state stale.
Patch
From d125c0dee27c0641133c990eb1ae3865564f5eac Mon Sep 17 00:00:00 2001
From: Mohamed Adel <adelm@google.com>
Date: Mon, 13 Jul 2026 14:20:04 -0700
Subject: [PATCH] [Android] Recompute toolbar theme on SSL state changes
BrowserServicesThemeColorProvider and TopUiThemeColorProvider derive the
toolbar primary color from the activity theme color when
Tab#isThemingAllowed() is true. Neither provider's tab observer reacted
to onSSLStateUpdated, so when the visible SSL state changed without a
navigation and the tab's own theme color was already UNSPECIFIED (so
onDidChangeThemeColor did not fire), the previous activity theme color
and BrandedColorScheme were left in place even though theming was no
longer allowed.
Add onSSLStateUpdated overrides to both tab observers so the toolbar
color and color scheme are recomputed alongside the other
connection-state UI when the SSL state changes.
TAG=agy
Bug: 517719358
Change-Id: Ia84ac465aaee46e36a8d7e64b14e094a78af498f
Fixed: 517719358
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8087875
Commit-Queue: Moe Adel <adelm@google.com>
Reviewed-by: Jinsuk Kim <jinsukkim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1661375}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProvider.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProvider.java
index 41b2200..a402dd6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProvider.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProvider.java
@@ -109,6 +109,11 @@
}
@Override
+ public void onSSLStateUpdated(Tab tab) {
+ updateTheme();
+ }
+
+ @Override
public void onDidChangeThemeColor(Tab tab, int color) {
updateTheme();
}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProviderUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProviderUnitTest.java
index 8a5095e2..71de7b0f 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProviderUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProviderUnitTest.java
@@ -340,6 +340,39 @@
}
@Test
+ public void testSSLStateUpdateRecomputesTheme() {
+ when(mCustomTabActivityTabProvider.get()).thenReturn(tab);
+ when(mToolbarThemeColorProvider.getToolbarBackgroundColor(eq(tab))).thenReturn(LIGHT_COLOR);
+ var intentDataProvider =
+ buildCctIntentDataProvider(
+ COLOR_SCHEME_LIGHT,
+ /* schemeParams= */ null,
+ /* isOpenedByChrome= */ false,
+ /* isIncognito= */ false);
+ var themeColorProvider = createThemeColorProvider(intentDataProvider);
+ themeColorProvider.setUseTabTheme(true);
+
+ assertEquals(
+ "Should use the page theme color before the SSL state changes",
+ LIGHT_COLOR,
+ themeColorProvider.getThemeColor());
+
+ int defaultColor = ChromeColors.getDefaultThemeColor(mContext, false);
+ when(mToolbarThemeColorProvider.getToolbarBackgroundColor(eq(tab)))
+ .thenReturn(defaultColor);
+ themeColorProvider.getTabObserver().onSSLStateUpdated(tab);
+
+ assertEquals(
+ "Should refresh the theme color when the SSL state changes",
+ defaultColor,
+ themeColorProvider.getThemeColor());
+ assertEquals(
+ "Should refresh the color scheme when the SSL state changes",
+ BrandedColorScheme.APP_DEFAULT,
+ themeColorProvider.getBrandedColorScheme());
+ }
+
+ @Test
public void testIncognitoTheme() {
// emulate incognito tab with chrome default theme
var intentDataProvider =
diff --git a/chrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProvider.java b/chrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProvider.java
index 4bf998a5..5c51e27 100644
--- a/chrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProvider.java
+++ b/chrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProvider.java
@@ -79,6 +79,11 @@
}
@Override
+ public void onSSLStateUpdated(Tab tab) {
+ updateColor(tab, tab.getThemeColor(), false);
+ }
+
+ @Override
public void onContentChanged(Tab tab) {
if (tab != null) {
updateColor(tab, tab.getThemeColor(), false);
diff --git a/chrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProviderUnitTest.java b/chrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProviderUnitTest.java
new file mode 100644
index 0000000..66fe6ca3
--- /dev/null
+++ b/chrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProviderUnitTest.java
@@ -0,0 +1,98 @@
+// 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.theme;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.view.ContextThemeWrapper;
+
+import androidx.annotation.ColorInt;
+
+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.ContextUtils;
+import org.chromium.base.supplier.ObservableSuppliers;
+import org.chromium.base.supplier.SettableNullableObservableSupplier;
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.browser.tab.TabObserver;
+import org.chromium.chrome.browser.tab.TabState;
+import org.chromium.chrome.browser.ui.theme.BrandedColorScheme;
+import org.chromium.components.browser_ui.styles.ChromeColors;
+
+/** Unit tests for {@link TopUiThemeColorProvider}. */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class TopUiThemeColorProviderUnitTest {
+ @Rule public MockitoRule mMockitoJUnit = MockitoJUnit.rule();
+
+ @Mock private Tab mTab;
+ @Captor private ArgumentCaptor<TabObserver> mTabObserverCaptor;
+
+ private static final @ColorInt int ACTIVITY_COLOR = Color.GREEN;
+
+ private Context mContext;
+ private TopUiThemeColorProvider mProvider;
+ private SettableNullableObservableSupplier<Tab> mTabSupplier;
+
+ @Before
+ public void setUp() {
+ mContext =
+ new ContextThemeWrapper(
+ ContextUtils.getApplicationContext(), R.style.Theme_BrowserUI_DayNight);
+
+ when(mTab.getContext()).thenReturn(mContext);
+ when(mTab.isIncognito()).thenReturn(false);
+ when(mTab.isNativePage()).thenReturn(false);
+ when(mTab.isThemingAllowed()).thenReturn(true);
+ when(mTab.getThemeColor()).thenReturn(TabState.UNSPECIFIED_THEME_COLOR);
+
+ mTabSupplier = ObservableSuppliers.createNullable();
+ mProvider =
+ new TopUiThemeColorProvider(
+ mContext,
+ mTabSupplier,
+ () -> ACTIVITY_COLOR,
+ /* isTablet= */ false,
+ /* allowThemingInNightMode= */ true,
+ /* allowBrightThemeColors= */ true,
+ /* allowThemingOnTablets= */ true);
+ mTabSupplier.set(mTab);
+ verify(mTab).addObserver(mTabObserverCaptor.capture());
+ }
+
+ @Test
+ public void testOnSSLStateUpdated() {
+ assertEquals(
+ "Activity theme color should be used while theming is allowed",
+ ACTIVITY_COLOR,
+ mProvider.getThemeColor());
+
+ when(mTab.isThemingAllowed()).thenReturn(false);
+ mTabObserverCaptor.getValue().onSSLStateUpdated(mTab);
+
+ assertEquals(
+ "Default theme color should be used once theming is no longer allowed",
+ ChromeColors.getDefaultThemeColor(mContext, /* isIncognito= */ false),
+ mProvider.getThemeColor());
+ assertEquals(
+ "Default color scheme should be used once theming is no longer allowed",
+ BrandedColorScheme.APP_DEFAULT,
+ mProvider.getBrandedColorScheme());
+ }
+}
diff --git a/chrome/browser/ui/android/theme/BUILD.gn b/chrome/browser/ui/android/theme/BUILD.gn
index d5937f2..b17cf4f 100644
--- a/chrome/browser/ui/android/theme/BUILD.gn
+++ b/chrome/browser/ui/android/theme/BUILD.gn
@@ -59,6 +59,7 @@
"java/src/org/chromium/chrome/browser/theme/BottomUiThemeColorProviderTest.java",
"java/src/org/chromium/chrome/browser/theme/ThemeUtilsUnitTest.java",
"java/src/org/chromium/chrome/browser/theme/ToolbarThemeColorProviderTest.java",
+ "java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProviderUnitTest.java",
]
deps = [
":java",
Original Bug Report
Potential security indicator degradation on post-commit downgrade in WebAPK/TWA
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 potential vulnerability in Android WebAPK/TWA surface theme color handling can lead to security indicator degradation. When a page undergoes a post-commit security state downgrade to DANGEROUS without navigation, the custom branded theme color can persist on the toolbar. Consequently, the forced-visible toolbar remains themed, the warning icon is tinted neutrally instead of red, and URL scheme emphasis is disabled.
Affected files:
chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProvider.javachrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.javachrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProvider.javachrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/LocationBarModel.java
Estimated timestamp from git blame: 2022-01-17
Potential Security Indicator Degradation on Post-Commit Downgrade in WebAPK/TWA
Description
In Android WebAPK and Trusted Web Activity (TWA) surfaces, a potential logic flaw allows an attacker-controlled manifest theme_color to persist even after the page’s security state is downgraded to DANGEROUS (e.g., due to loading an active subresource with certificate errors on a previously trusted domain).
Consequently, when the security toolbar is forced on-screen to warn the user, it remains themed with the attacker-chosen color, the SSL warning triangle icon is tinted with a neutral contrast color (grey or white) instead of warning red, and URL scheme emphasis is disabled. This significantly weakens the visual prominence of the DANGEROUS security warning.
Potential Root Cause Analysis
-
Short-Circuit in Theme Updates: When a tab’s security state changes to
DANGEROUS, the SSL-to-theme bridge (TabThemeColorHelper.onSSLStateUpdated->TabImpl.updateThemeColor()) clamps the theme color toUNSPECIFIED_THEME_COLORsince theming is no longer allowed. However, if the page does not explicitly specify a<meta name="theme-color">tag, the currentmThemeColorinTabImplis alreadyTabState.UNSPECIFIED_THEME_COLOR(default value). As a result, the update inTabImpl.java(line 1956) short-circuits:// chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java void updateThemeColor(int themeColor) { if (!isThemingAllowed()) { themeColor = TabState.UNSPECIFIED_THEME_COLOR; } if (mThemeColor == themeColor) { // UNSPECIFIED_THEME_COLOR == UNSPECIFIED_THEME_COLOR (0 == 0) return; // Short-circuits; onDidChangeThemeColor is never fired } ... }Because it returns early,
onDidChangeThemeColoris never fired to notify the registered theme providers. -
Lack of SSL State Observers: Both
BrowserServicesThemeColorProviderandTopUiThemeColorProviderfail to listen directly foronSSLStateUpdatedevents in their respective tab observers (chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProvider.javaline 89 andchrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProvider.javaline 72). SinceonDidChangeThemeColoris never fired, neither provider recalculates whether theming is allowed. -
Neutral Warning Tint and Lack of URL Emphasis: Because the theme providers fail to update,
LocationBarModel.mPrimaryColorretains the stale, attacker-provided WebAPK manifest color.- When the toolbar determines the warning icon tint in
LocationBarModel.getSecurityIconColorWithSecurityLevel()(chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/LocationBarModel.javaline 851), it calculates a branded color scheme (e.g.LIGHT_BRANDED_THEMEorDARK_BRANDED_THEME) instead ofBrandedColorScheme.APP_DEFAULT. Under branded schemes, it falls back to a neutral grey/white contrast tint (line 868) instead of the standard warning red (R.color.default_red). - Additionally,
LocationBarModel.shouldEmphasizeHttpsScheme()(line 597) returnsfalsebecause the system still reports that it is using a brand color (mIsUsingBrandColor == true), preventing the URL scheme from being emphasized.
- When the toolbar determines the warning icon tint in
Potential Steps to Trigger / Reproduce
Note: Since our automated tooling cannot run arbitrary code or execute interactive Android environments, the following are suggested/potential reproduction steps based on static analysis:
- Compile and install an Android WebAPK or TWA for a PWA that specifies
"theme_color": "#00aa00"(or another highly distinct color) in its manifest, but has no<meta name="theme-color">tag in its web pages. - From an external domain (e.g.,
https://cdn.attacker.test), serve an active subresource over TLS with an invalid certificate. Have the user proceed through the SSL certificate interstitial once (e.g., in Chrome) so that the certificate error is added to the local allowlist. - Open the installed WebAPK. The origin will verify, the toolbar will be hidden, and the status bar will be the manifest green color (
#00aa00). - Trigger the injection of the allowlisted insecure subresource (e.g.,
<script src="https://cdn.attacker.test/script.js"></script>) without executing a page navigation. - Observe that the TWA/WebAPK toolbar slides back into view to warn the user, but:
- The toolbar background is still green.
- The warning triangle icon is displayed but tinted white/grey instead of red.
- The HTTPS scheme is not emphasized.
Suggested Fix
Ensure that both TopUiThemeColorProvider and BrowserServicesThemeColorProvider listen to SSL/security state changes.
-
In
chrome/browser/ui/android/theme/java/src/org/chromium/chrome/browser/theme/TopUiThemeColorProvider.java, addonSSLStateUpdatedtomTabObserverto triggerupdateColor():@Override public void onSSLStateUpdated(Tab tab) { if (tab != null) { updateColor(tab, tab.getThemeColor(), false); } } -
In
chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/toolbar/BrowserServicesThemeColorProvider.java, addonSSLStateUpdatedtomTabObserverto triggerupdateTheme():@Override public void onSSLStateUpdated(Tab tab) { updateTheme(); }
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.