CVE-2026-113138
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/location_bar/custom_tab_bar_view.cc |
modified | |
forchrome/browser/ui/web_applications/app_browser_controller_unittest.cc |
modified |
Files Changed
chrome/browser/ui/views/location_bar/custom_tab_bar_view.ccchrome/browser/ui/web_applications/app_browser_controller_unittest.ccchrome/browser/ui/views/location_bar/custom_tab_bar_view.hchrome/browser/ui/views/location_bar/custom_tab_bar_view_browsertest.ccchrome/browser/ui/web_applications/app_browser_controller.cc
Patch
From e17009278376b12705c8b095d11897482abeacac Mon Sep 17 00:00:00 2001
From: Adriana Ixba <aixba@chromium.org>
Date: Mon, 21 Sep 2026 19:22:24 -0700
Subject: [PATCH] [PWA] Fix CustomTabBarView origin spoof via opaque origins
When an installed Desktop PWA navigates to an opaque-origin URL (such as
about:blank#<padding>https://victim.com/login),
AppBrowserController::FormatUrlOrigin previously formatted the full raw
URL including fragment and query, and CustomTabBarTitleOriginView
unconditionally applied head-elision (ELIDE_HEAD). This caused the
leading about:blank prefix to be elided, leaving only the
attacker-controlled HTTPS origin visible in the trusted origin bar.
This CL fixes the issue by:
1. Updating AppBrowserController::FormatUrlOrigin() to return
'about:blank' and 'about:srcdoc' directly without query/fragment, and
clearing ref and query components when formatting other opaque
origins.
2. Updating CustomTabBarView to only use ELIDE_HEAD for non-opaque
HTTP/HTTPS origins where the registrable domain is at the tail, and
using ELIDE_TAIL for opaque or non-HTTP/HTTPS URLs.
3. Adding unit and browser test coverage.
Bug: 561396577
Change-Id: If3234027f1ee17ede3565a81015881fea9d2b2c6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8406574
Commit-Queue: Adriana Ixba <aixba@chromium.org>
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Reviewed-by: Kaan Alsan <alsan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1702321}
---
diff --git a/chrome/browser/ui/views/location_bar/custom_tab_bar_view.cc b/chrome/browser/ui/views/location_bar/custom_tab_bar_view.cc
index 35b4ef5..00689038 100644
--- a/chrome/browser/ui/views/location_bar/custom_tab_bar_view.cc
+++ b/chrome/browser/ui/views/location_bar/custom_tab_bar_view.cc
@@ -55,6 +55,7 @@
#include "ui/views/style/typography_provider.h"
#include "ui/views/view_class_properties.h"
#include "url/gurl.h"
+#include "url/origin.h"
namespace {
@@ -136,10 +137,13 @@
.SetCrossAxisAlignment(views::LayoutAlignment::kStart);
}
- void Update(const std::u16string title, const std::u16string location) {
+ void Update(const std::u16string title,
+ const std::u16string location,
+ gfx::ElideBehavior location_elide_behavior) {
if (title_label_) {
title_label_->SetText(title);
}
+ location_label_->SetElideBehavior(location_elide_behavior);
location_label_->SetText(location);
location_label_->SetVisible(!location.empty());
}
@@ -193,6 +197,10 @@
return location_label_ && location_label_->GetVisible();
}
+ gfx::ElideBehavior GetLocationElideBehaviorForTesting() const {
+ return location_label_->GetElideBehavior();
+ }
+
private:
// Can be nullptr.
raw_ptr<views::Label> title_label_ = nullptr;
@@ -373,12 +381,22 @@
title = WindowMetadataController::FormatTitleForDisplay(
entry->GetTitleForDisplay());
}
+ const GURL visible_url = contents->GetVisibleURL();
if (ShouldDisplayUrl(contents)) {
location = web_app::AppBrowserController::FormatUrlOrigin(
- contents->GetVisibleURL(), url_formatter::kFormatUrlOmitDefaults);
+ visible_url, url_formatter::kFormatUrlOmitDefaults);
}
- title_origin_view_->Update(title, location);
+ // Only head-elide hierarchical HTTP/HTTPS origins (where the registrable
+ // domain is at the end). For opaque origins or non-HTTP/HTTPS schemes, tail
+ // elision is required so that a crafted suffix cannot spoof an HTTPS origin.
+ const url::Origin origin = url::Origin::Create(visible_url);
+ const gfx::ElideBehavior location_elide_behavior =
+ (!origin.opaque() && origin.GetURL().SchemeIsHTTPOrHTTPS())
+ ? gfx::ElideBehavior::ELIDE_HEAD
+ : gfx::ElideBehavior::ELIDE_TAIL;
+
+ title_origin_view_->Update(title, location, location_elide_behavior);
location_icon_view_->Update(/*suppress animations = */ false);
// Hide location icon if we're already hiding the origin.
@@ -462,6 +480,11 @@
return close_button_->GetVisible();
}
+gfx::ElideBehavior CustomTabBarView::GetLocationElideBehaviorForTesting()
+ const {
+ return title_origin_view_->GetLocationElideBehaviorForTesting(); // IN-TEST
+}
+
void CustomTabBarView::GoBackToApp() {
content::WebContents* web_contents = GetWebContents();
content::NavigationController& controller = web_contents->GetController();
diff --git a/chrome/browser/ui/views/location_bar/custom_tab_bar_view.h b/chrome/browser/ui/views/location_bar/custom_tab_bar_view.h
index f0a534a..3019051 100644
--- a/chrome/browser/ui/views/location_bar/custom_tab_bar_view.h
+++ b/chrome/browser/ui/views/location_bar/custom_tab_bar_view.h
@@ -14,6 +14,7 @@
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/mojom/menu_source_type.mojom-forward.h"
+#include "ui/gfx/text_constants.h"
#include "ui/menus/simple_menu_model.h"
#include "ui/views/accessible_pane_view.h"
#include "ui/views/context_menu_controller.h"
@@ -106,6 +107,7 @@
void GoBackToAppForTesting();
bool IsShowingOriginForTesting() const;
bool IsShowingCloseButtonForTesting() const;
+ gfx::ElideBehavior GetLocationElideBehaviorForTesting() const;
private:
// Takes the web contents for the custom tab bar back to the app scope.
diff --git a/chrome/browser/ui/web_applications/app_browser_controller.cc b/chrome/browser/ui/web_applications/app_browser_controller.cc
index 97761517..0c8be26 100644
--- a/chrome/browser/ui/web_applications/app_browser_controller.cc
+++ b/chrome/browser/ui/web_applications/app_browser_controller.cc
@@ -260,10 +260,23 @@
std::u16string AppBrowserController::FormatUrlOrigin(
const GURL& url,
url_formatter::FormatUrlTypes format_types) {
+ if (url.IsAboutBlank()) {
+ return url::kAboutBlankURL16;
+ }
+ if (url.IsAboutSrcdoc()) {
+ return url::kAboutSrcdocURL16;
+ }
auto origin = url::Origin::Create(url);
- return url_formatter::FormatUrl(origin.opaque() ? url : origin.GetURL(),
- format_types, base::UnescapeRule::SPACES,
- nullptr, nullptr, nullptr);
+ GURL url_to_format = origin.GetURL();
+ if (origin.opaque()) {
+ GURL::Replacements replacements;
+ replacements.ClearRef();
+ replacements.ClearQuery();
+ url_to_format = url.ReplaceComponents(replacements);
+ }
+ return url_formatter::FormatUrl(url_to_format, format_types,
+ base::UnescapeRule::SPACES, nullptr, nullptr,
+ nullptr);
}
const ui::ThemeProvider* AppBrowserController::GetThemeProvider() const {
diff --git a/chrome/browser/ui/views/location_bar/custom_tab_bar_view_browsertest.cc b/chrome/browser/ui/views/location_bar/custom_tab_bar_view_browsertest.cc
index 202ed937..24e7e485 100644
--- a/chrome/browser/ui/views/location_bar/custom_tab_bar_view_browsertest.cc
+++ b/chrome/browser/ui/views/location_bar/custom_tab_bar_view_browsertest.cc
@@ -775,6 +775,40 @@
app_browser_view->toolbar()->custom_tab_bar()->location_for_testing() +
u"/",
base::ASCIIToUTF16(embedded_https_test_server().GetURL("/").spec()));
+ EXPECT_EQ(app_browser_view->toolbar()
+ ->custom_tab_bar()
+ ->GetLocationElideBehaviorForTesting(),
+ gfx::ElideBehavior::ELIDE_HEAD);
+}
+
+// Verify that navigating to an opaque-origin URL (like about:blank with a
+// crafted fragment) strips the fragment and uses tail elision rather than
+// head elision, preventing origin spoofing.
+IN_PROC_BROWSER_TEST_F(CustomTabBarViewBrowserTest,
+ AboutBlankWithFragmentDoesNotSpoofOrigin) {
+ InstallPWA(
+ embedded_https_test_server().GetURL("app.com", "/ssl/google.html"));
+ ASSERT_TRUE(app_browser_);
+ EXPECT_EQ(app_browser_->GetType(), BrowserWindowInterface::Type::TYPE_APP);
+
+ BrowserView* app_view = BrowserView::GetBrowserViewForBrowser(app_browser_);
+ content::WebContents* web_contents = app_view->GetActiveWebContents();
+ CustomTabBarView* bar = app_view->toolbar()->custom_tab_bar();
+
+ NavigateAndWait(web_contents,
+ GURL("about:blank#////////////////https://victim.com/login"));
+
+ EXPECT_TRUE(bar->GetVisible());
+ EXPECT_EQ(bar->location_for_testing(), u"about:blank");
+ EXPECT_EQ(bar->GetLocationElideBehaviorForTesting(),
+ gfx::ElideBehavior::ELIDE_TAIL);
+
+ // Navigating to a hierarchical HTTPS URL should use head elision.
+ NavigateAndWait(web_contents, GURL("https://example.com/path"));
+ EXPECT_TRUE(bar->GetVisible());
+ EXPECT_EQ(bar->location_for_testing(), u"https://example.com");
+ EXPECT_EQ(bar->GetLocationElideBehaviorForTesting(),
+ gfx::ElideBehavior::ELIDE_HEAD);
}
// Check that the CustomTabBarView security chip updates when the visible
diff --git a/chrome/browser/ui/web_applications/app_browser_controller_unittest.cc b/chrome/browser/ui/web_applications/app_browser_controller_unittest.cc
index 86a722b..54eb1fa 100644
--- a/chrome/browser/ui/web_applications/app_browser_controller_unittest.cc
+++ b/chrome/browser/ui/web_applications/app_browser_controller_unittest.cc
@@ -29,6 +29,13 @@
// comprehensive tests in
// components/url_formatter/url_formatter_unittest.cc.
{"https://xn--36c-tfa.com", L"xn--36c-tfa.com"},
+ // Opaque origins should not retain fragments or queries that could spoof
+ // an origin when head-elided.
+ {"about:blank", L"about:blank"},
+ {"about:blank#https://victim.com/login", L"about:blank"},
+ {"about:blank?https://victim.com/login", L"about:blank"},
+ {"about:srcdoc#https://victim.com/login", L"about:srcdoc"},
+ {"data:text/html,hello#https://victim.com", L"data:text/html,hello"},
};
for (auto test_case : kTestCases) {
EXPECT_EQ(AppBrowserController::FormatUrlOrigin(GURL(test_case.input)),
Regression Test / PoC
diff --git a/chrome/browser/ui/views/location_bar/custom_tab_bar_view_browsertest.cc b/chrome/browser/ui/views/location_bar/custom_tab_bar_view_browsertest.cc
index 202ed937..24e7e485 100644
--- a/chrome/browser/ui/views/location_bar/custom_tab_bar_view_browsertest.cc
+++ b/chrome/browser/ui/views/location_bar/custom_tab_bar_view_browsertest.cc
@@ -775,6 +775,40 @@
app_browser_view->toolbar()->custom_tab_bar()->location_for_testing() +
u"/",
base::ASCIIToUTF16(embedded_https_test_server().GetURL("/").spec()));
+ EXPECT_EQ(app_browser_view->toolbar()
+ ->custom_tab_bar()
+ ->GetLocationElideBehaviorForTesting(),
+ gfx::ElideBehavior::ELIDE_HEAD);
+}
+
+// Verify that navigating to an opaque-origin URL (like about:blank with a
+// crafted fragment) strips the fragment and uses tail elision rather than
+// head elision, preventing origin spoofing.
+IN_PROC_BROWSER_TEST_F(CustomTabBarViewBrowserTest,
+ AboutBlankWithFragmentDoesNotSpoofOrigin) {
+ InstallPWA(
+ embedded_https_test_server().GetURL("app.com", "/ssl/google.html"));
+ ASSERT_TRUE(app_browser_);
+ EXPECT_EQ(app_browser_->GetType(), BrowserWindowInterface::Type::TYPE_APP);
+
+ BrowserView* app_view = BrowserView::GetBrowserViewForBrowser(app_browser_);
+ content::WebContents* web_contents = app_view->GetActiveWebContents();
+ CustomTabBarView* bar = app_view->toolbar()->custom_tab_bar();
+
+ NavigateAndWait(web_contents,
+ GURL("about:blank#////////////////https://victim.com/login"));
+
+ EXPECT_TRUE(bar->GetVisible());
+ EXPECT_EQ(bar->location_for_testing(), u"about:blank");
+ EXPECT_EQ(bar->GetLocationElideBehaviorForTesting(),
+ gfx::ElideBehavior::ELIDE_TAIL);
+
+ // Navigating to a hierarchical HTTPS URL should use head elision.
+ NavigateAndWait(web_contents, GURL("https://example.com/path"));
+ EXPECT_TRUE(bar->GetVisible());
+ EXPECT_EQ(bar->location_for_testing(), u"https://example.com");
+ EXPECT_EQ(bar->GetLocationElideBehaviorForTesting(),
+ gfx::ElideBehavior::ELIDE_HEAD);
}
// Check that the CustomTabBarView security chip updates when the visible
diff --git a/chrome/browser/ui/web_applications/app_browser_controller_unittest.cc b/chrome/browser/ui/web_applications/app_browser_controller_unittest.cc
index 86a722b..54eb1fa 100644
--- a/chrome/browser/ui/web_applications/app_browser_controller_unittest.cc
+++ b/chrome/browser/ui/web_applications/app_browser_controller_unittest.cc
@@ -29,6 +29,13 @@
// comprehensive tests in
// components/url_formatter/url_formatter_unittest.cc.
{"https://xn--36c-tfa.com", L"xn--36c-tfa.com"},
+ // Opaque origins should not retain fragments or queries that could spoof
+ // an origin when head-elided.
+ {"about:blank", L"about:blank"},
+ {"about:blank#https://victim.com/login", L"about:blank"},
+ {"about:blank?https://victim.com/login", L"about:blank"},
+ {"about:srcdoc#https://victim.com/login", L"about:srcdoc"},
+ {"data:text/html,hello#https://victim.com", L"data:text/html,hello"},
};
for (auto test_case : kTestCases) {
EXPECT_EQ(AppBrowserController::FormatUrlOrigin(GURL(test_case.input)),
Original Bug Report
PWA CustomTabBarView origin spoof via opaque-origin raw-URL fallback + ELIDE_HEAD
PWA CustomTabBarView origin spoof via opaque-origin raw-URL fallback + ELIDE_HEAD
Component: chrome/browser/ui/web_applications/app_browser_controller.cc · chrome/browser/ui/views/location_bar/custom_tab_bar_view.cc
Tested: Chrome M155 trunk, 6a9e8b23 (2026-09-14). Reproduced on macOS + Linux.
Summary
AppBrowserController::FormatUrlOrigin formats the entire raw URL (including fragment) when the origin is opaque. CustomTabBarView head-elides this string unconditionally with no scheme gate. An installed PWA navigating to about:blank#<padding>https://victim.com/login causes the trusted origin bar to display …https://victim.com/login — an attacker-chosen HTTPS origin — while the page is actually about:blank. The security chip shows neutral (not DANGEROUS).
Desktop C++ twin of the Android Document-PiP spoof fixed in 544b1a332661a (bug 553145497). That fix was Android-scoped; this desktop path is still live.
Steps to reproduce
- Unzip
poc/and serve it:python3 -m http.server 8076 - In Chrome, visit
http://localhost:8076/and install the PWA (address-bar icon or Menu → Install). - In the PWA window, click “Trigger spoof”.
- Observe the CustomTabBarView (blue bar below title bar): the head-elided line ends with
https://accounts.google.com/signin; the tooltip line showsabout:blank#/ServiceLogin?continue=…— a Google-like URL that is actually attacker-crafted. Seeevidence_linux.png.
Root cause
Three facts combine (all present at 6a9e8b23):
-
app_browser_controller.cc:264—FormatUrlOriginpasses the raw URL toFormatUrlwhenorigin.opaque()is true.kFormatUrlOmitDefaultsdoes not includekFormatUrlOmitRef, so the attacker’s fragment payload survives formatting. -
custom_tab_bar_view.cc:111—SetElideBehavior(gfx::ELIDE_HEAD)unconditionally. Head-elide keeps the end of the string, which is the attacker’s fake origin. (Desktop Document-PiP header switches to ELIDE_TAIL for non-http schemes — this view doesn’t.) -
custom_tab_bar_view.cc:61,369—ShouldDisplayUrlhas no scheme gate (only excludes interstitials), and the label is built fromGetVisibleURL().about:blankpasses through.
Impact
The CustomTabBarView is the only origin indicator in an installed PWA window — there is no omnibox. A spoofed HTTPS origin in this bar, combined with a neutral security chip, gives the user no signal that they are on about:blank rather than the displayed site. The attacker controls the padding and target origin, so any HTTPS site (banks, Google, identity providers) can be impersonated. The navigation can be auto-triggered by JS after install, requiring no further user interaction. The PoC demonstrates padding that blends with Google OAuth URLs, making even the un-elided tooltip line appear legitimate.
Suggested fix
Gate opaque/non-http(s) URLs the way the Document-PiP header does: display a non-spoofable placeholder for opaque origins, or switch to ELIDE_TAIL and omit the fragment for non-http(s) schemes.
Artifacts
poc/— installable PWA (manifest.json, sw.js, index.html, icon.png)evidence_linux.png— PWA window on Linux (Chrome 151, Ubuntu 26.04) showing spoofed CustomTabBarView. Also reproduced on macOS.