Low chrome Input Validation 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in UI
DescriptionImproper input validation in UI
ComponentUI
Sub-componentTabSharingToolbar
Bug ClassInput Validation
Tracker553274389
Fix commitbce8d62ded2f (chromium/src) +262/-48
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Files Changed

  • chrome/android/junit/src/org/chromium/chrome/browser/media/TabSharingToolbarMediatorTest.java
  • chrome/android/java/src/org/chromium/chrome/browser/media/TabSharingToolbarMediator.java
  • chrome/android/junit/src/org/chromium/chrome/browser/media/TabSharingToolbarUiCoordinatorTest.java
From bce8d62ded2f55e7efda1eb8257f454cbe288f97 Mon Sep 17 00:00:00 2001
From: Eric Lok <lokeric@google.com>
Date: Sun, 27 Sep 2026 17:43:08 -0700
Subject: [PATCH] [TabSharingToolbar] Recompute origin status strings on navigation

Previously, TabSharingToolbarMediator computed the capturer and capturee
origin display strings once during initialization and stored them in
final fields. Because tab video capture persists across navigations by
design, cross-origin navigations in the captured tab resulted in the
toolbar displaying stale origin strings for the remainder of the
session.

Fix this by: 1. Making the cached status strings mutable and extracting
a helper to rebuild them from getLastCommittedUrl(). 2. Attaching
WebContentsObservers to both the capturer and capturee WebContents to
rebuild the status strings and update the toolbar model on
primaryPageChanged(). 3. Detaching both observers in destroy().

TAG=agy
CONV=78698e81-0149-4ffa-9ba0-fb7b711587a8

Test: tools/autotest.py -C out/android_x64 TabSharingToolbarMediatorTest
Bug: 553274389
Change-Id: I98866b4a354a245245ad2f9605a70636422b1f08
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8425163
Commit-Queue: Eric Lok <lokeric@google.com>
Auto-Submit: Eric Lok <lokeric@google.com>
Reviewed-by: Phil Yan <philyan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1705779}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/TabSharingToolbarMediator.java b/chrome/android/java/src/org/chromium/chrome/browser/media/TabSharingToolbarMediator.java
index 59be2ce..1c992ac5 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/TabSharingToolbarMediator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/TabSharingToolbarMediator.java
@@ -18,7 +18,9 @@
 import org.chromium.chrome.browser.ui.native_page.NativePage;
 import org.chromium.components.url_formatter.SchemeDisplay;
 import org.chromium.components.url_formatter.UrlFormatter;
+import org.chromium.content_public.browser.Page;
 import org.chromium.content_public.browser.WebContents;
+import org.chromium.content_public.browser.WebContentsObserver;
 import org.chromium.ui.modelutil.PropertyModel;
 import org.chromium.ui.text.SpanApplier;
 import org.chromium.ui.text.SpanApplier.SpanInfo;
@@ -43,9 +45,16 @@
 
     private final WebContents mCapturer;
     private final WebContents mCapturee;
