Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in History Navigation
DescriptionIncorrect security UI in History Navigation
ComponentHistory Navigation
Bug ClassLogic Error
Tracker474817168
Fix commita8e095a03482 (chromium/src) +75/-10
CISA KEVNot listed
CreditedIslam Rzayev
Disclosed2026-04-07

Changed Functions

FunctionChangeNotes
IN_PROC_BROWSER_TEST_P
chrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc
modified
GURL
components/javascript_dialogs/app_modal_dialog_manager.cc
modified
GURL
components/javascript_dialogs/app_modal_dialog_manager.h
modified
Origin
components/javascript_dialogs/app_modal_dialog_manager.h
modified

Files Changed

  • chrome/browser/ui/javascript_dialogs/chrome_app_modal_dialog_manager_delegate.cc
  • chrome/browser/ui/javascript_dialogs/javascript_dialog_browsertest.cc
  • components/javascript_dialogs/app_modal_dialog_manager.cc
  • components/javascript_dialogs/app_modal_dialog_manager.h
  • components/javascript_dialogs/app_modal_dialog_manager_unittest.cc
  • components/javascript_dialogs/core/dialog_util.cc
From a8e095a03482aaa9554da7835c8ccbb0f146bca4 Mon Sep 17 00:00:00 2001
From: Antonio Sartori <[email protected]>
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 <[email protected]>
Reviewed-by: Gauthier Ambard <[email protected]>
Commit-Queue: Antonio Sartori <[email protected]>
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() &&
+
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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:

  1. User right-clicks data URI link/image → Open in new tab
  2. CSP blocks script execution (expected)
  3. User closes the data URI tab
  4. User opens history sidebar (three dots → History)
  5. User clicks data URI entry from sidebar
  6. Script executes in original origin context, CSP bypassed, XSS achieved

Bug #2: Privileged Context XSS

  1. Open chrome://history/ (Ctrl+H)
  2. Right-click data URI entry → Open in new tab
  3. CSP blocks (expected), close tab
  4. Open history sidebar, click same data URI
  5. 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 \

View on issue tracker