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:VeryLongString@evil.com, 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:VeryLongString@evil.com 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:VeryLongString@evil.com, 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 <nparker@chromium.org>
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 <ravjit@chromium.org>
Commit-Queue: Nathan Parker <nparker@chromium.org>
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:VeryLongString@evil.com");
+
+ 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:VeryLongString@evil.com");
+
+ 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 \