CVE-2026-13987
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.javachrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.javachrome/android/junit/src/org/chromium/chrome/browser/tab/TabUnitTest.java
Patch
From 8b38224fc2a8f6d14b535cf8ce50bb52cf44e25d Mon Sep 17 00:00:00 2001
From: Calder Kitagawa <ckitagawa@chromium.org>
Date: Tue, 19 May 2026 09:56:22 -0700
Subject: [PATCH] [Tab] Update theme color on security status change
Fixed: 514039122
Change-Id: I4465001abc30ef9e4505f75b981b9967d489f4a6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7853600
Reviewed-by: Sky Malice <skym@chromium.org>
Auto-Submit: Calder Kitagawa <ckitagawa@chromium.org>
Commit-Queue: Calder Kitagawa <ckitagawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1632961}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
index c0e2679..2e49bf3 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
@@ -1955,7 +1955,12 @@
}
void updateThemeColor(int themeColor) {
- if (mThemeColor == themeColor) return;
+ if (!isThemingAllowed()) {
+ themeColor = TabState.UNSPECIFIED_THEME_COLOR;
+ }
+ if (mThemeColor == themeColor) {
+ return;
+ }
mThemeColor = themeColor;
RewindableIterator<TabObserver> observers = getTabObservers();
while (observers.hasNext()) observers.next().onDidChangeThemeColor(this, themeColor);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java
index 0b89fb04..33f8dfd1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java
@@ -366,6 +366,13 @@
}
@Override
+ public void didChangeVisibleSecurityState() {
+ if (!mTab.isThemingAllowed()) {
+ mTab.updateThemeColor(assumeNonNull(mTab.getWebContents()).getThemeColor());
+ }
+ }
+
+ @Override
public void onBackgroundColorChanged() {
mTab.changeWebContentBackgroundColor(
assumeNonNull(mTab.getWebContents()).getBackgroundColor());
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/tab/TabUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/tab/TabUnitTest.java
index 1fdbcba0a..4b7ff7f 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/tab/TabUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/tab/TabUnitTest.java
@@ -19,11 +19,13 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.content.Context;
+import android.graphics.Color;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewStructure;
@@ -53,9 +55,13 @@
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.prefs.PrefService;
+import org.chromium.components.security_state.ConnectionSecurityLevel;
+import org.chromium.components.security_state.SecurityStateModel;
+import org.chromium.components.security_state.SecurityStateModelJni;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.components.user_prefs.UserPrefsJni;
import org.chromium.content_public.browser.WebContents;
+import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.ui.base.WindowAndroid;
import java.lang.ref.WeakReference;
@@ -79,12 +85,16 @@
@Mock private NativePage mNativePage;
@Mock private TabDelegateFactory mDelegateFactory;
@Mock private TabWebContentsDelegateAndroid mTabWebContentsDelegateAndroid;
- @Mock private WebContents mWebContents;
+
+ @Mock(extraInterfaces = {WebContentsObserver.Observable.class})
+ private WebContents mWebContents;
+
@Mock private View mNativePageView;
@Mock private ChromeActivity mChromeActivity;
@Mock private UserPrefs.Natives mUserPrefsNatives;
@Mock private PrefService mPrefs;
@Mock TabImpl.Natives mNativeMock;
+ @Mock private SecurityStateModel.Natives mSecurityStateModelNatives;
@Captor private ArgumentCaptor<Callback<Tab>> mCallbackCaptor;
private final SettableLookAheadObservableSupplier<Tab> mTabSupplier =
@@ -101,6 +111,7 @@
doReturn(mContext).when(mWeakReferenceContext).get();
doReturn(mContext).when(mContext).getApplicationContext();
UserPrefsJni.setInstanceForTesting(mUserPrefsNatives);
+ SecurityStateModelJni.setInstanceForTesting(mSecurityStateModelNatives);
when(mUserPrefsNatives.get(mProfile)).thenReturn(mPrefs);
mTab =
@@ -410,4 +421,108 @@
Tab tab = new TabImpl(1, mProfile, TabLaunchType.FROM_LINK, /* isArchived= */ false);
assertThat(tab.getTimestampMillis(), equalTo(TabImpl.INVALID_TIMESTAMP));
}
+
+ @Test
+ @SmallTest
+ public void testUpdateThemeColor_themingAllowed() {
+ when(mSecurityStateModelNatives.getSecurityLevelForWebContents(mWebContents))
+ .thenReturn(ConnectionSecurityLevel.NONE);
+ TabImpl tab =
+ new TabImpl(
+ TAB1_ID, mProfile, TabLaunchType.FROM_CHROME_UI, /* isArchived= */ false) {
+ @Override
+ public boolean isInitialized() {
+ return true;
+ }
+
+ @Override
+ public WebContents getWebContents() {
+ return mWebContents;
+ }
+ };
+ tab.addObserver(mObserver);
+
+ tab.updateThemeColor(Color.RED);
+ verify(mObserver).onDidChangeThemeColor(tab, Color.RED);
+
+ tab.updateThemeColor(Color.RED);
+ // Not called a second time.
+ verify(mObserver).onDidChangeThemeColor(tab, Color.RED);
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateThemeColor_themingNotAllowed() {
+ when(mSecurityStateModelNatives.getSecurityLevelForWebContents(mWebContents))
+ .thenReturn(ConnectionSecurityLevel.NONE);
+ TabImpl tab =
+ new TabImpl(
+ TAB1_ID, mProfile, TabLaunchType.FROM_CHROME_UI, /* isArchived= */ false) {
+ @Override
+ public boolean isInitialized() {
+ return true;
+ }
+
+ @Override
+ public WebContents getWebContents() {
+ return mWebContents;
+ }
+ };
+ tab.addObserver(mObserver);
+
+ // Set initial theme color while theming is allowed.
+ tab.updateThemeColor(Color.RED);
+ verify(mObserver).onDidChangeThemeColor(tab, Color.RED);
+
+ // Disallow theming.
+ when(mSecurityStateModelNatives.getSecurityLevelForWebContents(mWebContents))
+ .thenReturn(ConnectionSecurityLevel.DANGEROUS);
+
+ tab.updateThemeColor(Color.BLUE);
+ verify(mObserver).onDidChangeThemeColor(tab, TabState.UNSPECIFIED_THEME_COLOR);
+
+ // Calling again when already unspecified should not emit.
+ tab.updateThemeColor(Color.GREEN);
+ verify(mObserver, times(1)).onDidChangeThemeColor(tab, TabState.UNSPECIFIED_THEME_COLOR);
+ }
+
+ @Test
+ @SmallTest
+ public void testDidChangeVisibleSecurityState_themingNotAllowed() {
+ when(mSecurityStateModelNatives.getSecurityLevelForWebContents(mWebContents))
+ .thenReturn(ConnectionSecurityLevel.NONE);
+ when(mWebContents.getThemeColor()).thenReturn(Color.RED);
+ TabImpl tab =
+ new TabImpl(
+ TAB1_ID, mProfile, TabLaunchType.FROM_CHROME_UI, /* isArchived= */ false) {
+ @Override
+ public boolean isInitialized() {
+ return true;
+ }
+
+ @Override
+ public WebContents getWebContents() {
+ return mWebContents;
+ }
+ };
+ tab.addObserver(mObserver);
+
+ TabWebContentsObserver tabWebContentsObserver = TabWebContentsObserver.from(tab);
+ tabWebContentsObserver.initWebContents(mWebContents);
Original Bug Report
Potential persistence of page-supplied theme-color after security downgrade to DANGEROUS
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 logic error in TabImpl allows an attacker-set theme-color to persist even after a page’s security level is downgraded to DANGEROUS. This causes the Android toolbar to retain the attacker-chosen color and prevents security indicators from displaying their critical red warning tint. The issue stems from a value-equality short-circuit that suppresses UI re-evaluation during post-commit security state changes.
Affected files:
chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.javachrome/android/java/src/org/chromium/chrome/browser/tab/TabThemeColorHelper.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
Technical Analysis
A potential vulnerability exists in Chrome for Android where a page-supplied <meta name="theme-color"> remains active after a security downgrade to DANGEROUS. This state occurs when a page initially committed as SECURE subsequently loads active content (like a script) from a host with certificate errors that the user has previously bypassed.
The issue is located in the interaction between TabImpl.java and its observers:
- Early Return in
TabImpl.updateThemeColor: When a security downgrade occurs,TabThemeColorHelpertriggers an update by passing the currently cached theme color back toTabImpl.updateThemeColor(int themeColor). Inside this method, a checkif (mThemeColor == themeColor) return;(line 1945) causes an early exit if the color value itself hasn’t changed. - Suppressed Notifications: Because of the early return,
TabImplfails to fire theonDidChangeThemeColorevent. Observers likeTopUiThemeColorProvider—which are responsible for reverting the toolbar to the default system color whentab.isThemingAllowed()returns false—are never notified to re-evaluate the tab’s state. - Degraded Security Indicators: When the toolbar remains in a ‘branded’ (custom color) state,
LocationBarModelmodifies the appearance of security indicators to maintain visual contrast:getSecurityIconColorWithSecurityLevel(line 845) returns a neutral (grey/white) tint instead ofR.color.default_redbecause the color scheme is notAPP_DEFAULT.shouldEmphasizeHttpsScheme(line 580) returns false when a brand color is in use, causingOmniboxUrlEmphasizerto render the struck-through ‘https’ prefix in a neutral color rather than red.
Potential Reproduction Steps
These are suggested steps based on code analysis; a functional proof of concept has not yet been executed.
- User visits
https://bad-cert.testand chooses to “Proceed” through the SSL interstitial, adding the host to the session’s exception list. - User navigates to
https://attacker.test(valid certificate), which specifies a theme color:<meta name="theme-color" content="#1a73e8">. - The toolbar turns blue. The security state is
SECURE. - The attacker’s page dynamically loads a script from the previously bypassed host:
<script src="https://bad-cert.test/script.js"></script>. - The security level is downgraded to
DANGEROUSdue to active mixed content/cert errors. - Observed Result: The toolbar remains blue. The “dangerous” triangle icon and the struck-through ‘https’ in the omnibox appear in neutral white/grey instead of the expected red warning color.
Suggested Fix
Modify TabImpl.updateThemeColor to ensure observers are notified when the security state changes, even if the color value remains the same. Alternatively, TopUiThemeColorProvider and LocationBarModel should listen directly to SSL state changes and re-verify isThemingAllowed() independently of color change events.
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.