-    private final CharSequence mCaptureeStatus;
-    private final CharSequence mCapturerStatus;
-    private final CharSequence mOtherTabsStatus;
+    private final ClickableSpan mCapturerSpan;
+    private final ClickableSpan mCaptureeSpan;
+    private final WebContentsObserver mCapturerObserver;
+    private final WebContentsObserver mCaptureeObserver;
+
+    private @Nullable String mCapturerName;
+    private @Nullable String mCaptureeName;
+    private CharSequence mCaptureeStatus = "";
+    private CharSequence mCapturerStatus = "";
+    private CharSequence mOtherTabsStatus = "";
 
     /**
      * Initializes the mediator.
@@ -67,35 +76,13 @@
 
         mCapturer = mBridge.getCapturer();
         mCapturee = mBridge.getCapturee();
+        mCapturerSpan = buildClickToNavigateToTabSpan(mCapturer);
+        mCaptureeSpan = buildClickToNavigateToTabSpan(mCapturee);
 
-        ClickableSpan capturerSpan = buildClickToNavigateToTabSpan(mCapturer);
-        ClickableSpan captureeSpan = buildClickToNavigateToTabSpan(mCapturee);
-        String capturerName =
-                UrlFormatter.formatUrlForSecurityDisplay(
-                        mCapturer.getLastCommittedUrl(), SchemeDisplay.OMIT_HTTP_AND_HTTPS);
-        String captureeName =
-                UrlFormatter.formatUrlForSecurityDisplay(
-                        mCapturee.getLastCommittedUrl(), SchemeDisplay.OMIT_HTTP_AND_HTTPS);
-        mCaptureeStatus =
-                SpanApplier.applySpans(
-                        mContext.getString(
-                                R.string.tab_sharing_toolbar_sharing_current_tab_label,
-                                LINK + capturerName + LINK_END),
-                        new SpanInfo(LINK, LINK_END, capturerSpan));
-        mCapturerStatus =
-                SpanApplier.applySpans(
-                        mContext.getString(
-                                R.string.tab_sharing_toolbar_sharing_another_tab_to_this_tab_label,
-                                LINK + captureeName + LINK_END),
-                        new SpanInfo(LINK, LINK_END, captureeSpan));
-        mOtherTabsStatus =
-                SpanApplier.applySpans(
-                        mContext.getString(
-                                R.string.tab_sharing_toolbar_sharing_another_tab_label,
-                                LINK1 + captureeName + LINK1_END,
-                                LINK2 + capturerName + LINK2_END),
-                        new SpanInfo(LINK1, LINK1_END, captureeSpan),
-                        new SpanInfo(LINK2, LINK2_END, capturerSpan));
+        rebuildStatusStrings();
+
+        mCapturerObserver = createNavigationObserver(mCapturer);
+        mCaptureeObserver = createNavigationObserver(mCapturee);
 
         mActiveTabObserver =
                 new ActivityTabProvider.ActivityTabTabObserver(tabProvider, true) {
@@ -116,6 +103,10 @@
 
                     @Override
                     public void onUrlUpdated(Tab tab) {
+                        if (tab.getWebContents() == mCapturee
+                                || tab.getWebContents() == mCapturer) {
+                            rebuildStatusStrings();
+                        }
                         updateToolbarForTab(tab);
                     }
                 };
@@ -128,6 +119,82 @@
         updateToolbarForTab(mTabProvider.get());
     }
 
+    private WebContentsObserver createNavigationObserver(WebContents webContents) {
+        return new WebContentsObserver(webContents) {
+            @Override
+            public void primaryPageChanged(Page page) {
+                onSharedWebContentsNavigated();
+            }
+
+            @Override
+            public void webContentsDestroyed() {
+                observe(null);
+            }
+        };
+    }
+
+    private void onSharedWebContentsNavigated() {
+        if (rebuildStatusStrings()) {
+            updateToolbarForTab(mTabProvider.get());
+        }
+    }
+
+    /**
+     * Recomputes the formatted origin status strings if either the capturer or capturee origin has
+     * changed.
+     *
+     * <p>Navigation callbacks (such as {@link WebContentsObserver#primaryPageChanged} and {@link
+     * ActivityTabProvider.ActivityTabTabObserver#onUrlUpdated}) can fire multiple times during a
+     * single page load or during same-origin navigations (e.g. URL fragment/query updates or
+     * history.pushState) where the formatted security display origin remains identical.
+     *
+     * <p>Short-circuiting when the formatted origins match avoids unnecessary {@link
+     * android.text.SpannableString} allocations and preserves object reference identity for {@code
+     * mCaptureeStatus}, {@code mCapturerStatus}, and {@code mOtherTabsStatus}. This allows {@link
+     * PropertyModel#set} to no-op via reference equality and prevents redundant {@code TextView}
+     * re-bindings and layout passes in the toolbar UI.
+     *
+     * @return True if the status strings were rebuilt due to an origin change; false otherwise.
+     */
+    private boolean rebuildStatusStrings() {
+        if (mCapturer.isDestroyed() || mCapturee.isDestroyed()) {
+            return false;
+        }
+        String capturerName =
+                UrlFormatter.formatUrlForSecurityDisplay(
+                        mCapturer.getLastCommittedUrl(), SchemeDisplay.OMIT_HTTP_AND_HTTPS);
+        String captureeName =
+                UrlFormatter.formatUrlForSecurityDisplay(
+                        mCapturee.getLastCommittedUrl(), SchemeDisplay.OMIT_HTTP_AND_HTTPS);
+        if (capturerName.equals(mCapturerName) && captureeName.equals(mCaptureeName)) {
+            return false;
+        }
+        mCapturerName = capturerName;
+        mCaptureeName = captureeName;
+
+        mCaptureeStatus =
+                SpanApplier.applySpans(
+                        mContext.getString(
+                                R.string.tab_sharing_toolbar_sharing_current_tab_label,
+                                LINK + capturerName + LINK_END),
+                        new SpanInfo(LINK, LINK_END, mCapturerSpan));
+        mCapturerStatus =
+                SpanApplier.applySpans(
+                        mContext.getString(
+                                R.string.tab_sharing_toolbar_sharing_another_tab_to_this_tab_label,
+                                LINK + captureeName + LINK_END),
+                        new SpanInfo(LINK, LINK_END, mCaptureeSpan));
+        mOtherTabsStatus =
+                SpanApplier.applySpans(
+                        mContext.getString(
+                                R.string.tab_sharing_toolbar_sharing_another_tab_label,
+                                LINK1 + captureeName + LINK1_END,
+                                LINK2 + capturerName + LINK2_END),
+                        new SpanInfo(LINK1, LINK1_END, mCaptureeSpan),
+                        new SpanInfo(LINK2, LINK2_END, mCapturerSpan));
+        return true;
+    }
+
     /**
      * Updates the toolbar status text and the "Share this tab instead" button visibility based on
      * the currently active tab.
@@ -235,5 +302,7 @@
     /** Cleans up resources. */
     public void destroy() {
         mActiveTabObserver.destroy();
+        mCapturerObserver.observe(null);
+        mCaptureeObserver.observe(null);
     }
 }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/media/TabSharingToolbarMediatorTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/media/TabSharingToolbarMediatorTest.java
