Low watch CVSS 4.2 chrome Spoofing 🔧 Commit mapped

Overview

Low
Severity
4.2
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in SplitView
DescriptionIncorrect security UI in SplitView
ComponentSplitView
Sub-componentSxS
Bug ClassSpoofing
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.
Tracker444932667
Fix commit5afd445e6dea (chromium/src) +59/-2
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedHafiizh
Reported2025-09-14 · bounty $1000 (per vendor advisory)
Fix landed2025-09-17 (public git)
Disclosed2025-10-28
Patch gap41 days — fix public in source before the advisory

Background

Tab Search WebUI
the chrome://tab-search.top-chrome page (tab_search_page.ts, tab_search_item.css). It lists open and recently closed tabs, showing each tab’s title as #primaryText and a host string as #secondaryText. The commit’s [SxS] tag ties it to the Split View (“side-by-side”) tab picker named in the advisory.
URL display guidelines
Chromium’s rules for showing origins in security-relevant UI. When space is short, a hostname must be elided from the front so the registrable domain at the end stays visible, and non-network schemes such as blob: and file: get a descriptive label instead of a host.
`direction: rtl` eliding
a CSS technique that puts the overflow ellipsis at the start of a single-line, nowrap text box, so the rightmost (most significant) labels of a hostname are what stays visible.
`loadTimeData` strings
localized strings passed from C++ (TabSearchUI) into the WebUI. Here these are blobUrlSource and fileUrlSource, which reuse the hover-card strings IDS_HOVER_CARD_BLOB_URL_SOURCE and IDS_HOVER_CARD_FILE_URL_SOURCE.

Root Cause Analysis

Before the patch, TabSearchPage.tabData_ built each row’s display host as new URL(normalizeURL(tab.url.url)).hostname, with no handling for the URL’s scheme. The #secondaryText element was ellipsized at the end like normal left-to-right text. This broke two URL-display rules. First, a long attacker-chosen hostname (for example, one beginning with a trusted brand followed by many subdomain labels) was cut off at the end, so the user saw the trusted-looking prefix while the real registrable domain was hidden. Second, blob: and file: tabs were shown by a raw hostname value, not the “temporary data” / “local or shared file” labels already used by hover cards and the mini toolbar.

The fix adds getDisplayHostnameForUrl_, which returns the localized blobUrlSource or fileUrlSource string for those schemes and url.hostname otherwise. It also adds a #secondaryText { direction: rtl; } rule so long hosts are elided from the front, and passes the two strings from tab_search_ui.cc.

Key insight
The tab picker’s origin text treated the hostname as ordinary text: it truncated the end and ignored the scheme. As a result, the part that identifies the site could be hidden or misrepresented. The fix applies the standard URL-display rules: elide from the front, and use fixed labels for blob: and file: URLs.

Attack Path

  1. Host content on a deceptive hostname The attacker gets the victim to open a tab on a very long hostname whose leading labels imitate a trusted site, or a blob: URL created by attacker content.
  2. User opens the tab picker The victim opens Tab Search or the Split View tab picker that uses it, and tabData_ fills in that tab’s hostname for display.
  3. Misleading origin text is shown With end-elision, only the trusted-looking leading labels fit in #secondaryText, and a blob: tab shows a raw hostname value instead of “temporary data”.
  4. User acts on the spoofed identity The victim believes the tab belongs to a trusted site and selects it (for example, to place it in a split view) and then trusts its content (inferred).

Impact Assessment

The impact is limited to spoofing the origin text shown in a browser-owned WebUI surface. There is no memory-safety or privilege effect. The attacker only needs ordinary web content on a hostname they control, plus the victim opening Tab Search or the Split View picker. No compromised renderer or extension is needed. Whether Split View is enabled by default in a given build is not shown by the diff. The vendor rates this Low severity (“Incorrect security UI in SplitView”), and the provided metadata includes no exploited-in-the-wild statement.

Changed Functions

FunctionChangeNotes
if
chrome/browser/resources/tab_search/tab_search_page.ts
modified

Files Changed

  • chrome/browser/resources/tab_search/tab_search_item.css
  • chrome/browser/resources/tab_search/tab_search_page.ts
  • chrome/browser/ui/webui/tab_search/tab_search_ui.cc
  • chrome/test/data/webui/tab_search/tab_search_page_test.ts

Audit Directions

  • Hostname shown in other WebUI lists
    Search WebUI TypeScript for new URL(...).hostname or .host bound into elided text (history, tab groups, side panels, tab strip, read-anything). Check that these use front-elision or the C++ url_formatter helpers and handle blob:, file:, filesystem: and data: schemes.
  • CSS ellipsis on origin text
    Look for text-overflow: ellipsis with left-to-right direction on elements that show hosts or origins, since end-elision of a hostname goes against the URL display guidelines.
  • Inconsistent scheme labeling across surfaces
    Compare how hover cards, the mini toolbar, Split View and Tab Search label special schemes. A surface that falls back to the raw URL where others show a fixed label is a likely spoofing spot.
