Chrome · Split View
CVE-2025-13636
Logic Error in Split View
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/resources/tab_search/split_view/app.ts |
modified | |
ifchrome/browser/resources/tab_search/tab_data.ts |
modified | |
ifchrome/browser/resources/tab_search/tab_search_page.ts |
modified |
Files Changed
chrome/browser/resources/tab_search/split_view/app.tschrome/browser/resources/tab_search/tab_data.tschrome/browser/resources/tab_search/tab_search_page.tschrome/browser/ui/webui/tab_search/tab_search_page_handler.ccchrome/test/data/webui/tab_search/split_new_tab_page_test.ts
Patch
From e49ad13fa0e61b4b2acac9e524103325f10b0e21 Mon Sep 17 00:00:00 2001 From: Alison Gale <[email protected]> Date: Wed, 08 Oct 2025 16:20:46 -0700 Subject: [PATCH] [SxS] Handle uncommitted urls from split view and tab search For these URLs we display about:blank in the omnibox so this will be consistent. While I'm here I also shared the display URL logic so the split NTP will show when its a local file or something. Split NTP: https://screenshot.googleplex.com/BdMGxsXWM6BN3xp Tab search: https://screenshot.googleplex.com/7GCb8RuGuY7N49u Bug: 446181124 Change-Id: I42df102c3b3d4e51819bc89a3dc9549860178c16 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7017483 Commit-Queue: Alison Gale <[email protected]> Reviewed-by: Yuheng Huang <[email protected]> Reviewed-by: Chris Thompson <[email protected]> Cr-Commit-Position: refs/heads/main@{#1527218} --- diff --git a/chrome/browser/resources/tab_search/split_view/app.ts b/chrome/browser/resources/tab_search/split_view/app.ts index e86a8c4..1f2af3ac 100644 --- a/chrome/browser/resources/tab_search/split_view/app.ts +++ b/chrome/browser/resources/tab_search/split_view/app.ts @@ -11,7 +11,7 @@ import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js'; import type {SelectableLazyListElement} from '../selectable_lazy_list.js'; -import {normalizeURL, TabData, TabItemType} from '../tab_data.js'; +import {getDisplayHostnameForUrl, normalizeURL, TabData, TabItemType} from '../tab_data.js'; import type {ProfileData, Tab, TabsRemovedInfo, TabUpdateInfo} from '../tab_search.mojom-webui.js'; import type {TabSearchApiProxy} from '../tab_search_api_proxy.js'; import {TabSearchApiProxyImpl} from '../tab_search_api_proxy.js'; @@ -210,8 +210,9 @@ private getTabData_(tab: Tab, inActiveWindow: boolean, type: TabItemType): TabData { - const tabData = - new TabData(tab, type, new URL(normalizeURL(tab.url.url)).hostname); + const displayUrl = + getDisplayHostnameForUrl(new URL(normalizeURL(tab.url.url))); + const tabData = new TabData(tab, type, displayUrl); if (type === TabItemType.OPEN_TAB) { tabData.inActiveWindow = inActiveWindow; diff --git a/chrome/browser/resources/tab_search/tab_data.ts b/chrome/browser/resources/tab_search/tab_data.ts index 56f4a5d..5eb3d2e 100644 --- a/chrome/browser/resources/tab_search/tab_data.ts +++ b/chrome/browser/resources/tab_search/tab_data.ts @@ -118,6 +118,18 @@ return url || 'about:blank'; } +export function getDisplayHostnameForUrl(url: URL): string { + if (url.protocol === 'blob:') { + return loadTimeData.getString('blobUrlSource'); + } else if (url.protocol === 'file:') { + return loadTimeData.getString('fileUrlSource'); + } else if (url.protocol === 'about:' && url.pathname === 'blank') { + return 'about:blank'; + } else { + return url.hostname; + } +} + export function getTitle(data: TabData|TabGroupData): string|undefined { if (data.type === TabItemType.RECENTLY_CLOSED_TAB_GROUP) { return undefined; diff --git a/chrome/browser/resources/tab_search/tab_search_page.ts b/chrome/browser/resources/tab_search/tab_search_page.ts index 3dbd50776..c45a68c 100644 --- a/chrome/browser/resources/tab_search/tab_search_page.ts +++ b/chrome/browser/resources/tab_search/tab_search_page.ts @@ -26,7 +26,7 @@ import {search} from './search.js'; import type {SelectableLazyListElement} from './selectable_lazy_list.js'; import {NO_SELECTION, selectorNavigationKeys} from './selectable_lazy_list.js'; -import {ariaLabel, getHostname, getTabGroupTitle, getTitle, type ItemData, normalizeURL, TabData, TabGroupData, TabItemType, tokenEquals, tokenToString} from './tab_data.js'; +import {ariaLabel, getDisplayHostnameForUrl, getHostname, getTabGroupTitle, getTitle, type ItemData, normalizeURL, TabData, TabGroupData, TabItemType, tokenEquals, tokenToString} from './tab_data.js'; import type {ProfileData, RecentlyClosedTab, Tab, TabGroup, TabsRemovedInfo, TabUpdateInfo} from './tab_search.mojom-webui.js'; import {TabSearchSection} from './tab_search.mojom-webui.js'; import type {TabSearchApiProxy} from './tab_search_api_proxy.js'; @@ -661,22 +661,12 @@ 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, - this.getDisplayHostnameForUrl_(new URL(normalizeURL(tab.url.url)))); + 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_page_handler.cc b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc index 36955759..e4580632 100644 --- a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc +++ b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc @@ -1428,10 +1428,13 @@ // A visible URL is used when the a new tab is still loading. // If it is cancelled during loading the visible URL becomes empty. // We will display an empty URL as about:blank in Javascript. - tab_data->url = - !last_committed_url.is_valid() || last_committed_url.is_empty() - ? tab_renderer_data.visible_url - : last_committed_url; + if (!last_committed_url.is_valid() || last_committed_url.is_empty()) { + tab_data->url = tab_renderer_data.should_display_url + ? tab_renderer_data.visible_url + : GURL(url::kAboutBlankURL); + } else { + tab_data->url = last_committed_url; + } if (tab_renderer_data.favicon.IsEmpty()) { tab_data->is_default_favicon = true; diff --git a/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts b/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts index 2de65aa8..2ccc569 100644 --- a/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts +++ b/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts @@ -114,6 +114,49 @@ assertEquals(3, tabSearchItems.length); }); + test('Formats urls properly', async () => { + await splitNewTabPageSetup(); + + const windowData = createWindowData(); + windowData[0]!.tabs.push( + createTab({ + index: 6, + lastActiveTimeTicks: {internalValue: BigInt(10)}, + tabId: 8, + title: '', + url: {url: 'about:blank'}, + }), + createTab({ + index: 7, + lastActiveTimeTicks: {internalValue: BigInt(11)}, + tabId: 9, + title: 'file.jpg', + url: {url: 'file://file.jpg'}, + }), + createTab({ + index: 8, + lastActiveTimeTicks: {internalValue: BigInt(12)}, + tabId: 10, + title: 'Data', + url: {url: 'blob://data'}, + }), + ); + testApiProxy.getCallbackRouterRemote().tabsChanged(createProfileData({ + windows: windowData, + })); + await eventToPromise('viewport-filled', splitNewTabPage.$.splitTabsList); + + const tabSearchItems = + splitNewTabPage.shadowRoot.querySelectorAll('tab-search-item'); + assertEquals( + loadTimeData.getString('blobUrlSource'), + tabSearchItems[1]!.data.hostname); + assertEquals( + loadTimeData.getString('fileUrlSource'), + tabSearchItems[2]!.data.hostname); + assertEquals('about:blank', tabSearchItems[3]!.data.hostname); + }); + test('Sorts list', async () => { await splitNewTabPageSetup(); const tabSearchItems =
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts b/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts
index 2de65aa8..2ccc569 100644
--- a/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts
+++ b/chrome/test/data/webui/tab_search/split_new_tab_page_test.ts
@@ -114,6 +114,49 @@
assertEquals(3, tabSearchItems.length);
});
+ test('Formats urls properly', async () => {
+ await splitNewTabPageSetup();
+
+ const windowData = createWindowData();
+ windowData[0]!.tabs.push(
+ createTab({
+ index: 6,
+ lastActiveTimeTicks: {internalValue: BigInt(10)},
+ tabId: 8,
+ title: '',
+ url: {url: 'about:blank'},
+ }),
+ createTab({
+ index: 7,
+ lastActiveTimeTicks: {internalValue: BigInt(11)},
+ tabId: 9,
+ title: 'file.jpg',
+ url: {url: 'file://file.jpg'},
+ }),
+ createTab({
+ index: 8,
+ lastActiveTimeTicks: {internalValue: BigInt(12)},
+ tabId: 10,
+ title: 'Data',
+ url: {url: 'blob://data'},
+ }),
+ );
+ testApiProxy.getCallbackRouterRemote().tabsChanged(createProfileData({
+ windows: windowData,
+ }));
+ await eventToPromise('viewport-filled', splitNewTabPage.$.splitTabsList);
+
+ const tabSearchItems =
+ splitNewTabPage.shadowRoot.querySelectorAll('tab-search-item');
+ assertEquals(
+ loadTimeData.getString('blobUrlSource'),
+ tabSearchItems[1]!.data.hostname);
+ assertEquals(
+ loadTimeData.getString('fileUrlSource'),
+ tabSearchItems[2]!.data.hostname);
+ assertEquals('about:blank', tabSearchItems[3]!.data.hostname);
+ });
+
test('Sorts list', async () => {
await splitNewTabPageSetup();
const tabSearchItems =
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.
References
On This Page