CVE-2026-5905
Overview
Background
- Pop-up blocked bubble
- the content-settings UI (
ContentSettingBubbleModelforContentSettingsType::POPUPS) that lists URLs Chrome prevented a page from opening in a new window. - URL userinfo component
- the optional
username:password@segment permitted before the host in a URL, e.g.http://google.com:[email protected], where the real host isevil.com. - `GURL::spec()`
- returns the full canonical URL string including the userinfo component, with no elision or security-oriented formatting.
- `url_formatter::FormatUrl`
- the display-formatting helper that applies flags such as
kFormatUrlOmitUsernamePasswordto produce a human-facing, non-spoofable URL string.
Root Cause Analysis
The helper that builds each blocked-popup list-item title (enclosing function name is not shown in the hunk; the @@ context only exposes its const GURL& url parameter and an “Empty URLs should get a placeholder” comment) formed the title directly from url.spec(). Because GURL::spec() emits the raw canonical URL including any username:password@ userinfo, an attacker-controlled popup target such as http://google.com:[email protected] produced a title beginning with google.com, visually implying the popup came from google.com when the actual host is evil.com. The displayed string thus violated the invariant that security-facing UI must reflect the true origin/host, not attacker-supplied userinfo.
The fix replaces base::UTF8ToUTF16(url.spec()) with url_formatter::FormatUrl(url, url_formatter::kFormatUrlOmitUsernamePassword, ...), which strips the userinfo so the displayed title shows the genuine host. The shipped regression test confirms the intended behavior: after the fix the title must not contain google.com and must contain evil.com.
GURL::spec() string in security UI, letting the username:password@ userinfo masquerade as the host; the fix routes the URL through url_formatter::FormatUrl with kFormatUrlOmitUsernamePassword so the userinfo is omitted before display.Attack Path
- Host a page that triggers a blocked popup
The attacker serves a page that calls
window.openwith a crafted target, which Chrome’s pop-up blocker intercepts and records. - Embed a spoofing userinfo in the popup URL
The target is
http://google.com:[email protected], placing a trusted-looking brand in theusername:password@userinfo whileevil.comis the real host. - User opens the "Pop-ups blocked" bubble
The blocked-item title is built from
url.spec(), so it renders beginning withgoogle.com, hiding the true destination. - User is misled about the popup source
Believing the blocked popup belongs to
google.com, the user may choose to allow/open it, navigating to attacker-controlledevil.com.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fchrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc |
modified |
Files Changed
chrome/browser/ui/content_settings/content_setting_bubble_model.ccchrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
Audit Directions
- Raw `spec()`/`GetContent()` in UIAudit any security- or user-facing surface (bubbles, dialogs, notifications, permission prompts) that builds display strings from
GURL::spec(),host()alone, or unformatted URLs instead ofurl_formatter::FormatUrl/FormatUrlForSecurityDisplay. - Userinfo-bearing URLsLook for display paths that do not set
kFormatUrlOmitUsernamePassword, sinceusername:password@hostis a classic vector for host spoofing in any truncated or brand-prominent label. - Content-settings and blocked-content listsReview other
ContentSettingBubbleModellist-item and subtitle builders (not only popups) for the same raw-spec pattern.
Patch
From 848a7a9f1feeba1debc2abfc9b9b1b38ec2b28d1 Mon Sep 17 00:00:00 2001 From: Nathan Parker <[email protected]> Date: Thu, 19 Feb 2026 08:18:27 -0800 Subject: [PATCH] Fix URL spoofing on "Pop-ups blocked" dialog in Chrome. Switched to FormatUrl() which removes the username:password@ component. Example of new formatting, with hover: http://screen/M4wV4PuQ4vu2zDr Bug: 483899628 Change-Id: Iff473a1718f7f4662abddbeff29223973c7de845 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7577181 Reviewed-by: Ravjit Uppal <[email protected]> Commit-Queue: Nathan Parker <[email protected]> Cr-Commit-Position: refs/heads/main@{#1587161} --- diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model.cc index 306b3d8..45fc4ca6 100644 --- a/chrome/browser/ui/content_settings/content_setting_bubble_model.cc +++ b/chrome/browser/ui/content_settings/content_setting_bubble_model.cc @@ -68,6 +68,7 @@ #include "components/subresource_filter/core/browser/subresource_filter_constants.h" #include "components/subresource_filter/core/browser/subresource_filter_features.h" #include "components/url_formatter/elide_url.h" +#include "components/url_formatter/url_formatter.h" #include "components/vector_icons/vector_icons.h" #include "content/public/browser/page.h" #include "content/public/browser/permission_controller.h" @@ -171,10 +172,12 @@ const GURL& url) { // Empty URLs should get a placeholder. // TODO(csharrison): See if we can DCHECK that the URL will be valid here. - std::u16string title = url.spec().empty() - ? l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) - : base::UTF8ToUTF16(url.spec()); - + std::u16string title = + url.spec().empty() + ? l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) + : url_formatter::FormatUrl( + url, url_formatter::kFormatUrlOmitUsernamePassword, + base::UnescapeRule::NONE, nullptr, nullptr, nullptr); // Format the title to include the unicode single dot bullet code-point // \u2022 and two spaces. title = l10n_util::GetStringFUTF16(IDS_LIST_BULLET, title); diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc index 10f443bb..ae12051 100644 --- a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc +++ b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc @@ -1391,6 +1391,44 @@ } } +TEST_F(ContentSettingBubbleModelTest, PopupBubbleModelListItemsSpoofing) { + const GURL url("https://www.example.test/"); + WebContentsTester::For(web_contents())->NavigateAndCommit(url); + blocked_content::PopupBlockerTabHelper::CreateForWebContents(web_contents()); + + // Malicious URL + GURL spoof_url("http://google.com:[email protected]"); + + BlockedWindowParams params(spoof_url, url::Origin(), nullptr, + content::Referrer(), std::string(), + WindowOpenDisposition::NEW_POPUP, + blink::mojom::WindowFeatures(), false, true); + + NavigateParams navigate_params = + params.CreateNavigateParams(process(), web_contents()); + blocked_content::MaybeBlockPopup( + web_contents(), &url, + std::make_unique<ChromePopupNavigationDelegate>( + std::move(navigate_params)), + nullptr, params.features(), + HostContentSettingsMapFactory::GetForProfile(profile())); + + std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model( + ContentSettingBubbleModel::CreateContentSettingBubbleModel( + nullptr, web_contents(), ContentSettingsType::POPUPS)); + + const auto& list_items = + content_setting_bubble_model->bubble_content().list_items; + ASSERT_EQ(1U, list_items.size()); + + // Check the title + std::u16string title = list_items[0].title; + // It should NOT contain "google.com" if it's formatted correctly for + // security. + EXPECT_EQ(std::u16string::npos, title.find(u"google.com")); + EXPECT_NE(std::u16string::npos, title.find(u"evil.com")); +} + TEST_F(ContentSettingBubbleModelTest, ValidUrl) { WebContentsTester::For(web_contents()) ->NavigateAndCommit(GURL("https://www.example.com"));
Regression Test / PoC
diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
index 10f443bb..ae12051 100644
--- a/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
+++ b/chrome/browser/ui/content_settings/content_setting_bubble_model_unittest.cc
@@ -1391,6 +1391,44 @@
}
}
+TEST_F(ContentSettingBubbleModelTest, PopupBubbleModelListItemsSpoofing) {
+ const GURL url("https://www.example.test/");
+ WebContentsTester::For(web_contents())->NavigateAndCommit(url);
+ blocked_content::PopupBlockerTabHelper::CreateForWebContents(web_contents());
+
+ // Malicious URL
+ GURL spoof_url("http://google.com:[email protected]");
+
+ BlockedWindowParams params(spoof_url, url::Origin(), nullptr,
+ content::Referrer(), std::string(),
+ WindowOpenDisposition::NEW_POPUP,
+ blink::mojom::WindowFeatures(), false, true);
+
+ NavigateParams navigate_params =
+ params.CreateNavigateParams(process(), web_contents());
+ blocked_content::MaybeBlockPopup(
+ web_contents(), &url,
+ std::make_unique<ChromePopupNavigationDelegate>(
+ std::move(navigate_params)),
+ nullptr, params.features(),
+ HostContentSettingsMapFactory::GetForProfile(profile()));
+
+ std::unique_ptr<ContentSettingBubbleModel> content_setting_bubble_model(
+ ContentSettingBubbleModel::CreateContentSettingBubbleModel(
+ nullptr, web_contents(), ContentSettingsType::POPUPS));
+
+ const auto& list_items =
+ content_setting_bubble_model->bubble_content().list_items;
+ ASSERT_EQ(1U, list_items.size());
+
+ // Check the title
+ std::u16string title = list_items[0].title;
+ // It should NOT contain "google.com" if it's formatted correctly for
+ // security.
+ EXPECT_EQ(std::u16string::npos, title.find(u"google.com"));
+ EXPECT_NE(std::u16string::npos, title.find(u"evil.com"));
+}
+
TEST_F(ContentSettingBubbleModelTest, ValidUrl) {
WebContentsTester::For(web_contents())
->NavigateAndCommit(GURL("https://www.example.com"));
Original Bug Report
URL Spoofing on Block or allow pop-ups in Chrome
Steps to reproduce the problem
- Save this 2 HTML files (orig.html and fake.html) and analyze the link in the popup message, it looks the same
- Allow the popup
- The victim redirected to evil.com instead of google.com
Problem Description
When using window.open, Chrome shows a popup listing temporarily blocked URLs and waits for the user’s approval. If a suspicious URL is detected, the user can choose to keep it blocked. However, in this popup Chrome displays only the URL’s prefix and suffix. By inserting a malicious URL in the middle using @, an attacker can disguise it. If the user allows the popup, instead of opening a safe website, Chrome will open the malicious URL.
Summary
URL Spoofing on Block or allow pop-ups in Chrome
Additional Data
Category: Security
Chrome Channel: Not sure
Regression: N/A \