From 5afd445e6deae19de5bf3629b451cea29a7b860e Mon Sep 17 00:00:00 2001
From: Alison Gale <[email protected]>
Date: Wed, 17 Sep 2025 12:56:50 -0700
Subject: [PATCH] [SxS] Fix URL eliding for tab search

Show "temporary data" for blob: URLs and "local or shared file" for
file: URLs to match what we do for hovercards and mini toolbar. Elide
URLs from the front to avoid spoofing with long subdomains.

Before: https://screenshot.googleplex.com/BFA7KaDQtYpR5ZH
After: https://screenshot.googleplex.com/3e9uyAMCfZxxSHL

Guidelines: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/url_display_guidelines/url_display_guidelines.md#eliding-urls

Bug: 444932667,444915898
Change-Id: Id3a540eaa4392a85f9db2d0b5424d009a1bd271c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6956837
Reviewed-by: Chris Thompson <[email protected]>
Reviewed-by: Yuheng Huang <[email protected]>
Commit-Queue: Alison Gale <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1516831}
---

diff --git a/chrome/browser/resources/tab_search/tab_search_item.css b/chrome/browser/resources/tab_search/tab_search_item.css
index 8a06e14e..5b3a2ae1 100644
--- a/chrome/browser/resources/tab_search/tab_search_item.css
+++ b/chrome/browser/resources/tab_search/tab_search_item.css
@@ -79,6 +79,11 @@
   white-space: nowrap;
 }
 
+#secondaryText {
+  /* Elide hostname from the front to prevent URL spoofing. */
+  direction: rtl;
+}
+
 #primaryText {
   color: var(--cr-primary-text-color);
   font-size: var(--mwb-primary-text-font-size);
diff --git a/chrome/browser/resources/tab_search/tab_search_page.ts b/chrome/browser/resources/tab_search/tab_search_page.ts
index c58fdc3..3dbd50776 100644
--- a/chrome/browser/resources/tab_search/tab_search_page.ts
+++ b/chrome/browser/resources/tab_search/tab_search_page.ts
@@ -661,11 +661,22 @@
     return ariaLabel(tabData);
   }
 
+  private getDisplayHostnameForUrl_(url: URL): string {
+    if (url.protocol === 'blob:') {
+      return loadTimeData.getString('blobUrlSource');
+    } else if (url.protocol === 'file:') {
+      return loadTimeData.getString('fileUrlSource');
+    } else {
+      return url.hostname;
+    }
+  }
+
   private tabData_(
       tab: Tab|RecentlyClosedTab, inActiveWindow: boolean, type: TabItemType,
       tabGroupsMap: Map<string, TabGroup>): TabData {
-    const tabData =
-        new TabData(tab, type, new URL(normalizeURL(tab.url.url)).hostname);
+    const tabData = new TabData(
+        tab, type,
+        this.getDisplayHostnameForUrl_(new URL(normalizeURL(tab.url.url))));
 
     if (tab.groupId) {
       tabData.tabGroup = tabGroupsMap.get(tokenToString(tab.groupId));
diff --git a/chrome/browser/ui/webui/tab_search/tab_search_ui.cc b/chrome/browser/ui/webui/tab_search/tab_search_ui.cc
index c3c82b31..f30546bc 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search_ui.cc
+++ b/chrome/browser/ui/webui/tab_search/tab_search_ui.cc
@@ -79,10 +79,12 @@
        IDS_TAB_SEARCH_A11Y_RECENTLY_CLOSED_TAB_GROUP},
       {"audioMuting", IDS_TAB_AX_LABEL_AUDIO_MUTING_FORMAT},
       {"audioPlaying", IDS_TAB_AX_LABEL_AUDIO_PLAYING_FORMAT},
+      {"blobUrlSource", IDS_HOVER_CARD_BLOB_URL_SOURCE},
       {"clearSearch", IDS_CLEAR_SEARCH},
       {"closeTab", IDS_TAB_SEARCH_CLOSE_TAB},
       {"collapseRecentlyClosed", IDS_TAB_SEARCH_COLLAPSE_RECENTLY_CLOSED},
       {"expandRecentlyClosed", IDS_TAB_SEARCH_EXPAND_RECENTLY_CLOSED},
+      {"fileUrlSource", IDS_HOVER_CARD_FILE_URL_SOURCE},
       {"mediaRecording", IDS_TAB_AX_LABEL_MEDIA_RECORDING_FORMAT},
       {"audioRecording", IDS_TAB_AX_LABEL_AUDIO_RECORDING_FORMAT},
       {"videoRecording", IDS_TAB_AX_LABEL_VIDEO_RECORDING_FORMAT},
diff --git a/chrome/test/data/webui/tab_search/tab_search_page_test.ts b/chrome/test/data/webui/tab_search/tab_search_page_test.ts
index b91f9ca..5183ed0 100644
--- a/chrome/test/data/webui/tab_search/tab_search_page_test.ts
+++ b/chrome/test/data/webui/tab_search/tab_search_page_test.ts
@@ -4,6 +4,7 @@
 
 import 'chrome://tab-search.top-chrome/tab_search.js';
 
+import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
 import {MetricsReporterImpl} from 'chrome://resources/js/metrics_reporter/metrics_reporter.js';
 import type {ProfileData, RecentlyClosedTab, Tab, TabSearchItemElement, TabSearchPageElement} from 'chrome://tab-search.top-chrome/tab_search.js';
 import {SEARCH_QUERY_MAX_LENGTH, TabGroupColor, TabSearchApiProxyImpl} from 'chrome://tab-search.top-chrome/tab_search.js';
@@ -844,4 +845,42 @@
     const [tabInfo] = await testProxy.whenCalled('switchToTab');
     assertEquals(1, tabInfo.tabId);
   });
