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 <agale@chromium.org>
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 <agale@chromium.org>
Reviewed-by: Yuheng Huang <yuhengh@chromium.org>
Reviewed-by: Chris Thompson <cthomp@chromium.org>
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