CVE-2026-5899
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Pchrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc |
modified | |
GURLcomponents/javascript_dialogs/app_modal_dialog_manager.cc |
modified | |
GURLcomponents/javascript_dialogs/app_modal_dialog_manager.h |
modified | |
Origincomponents/javascript_dialogs/app_modal_dialog_manager.h |
modified |
Files Changed
chrome/browser/ui/javascript_dialogs/chrome_app_modal_dialog_manager_delegate.ccchrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cccomponents/javascript_dialogs/app_modal_dialog_manager.cccomponents/javascript_dialogs/app_modal_dialog_manager.hcomponents/javascript_dialogs/app_modal_dialog_manager_unittest.cccomponents/javascript_dialogs/core/dialog_util.cc
Patch
From a8e095a03482aaa9554da7835c8ccbb0f146bca4 Mon Sep 17 00:00:00 2001
From: Antonio Sartori <antoniosartori@chromium.org>
Date: Wed, 18 Feb 2026 02:11:40 -0800
Subject: [PATCH] Don't display precursor origin on javascript alerts with data: url
This CL tweaks the beheaviour of the util function computing the
message to be displayed on javascript alert. If the alerting top-level
document has a data: url, we don't display the precursor origin
anymore (even if we had one, which only applies in some situations).
Bug: 474817168
Change-Id: I0ee8d60d0c0bcd2ddd1bd158ff0aad344870b595
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7462512
Reviewed-by: Avi Drissman <avi@chromium.org>
Reviewed-by: Gauthier Ambard <gambard@chromium.org>
Commit-Queue: Antonio Sartori <antoniosartori@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1586300}
---
diff --git a/chrome/browser/ui/javascript_dialogs/chrome_app_modal_dialog_manager_delegate.cc b/chrome/browser/ui/javascript_dialogs/chrome_app_modal_dialog_manager_delegate.cc
index 16e3c449e..19e7abf8 100644
--- a/chrome/browser/ui/javascript_dialogs/chrome_app_modal_dialog_manager_delegate.cc
+++ b/chrome/browser/ui/javascript_dialogs/chrome_app_modal_dialog_manager_delegate.cc
@@ -45,5 +45,6 @@
}
return javascript_dialogs::AppModalDialogManager::GetSiteFrameTitle(
+ web_contents->GetPrimaryMainFrame()->GetLastCommittedURL(),
web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin(), origin);
}
diff --git a/chrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc b/chrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc
index bfdeab1..67bdb13 100644
--- a/chrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc
+++ b/chrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc
@@ -453,7 +453,8 @@
// Tests that the title for a dialog generated from a page with a non-HTTP URL
// that was spawned by an HTTP URL has that HTTP URL used for the title.
-IN_PROC_BROWSER_TEST_P(JavaScriptDialogOriginTest, TitleForNonHTTPOrigin) {
+IN_PROC_BROWSER_TEST_P(JavaScriptDialogOriginTest,
+ TitleForNonHTTPOriginInSubframe) {
GURL url = embedded_test_server()->GetURL("a.com", "/title1.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
content::WebContents* tab =
@@ -486,6 +487,49 @@
dialog_manager->GetTitle(tab, subframe->GetLastCommittedOrigin()));
}
+IN_PROC_BROWSER_TEST_P(JavaScriptDialogOriginTest,
+ TitleForNonHTTPOriginInMainFrame) {
+ GURL url = embedded_test_server()->GetURL("a.com", "/title1.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+ content::WebContents* tab =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Create a popup / new tab.
+ content::TestNavigationObserver opened_tab_observer(nullptr);
+ opened_tab_observer.StartWatchingNewWebContents();
+ GURL test_url(GetParam());
+ std::string script = content::JsReplace(R"(
+ let a = document.createElement("a");
+ a.href = $1;
+ a.target = "_blank";
+ a.id = "link";
+ a.textContent = "Open a new tab";
+ document.body.appendChild(a);)",
+ test_url);
+ ASSERT_TRUE(content::ExecJs(tab, script));
+ content::SimulateMouseClickOrTapElementWithId(tab, "link");
+ opened_tab_observer.Wait();
+ ASSERT_EQ(2, browser()->tab_strip_model()->count());
+
+ content::WebContents* opened_tab =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Wait until newly opened tab is fully loaded.
+ ASSERT_TRUE(WaitForLoadStop(opened_tab));
+
+ // Verify the title that would be used for a dialog spawned by the new tab.
+ javascript_dialogs::AppModalDialogManager* dialog_manager =
+ javascript_dialogs::AppModalDialogManager::GetInstance();
+ EXPECT_EQ(base::UTF8ToUTF16(
+ test_url.SchemeIs("data")
+ ? "This page says"
+ : base::StringPrintf("a.com:%d says",
+ embedded_test_server()->port())),
+ dialog_manager->GetTitle(
+ opened_tab,
+ opened_tab->GetPrimaryMainFrame()->GetLastCommittedOrigin()));
+}
+
IN_PROC_BROWSER_TEST_F(JavaScriptDialogTest,
HandlesSwappingTabWithDialogIntoSplitView) {
// Create three tabs with the first two in a split view.
diff --git a/components/javascript_dialogs/app_modal_dialog_manager.cc b/components/javascript_dialogs/app_modal_dialog_manager.cc
index 9462609..f956412 100644
--- a/components/javascript_dialogs/app_modal_dialog_manager.cc
+++ b/components/javascript_dialogs/app_modal_dialog_manager.cc
@@ -22,6 +22,8 @@
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/font_list.h"
+class GURL;
+
namespace javascript_dialogs {
namespace {
@@ -82,15 +84,18 @@
// Otherwise, return the formatted URL.
return GetSiteFrameTitle(
+ web_contents->GetPrimaryMainFrame()->GetLastCommittedURL(),
web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin(),
alerting_frame_origin);
}
// static
std::u16string AppModalDialogManager::GetSiteFrameTitle(
+ const GURL& main_frame_url,
const url::Origin& main_frame_origin,
const url::Origin& alerting_frame_origin) {
- return util::DialogTitle(main_frame_origin, alerting_frame_origin);
+ return util::DialogTitle(main_frame_url, main_frame_origin,
+ alerting_frame_origin);
}
void AppModalDialogManager::RunJavaScriptDialog(
diff --git a/components/javascript_dialogs/app_modal_dialog_manager.h b/components/javascript_dialogs/app_modal_dialog_manager.h
index d9e3e97..bf5520d 100644
--- a/components/javascript_dialogs/app_modal_dialog_manager.h
+++ b/components/javascript_dialogs/app_modal_dialog_manager.h
@@ -14,6 +14,8 @@
#include "components/javascript_dialogs/app_modal_dialog_manager_delegate.h"
#include "content/public/browser/javascript_dialog_manager.h"
+class GURL;
+
namespace url {
class Origin;
}
@@ -81,6 +83,7 @@
bool reset_state) override;
static std::u16string GetSiteFrameTitle(
+ const GURL& main_frame_url,
const url::Origin& main_frame_origin,
const url::Origin& alerting_frame_origin);
diff --git a/components/javascript_dialogs/app_modal_dialog_manager_unittest.cc b/components/javascript_dialogs/app_modal_dialog_manager_unittest.cc
index d758c52..723343e9 100644
--- a/components/javascript_dialogs/app_modal_dialog_manager_unittest.cc
+++ b/components/javascript_dialogs/app_modal_dialog_manager_unittest.cc
@@ -47,8 +47,7 @@
"An embedded page on this page says"},
// data:
- // /!\ NOTE that this is for data URLs entered directly in the omnibox.
- // For pages that generate frames with data URLs, see the browsertest.
+ // See also the javascript_dialog_browsertest.
// - main frame:
{"data main frame", "data:blahblah", true, "", "This page says"},
// - subframe:
@@ -104,7 +103,8 @@
? main_frame_origin
: url::Origin::Create(GURL(test_case.alerting_frame_url));
std::u16string result = AppModalDialogManager::GetSiteFrameTitle(
- main_frame_origin, alerting_frame_origin);
+ GURL(test_case.main_frame_url), main_frame_origin,
+ alerting_frame_origin);
EXPECT_EQ(test_case.expected, base::UTF16ToUTF8(result));
}
}
diff --git a/components/javascript_dialogs/core/dialog_util.cc b/components/javascript_dialogs/core/dialog_util.cc
index a4d69330..23136f92 100644
--- a/components/javascript_dialogs/core/dialog_util.cc
+++ b/components/javascript_dialogs/core/dialog_util.cc
@@ -11,6 +11,7 @@
#include "url/gurl.h"
#include "url/origin.h"
#include "url/scheme_host_port.h"
+#include "url/url_constants.h"
namespace javascript_dialogs::util {
@@ -33,7 +34,8 @@
precursor.scheme(), precursor.host(), precursor.port());
}
-std::u16string DialogTitle(const url::Origin& main_frame_origin,
+std::u16string DialogTitle(const GURL& main_frame_url,
+ const url::Origin& main_frame_origin,
const url::Origin& alerting_frame_origin) {
// Note that `Origin::Create()` handles unwrapping of `blob:` and
// `filesystem:` schemed URLs, so no special handling is needed for that.
@@ -47,7 +49,12 @@
bool is_same_origin_as_main_frame =
unwrapped_alerting_frame_origin.IsSameOriginWith(
unwrapped_main_frame_origin);
- if (unwrapped_alerting_frame_origin.GetURL().IsStandard() &&
+
Regression Test / PoC
diff --git a/chrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc b/chrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc
index bfdeab1..67bdb13 100644
--- a/chrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc
+++ b/chrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc
@@ -453,7 +453,8 @@
// Tests that the title for a dialog generated from a page with a non-HTTP URL
// that was spawned by an HTTP URL has that HTTP URL used for the title.
-IN_PROC_BROWSER_TEST_P(JavaScriptDialogOriginTest, TitleForNonHTTPOrigin) {
+IN_PROC_BROWSER_TEST_P(JavaScriptDialogOriginTest,
+ TitleForNonHTTPOriginInSubframe) {
GURL url = embedded_test_server()->GetURL("a.com", "/title1.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
content::WebContents* tab =
@@ -486,6 +487,49 @@
dialog_manager->GetTitle(tab, subframe->GetLastCommittedOrigin()));
}
+IN_PROC_BROWSER_TEST_P(JavaScriptDialogOriginTest,
+ TitleForNonHTTPOriginInMainFrame) {
+ GURL url = embedded_test_server()->GetURL("a.com", "/title1.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+ content::WebContents* tab =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Create a popup / new tab.
+ content::TestNavigationObserver opened_tab_observer(nullptr);
+ opened_tab_observer.StartWatchingNewWebContents();
+ GURL test_url(GetParam());
+ std::string script = content::JsReplace(R"(
+ let a = document.createElement("a");
+ a.href = $1;
+ a.target = "_blank";
+ a.id = "link";
+ a.textContent = "Open a new tab";
+ document.body.appendChild(a);)",
+ test_url);
+ ASSERT_TRUE(content::ExecJs(tab, script));
+ content::SimulateMouseClickOrTapElementWithId(tab, "link");
+ opened_tab_observer.Wait();
+ ASSERT_EQ(2, browser()->tab_strip_model()->count());
+
+ content::WebContents* opened_tab =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ // Wait until newly opened tab is fully loaded.
+ ASSERT_TRUE(WaitForLoadStop(opened_tab));
+
+ // Verify the title that would be used for a dialog spawned by the new tab.
+ javascript_dialogs::AppModalDialogManager* dialog_manager =
+ javascript_dialogs::AppModalDialogManager::GetInstance();
+ EXPECT_EQ(base::UTF8ToUTF16(
+ test_url.SchemeIs("data")
+ ? "This page says"
+ : base::StringPrintf("a.com:%d says",
+ embedded_test_server()->port())),
+ dialog_manager->GetTitle(
+ opened_tab,
+ opened_tab->GetPrimaryMainFrame()->GetLastCommittedOrigin()));
+}
+
IN_PROC_BROWSER_TEST_F(JavaScriptDialogTest,
HandlesSwappingTabWithDialogIntoSplitView) {
// Create three tabs with the first two in a split view.
diff --git a/components/javascript_dialogs/app_modal_dialog_manager_unittest.cc b/components/javascript_dialogs/app_modal_dialog_manager_unittest.cc
index d758c52..723343e9 100644
--- a/components/javascript_dialogs/app_modal_dialog_manager_unittest.cc
+++ b/components/javascript_dialogs/app_modal_dialog_manager_unittest.cc
@@ -47,8 +47,7 @@
"An embedded page on this page says"},
// data:
- // /!\ NOTE that this is for data URLs entered directly in the omnibox.
- // For pages that generate frames with data URLs, see the browsertest.
+ // See also the javascript_dialog_browsertest.
// - main frame:
{"data main frame", "data:blahblah", true, "", "This page says"},
// - subframe:
@@ -104,7 +103,8 @@
? main_frame_origin
: url::Origin::Create(GURL(test_case.alerting_frame_url));
std::u16string result = AppModalDialogManager::GetSiteFrameTitle(
- main_frame_origin, alerting_frame_origin);
+ GURL(test_case.main_frame_url), main_frame_origin,
+ alerting_frame_origin);
EXPECT_EQ(test_case.expected, base::UTF16ToUTF8(result));
}
}
Original Bug Report
Unviersal CSP Bypass/XSS & Privileged chrome:// page XSS via Browser History Sidebar navigation
Steps to reproduce the problem
Data URIs containing JavaScript payloads bypass Content Security Policy when reopened from Chrome’s history sidebar, leading to XSS on the original origin. Additionally, this can be escalated to execute JavaScript in the privileged chrome://history/ context.
Problem Description
Affected
Chrome/Chromium, Edge, and Chromium-based browsers.
Vulnerability Details
Bug #1: CSP Bypass leading to XSS
Any page with a strict CSP that allows data: in href or img-src is vulnerable to XSS:
- User right-clicks data URI link/image → Open in new tab
- CSP blocks script execution (expected)
- User closes the data URI tab
- User opens history sidebar (three dots → History)
- User clicks data URI entry from sidebar
- Script executes in original origin context, CSP bypassed, XSS achieved
Bug #2: Privileged Context XSS
- Open
chrome://history/(Ctrl+H) - Right-click data URI entry → Open in new tab
- CSP blocks (expected), close tab
- Open history sidebar, click same data URI
- Script executes under
chrome://history/context
Payloads
<a href=“data:text/html,<script>alert(‘XSS’)</script>” target="_blank">Click</a>
<img src=“data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20onload%3D%27alert(1)%27%2F%3E”>
Root Cause
Data URIs reopened from history sidebar lose their CSP association, allowing script execution without policy enforcement while retaining the original origin context.
Impact
- XSS on any origin allowing data URIs in href/src
- Complete CSP bypass regardless of policy strictness
- Privileged code execution in chrome:// context
- Potential access to sensitive browser APIs
Note
Even though data URIs are somewhat more restricted from accessing origin context, this bypass demonstrates a serious gap in CSP enforcement through the history sidebar. The ability to execute arbitrary JavaScript in both the original origin and privileged chrome:// contexts undermines the security guarantees that CSP is designed to provide.
Severity
High/Critical
Summary
Unviersal CSP Bypass/XSS & Privileged chrome:// page XSS via Browser History Sidebar navigation
Custom Questions
Reporter credit:
Islam Rzayev
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A \