+
+  test('Handles file URLs', async () => {
+    await setupTest(createProfileData({
+      windows: [{
+        active: true,
+        isHostWindow: true,
+        height: SAMPLE_WINDOW_HEIGHT,
+        tabs: [createTab({
+          title: 'My file',
+          url: {url: 'file:///home'},
+          lastActiveTimeTicks: {internalValue: BigInt(4)},
+        })],
+      }],
+    }));
+    const tabSearchItem =
+        tabSearchPage.$.tabsList.querySelector('tab-search-item')!;
+    assertEquals(
+        loadTimeData.getString('fileUrlSource'), tabSearchItem.data.hostname);
+  });
+
+  test('Handles blob URLs', async () => {
+    await setupTest(createProfileData({
+      windows: [{
+        active: true,
+        isHostWindow: true,
+        height: SAMPLE_WINDOW_HEIGHT,
+        tabs: [createTab({
+          title: 'My blob',
+          url: {url: 'blob:null/foo'},
+          lastActiveTimeTicks: {internalValue: BigInt(4)},
+        })],
+      }],
+    }));
+    const tabSearchItem =
+        tabSearchPage.$.tabsList.querySelector('tab-search-item')!;
+    assertEquals(
+        loadTimeData.getString('blobUrlSource'), tabSearchItem.data.hostname);
+  });
 });
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/test/data/webui/tab_search/tab_search_page_test.ts b/chrome/test/data/webui/tab_search/tab_search_page_test.ts
index b91f9ca..5183ed0 100644
--- a/chrome/test/data/webui/tab_search/tab_search_page_test.ts
+++ b/chrome/test/data/webui/tab_search/tab_search_page_test.ts
@@ -4,6 +4,7 @@
 
 import 'chrome://tab-search.top-chrome/tab_search.js';
 
+import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
 import {MetricsReporterImpl} from 'chrome://resources/js/metrics_reporter/metrics_reporter.js';
 import type {ProfileData, RecentlyClosedTab, Tab, TabSearchItemElement, TabSearchPageElement} from 'chrome://tab-search.top-chrome/tab_search.js';
 import {SEARCH_QUERY_MAX_LENGTH, TabGroupColor, TabSearchApiProxyImpl} from 'chrome://tab-search.top-chrome/tab_search.js';
@@ -844,4 +845,42 @@
     const [tabInfo] = await testProxy.whenCalled('switchToTab');
     assertEquals(1, tabInfo.tabId);
   });
+
+  test('Handles file URLs', async () => {
+    await setupTest(createProfileData({
+      windows: [{
+        active: true,
+        isHostWindow: true,
+        height: SAMPLE_WINDOW_HEIGHT,
+        tabs: [createTab({
+          title: 'My file',
+          url: {url: 'file:///home'},
+          lastActiveTimeTicks: {internalValue: BigInt(4)},
+        })],
+      }],
+    }));
+    const tabSearchItem =
+        tabSearchPage.$.tabsList.querySelector('tab-search-item')!;
+    assertEquals(
+        loadTimeData.getString('fileUrlSource'), tabSearchItem.data.hostname);
+  });
+
+  test('Handles blob URLs', async () => {
+    await setupTest(createProfileData({
+      windows: [{
+        active: true,
+        isHostWindow: true,
+        height: SAMPLE_WINDOW_HEIGHT,
+        tabs: [createTab({
+          title: 'My blob',
+          url: {url: 'blob:null/foo'},
+          lastActiveTimeTicks: {internalValue: BigInt(4)},
+        })],
+      }],
+    }));
+    const tabSearchItem =
+        tabSearchPage.$.tabsList.querySelector('tab-search-item')!;
+    assertEquals(
+        loadTimeData.getString('blobUrlSource'), tabSearchItem.data.hostname);
+  });
 });
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.