index 3f17dd5..f6695f6 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/media/TabSharingToolbarMediatorTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/media/TabSharingToolbarMediatorTest.java
@@ -6,6 +6,7 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -29,6 +30,7 @@
 import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
 
 import org.chromium.base.test.BaseRobolectricTestRunner;
 import org.chromium.chrome.browser.ActivityTabProvider;
@@ -36,6 +38,7 @@
 import org.chromium.chrome.browser.tab.TabObserver;
 import org.chromium.components.url_formatter.UrlFormatter;
 import org.chromium.components.url_formatter.UrlFormatterJni;
+import org.chromium.content_public.browser.Page;
 import org.chromium.content_public.browser.WebContents;
 import org.chromium.content_public.browser.WebContentsObserver;
 import org.chromium.ui.modelutil.PropertyModel;
@@ -44,11 +47,16 @@
 /** Unit tests for {@link TabSharingToolbarMediator}. */
 @RunWith(BaseRobolectricTestRunner.class)
 public class TabSharingToolbarMediatorTest {
-    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.LENIENT);
 
     @Mock private TabSharingUiBridge mBridge;
+
+    @Mock(extraInterfaces = {WebContentsObserver.Observable.class})
     private WebContents mCapturer;
-    @Mock private WebContents mCapturee;
+
+    @Mock(extraInterfaces = {WebContentsObserver.Observable.class})
+    private WebContents mCapturee;
+
     @Mock private WebContents mOtherWebContents;
     @Mock private Tab mCurrentTab;
     @Mock private UrlFormatter.Natives mUrlFormatterJniMock;
