Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in WebAppInstalls
DescriptionMemory corruption in WebAppInstalls
ComponentWebAppInstalls
Sub-componentPWA
Bug ClassMemory Corruption
Tracker561396577
Fix commite17009278376 (chromium/src) +85/-6
CISA KEVNot listed
Creditedphamtien.ohr.s
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/views/location_bar/custom_tab_bar_view.cc
modified
for
chrome/browser/ui/web_applications/app_browser_controller_unittest.cc
modified

Files Changed

  • chrome/browser/ui/views/location_bar/custom_tab_bar_view.cc
  • chrome/browser/ui/web_applications/app_browser_controller_unittest.cc
  • chrome/browser/ui/views/location_bar/custom_tab_bar_view.h
  • chrome/browser/ui/views/location_bar/custom_tab_bar_view_browsertest.cc
  • chrome/browser/ui/web_applications/app_browser_controller.cc
From e17009278376b12705c8b095d11897482abeacac Mon Sep 17 00:00:00 2001
From: Adriana Ixba <[email protected]>
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 <[email protected]>
Reviewed-by: Daniel Murphy <[email protected]>
Reviewed-by: Kaan Alsan <[email protected]>
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)),
Loading diff…

Regression Test / PoC

shipped with the fix
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)),
Loading diff…

Original Bug Report

reported by [email protected]

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

  1. Unzip poc/ and serve it: python3 -m http.server 8076
  2. In Chrome, visit http://localhost:8076/ and install the PWA (address-bar icon or Menu → Install).
  3. In the PWA window, click “Trigger spoof”.
  4. Observe the CustomTabBarView (blue bar below title bar): the head-elided line ends with https://accounts.google.com/signin; the tooltip line shows about:blank#/ServiceLogin?continue=… — a Google-like URL that is actually attacker-crafted. See evidence_linux.png.

Root cause

Three facts combine (all present at 6a9e8b23):

  1. app_browser_controller.cc:264 — FormatUrlOrigin passes the raw URL to FormatUrl when origin.opaque() is true. kFormatUrlOmitDefaults does not include kFormatUrlOmitRef, so the attacker’s fragment payload survives formatting.

  2. 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.)

  3. custom_tab_bar_view.cc:61,369 — ShouldDisplayUrl has no scheme gate (only excludes interstitials), and the label is built from GetVisibleURL(). about:blank passes 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.
View on issue tracker