@@ -62,11 +70,6 @@
     @Before
     public void setUp() {
         mContext = ApplicationProvider.getApplicationContext();
-        mCapturer =
-                Mockito.mock(
-                        WebContents.class,
-                        Mockito.withSettings()
-                                .extraInterfaces(WebContentsObserver.Observable.class));
         when(mBridge.getCapturer()).thenReturn(mCapturer);
         when(mBridge.getCapturee()).thenReturn(mCapturee);
         when(mBridge.isSourceSwitchingSupported()).thenReturn(true);
@@ -86,9 +89,7 @@
                 .thenAnswer(
                         (invocation) -> {
                             GURL url = (GURL) invocation.getArgument(0);
-                            if (url == capturerUrl) return "meet.google.com";
-                            if (url == captureeUrl) return "youtube.com";
-                            return "other.com";
+                            return url.getHost();
                         });
 
         MediaCaptureDevicesDispatcherAndroidJni.setInstanceForTesting(mDispatcherJniMock);
@@ -284,4 +285,149 @@
         spans[0].updateDrawState(paint);
         assertFalse(paint.isUnderlineText());
     }
+
+    @Test
+    public void testCaptureeNavigatesInBackground_UpdatesStatusText() {
+        // User is viewing the Capturer tab while Capturee tab is in the background.
+        when(mCurrentTab.getWebContents()).thenReturn(mCapturer);
+        mMediator = new TabSharingToolbarMediator(mContext, mModel, mBridge, mTabProvider);
+
+        CharSequence initialStatus = mModel.get(TabSharingToolbarProperties.STATUS_TEXT);
+        assertTrue(initialStatus.toString().contains("Sharing youtube.com to this tab"));
+
+        ArgumentCaptor<WebContentsObserver> observerCaptor =
+                ArgumentCaptor.forClass(WebContentsObserver.class);
+        verify((WebContentsObserver.Observable) mCapturee).addObserver(observerCaptor.capture());
+        WebContentsObserver captureeObserver = observerCaptor.getValue();
+
+        // Simulate cross-origin navigation in the background capturee tab.
+        when(mCapturee.getLastCommittedUrl())
+                .thenReturn(new GURL("https://sensitive-bank.example"));
+        captureeObserver.primaryPageChanged(Mockito.mock(Page.class));
+
+        CharSequence updatedStatus = mModel.get(TabSharingToolbarProperties.STATUS_TEXT);
+        assertTrue(
+                "Toolbar status must immediately reflect the new capturee origin",
+                updatedStatus.toString().contains("Sharing sensitive-bank.example to this tab"));
+    }
+
+    @Test
+    public void testCapturerNavigates_UpdatesStatusText() {
+        when(mCurrentTab.getWebContents()).thenReturn(mCapturee);
+        mMediator = new TabSharingToolbarMediator(mContext, mModel, mBridge, mTabProvider);
+
+        CharSequence initialStatus = mModel.get(TabSharingToolbarProperties.STATUS_TEXT);
+        assertTrue(initialStatus.toString().contains("Sharing this tab to meet.google.com"));
+
+        ArgumentCaptor<WebContentsObserver> observerCaptor =
+                ArgumentCaptor.forClass(WebContentsObserver.class);
+        verify((WebContentsObserver.Observable) mCapturer).addObserver(observerCaptor.capture());
+        WebContentsObserver capturerObserver = observerCaptor.getValue();
+
+        when(mCapturer.getLastCommittedUrl()).thenReturn(new GURL("https://new-capturer.example"));
+        capturerObserver.primaryPageChanged(Mockito.mock(Page.class));
+
+        CharSequence updatedStatus = mModel.get(TabSharingToolbarProperties.STATUS_TEXT);
+        assertTrue(updatedStatus.toString().contains("Sharing this tab to new-capturer.example"));
+    }
+
+    @Test
+    public void testDestroy_RemovesWebContentsObservers() {
+        mMediator = new TabSharingToolbarMediator(mContext, mModel, mBridge, mTabProvider);
+
+        ArgumentCaptor<WebContentsObserver> capturerObserverCaptor =
+                ArgumentCaptor.forClass(WebContentsObserver.class);
+        ArgumentCaptor<WebContentsObserver> captureeObserverCaptor =
+                ArgumentCaptor.forClass(WebContentsObserver.class);
+        verify((WebContentsObserver.Observable) mCapturer)
+                .addObserver(capturerObserverCaptor.capture());
+        verify((WebContentsObserver.Observable) mCapturee)
+                .addObserver(captureeObserverCaptor.capture());
+
+        mMediator.destroy();
+        mMediator = null;
+
+        verify((WebContentsObserver.Observable) mCapturer)
+                .removeObserver(capturerObserverCaptor.getValue());
+        verify((WebContentsObserver.Observable) mCapturee)
+                .removeObserver(captureeObserverCaptor.getValue());
+    }
+
+    @Test
+    public void testNavigation_ViewingOtherTab_UpdatesStatusText() {
+        // User is viewing a third tab (neither capturer nor capturee).
+        when(mCurrentTab.getWebContents()).thenReturn(mOtherWebContents);
+        mMediator = new TabSharingToolbarMediator(mContext, mModel, mBridge, mTabProvider);
+
+        CharSequence initialStatus = mModel.get(TabSharingToolbarProperties.STATUS_TEXT);
+        assertTrue(initialStatus.toString().contains("Sharing youtube.com to meet.google.com"));
+
+        ArgumentCaptor<WebContentsObserver> captureeObserverCaptor =
+                ArgumentCaptor.forClass(WebContentsObserver.class);
+        verify((WebContentsObserver.Observable) mCapturee)
+                .addObserver(captureeObserverCaptor.capture());
+
+        when(mCapturee.getLastCommittedUrl())
+                .thenReturn(new GURL("https://sensitive-bank.example"));
+        captureeObserverCaptor.getValue().primaryPageChanged(Mockito.mock(Page.class));
+
+        CharSequence updatedStatus = mModel.get(TabSharingToolbarProperties.STATUS_TEXT);
+        assertTrue(
+                updatedStatus
+                        .toString()
+                        .contains("Sharing sensitive-bank.example to meet.google.com"));
+    }
+
+    @Test
+    public void testSameOriginNavigation_ShortCircuitsRebuild() {
+        when(mCurrentTab.getWebContents()).thenReturn(mCapturer);
+        mMediator = new TabSharingToolbarMediator(mContext, mModel, mBridge, mTabProvider);
+
+        CharSequence initialStatusInstance = mModel.get(TabSharingToolbarProperties.STATUS_TEXT);
+        assertNotNull(initialStatusInstance);
+
+        ArgumentCaptor<WebContentsObserver> captureeObserverCaptor =
+                ArgumentCaptor.forClass(WebContentsObserver.class);
+        verify((WebContentsObserver.Observable) mCapturee)
+                .addObserver(captureeObserverCaptor.capture());
+
+        // Simulate a same-origin navigation (path/query change) where host remains "youtube.com".
+        when(mCapturee.getLastCommittedUrl())
+                .thenReturn(new GURL("https://youtube.com/watch?v=12345"));
+        captureeObserverCaptor.getValue().primaryPageChanged(Mockito.mock(Page.class));
+
+        CharSequence statusAfterSameOriginNav = mModel.get(TabSharingToolbarProperties.STATUS_TEXT);
+        assertSame(
+                "Status CharSequence instance should not be re-allocated when origin is unchanged",
+                initialStatusInstance,
+                statusAfterSameOriginNav);
+    }
+
... (file diff truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.