Low watch CVSS 6.1 chrome Spoofing 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
6.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in History Navigation
DescriptionIncorrect security UI in History Navigation
ComponentHistory Navigation
Bug ClassSpoofing
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker474817168
Fix commita8e095a03482 (chromium/src) +75/-10
CWECWE-346 (Origin validation error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
CISA KEVNot listed
CreditedIslam Rzayev
Reported2026-01-11 (per vendor advisory)
Fix landed2026-02-18 (public git)
Disclosed2026-04-07
Patch gap48 days — fix public in source before the advisory

Background

JavaScript dialog title
The “<origin> says” (or “This page says”) banner Chrome prepends to alert()/confirm()/prompt() dialogs so the user can tell which site is speaking.
Precursor origin
For an opaque origin (such as a data: URL document), the origin of the context that created it, which Chrome tracks so it can attribute behavior back to the initiator.
`data:` URL main frame
A top-level document whose URL is a data: URI; it has an opaque origin but may carry a precursor origin inherited from the page that opened it.
`util::DialogTitle`
The helper in dialog_util.cc that computes the dialog banner text from the main-frame and alerting-frame origins.

Root Cause Analysis

When a top-level data: URL document (for example one opened via a link with target="_blank") raised a JavaScript dialog, util::DialogTitle derived the banner from the main frame’s origin only. Because a data: document carries an opaque origin with a precursor, the code could resolve and display that precursor origin’s host in the banner, making the dialog appear to come from the HTTP site that spawned the tab rather than from the untrusted data: content. util::DialogTitle had no access to the actual committed URL, so it could not tell that the main frame was a non-standard data: document that should be shown generically.

The fix threads the main frame’s GetLastCommittedURL() down through AppModalDialogManager::GetSiteFrameTitle into util::DialogTitle, so that when the top-level document is a data: URL the function suppresses the precursor origin and falls back to the generic “This page says” text. The regression test TitleForNonHTTPOriginInMainFrame asserts exactly this: a data: URL new tab yields “This page says” while an HTTP tab yields “a.com:<port> says”.

Key insight
The dialog title was computed from origin alone, which for a data: main frame could surface an inherited precursor host and misattribute the dialog to a trusted site; the fix supplies the committed URL so the code can recognize the data: scheme and show a neutral title instead.

Attack Path

  1. Open an untrusted top-level `data:` document An attacker-controlled HTTP page opens a new tab/window pointing at a data: URL, so the new top-level document has an opaque origin whose precursor is the attacker’s (or any) spawning origin.
  2. Raise a JavaScript dialog Script in the data: document calls alert()/confirm()/prompt(), invoking AppModalDialogManager to build the dialog title.
  3. Misleading banner is shown Pre-patch, util::DialogTitle resolved the precursor origin and displayed it, so the dialog banner named a host the user might trust rather than disclosing that the content is an opaque data: page (spoofed security UI).
  4. Suppression after fix With the committed URL now passed in, a data: main frame produces the generic “This page says” banner, removing the misattribution.

Impact Assessment

An attacker gains incorrect security UI: a JavaScript dialog spawned by an opaque data: top-level document could be labeled with a precursor origin, leading the user to attribute attacker-chosen dialog text to a different, possibly trusted, site. This is reachable by ordinary web content in a stock build (opening a data: URL tab and calling alert()), requires no special flag or extension, and the vendor advisory rates it low severity; no memory-safety or escalation beyond UI spoofing is shown by the patch.

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

Audit Directions

  • Opaque-origin UI attribution
    Audit any security-relevant UI (dialogs, permission prompts, download/origin chips) that formats an origin for display; for opaque origins, check whether a precursor/initiator origin can leak into user-facing text and misattribute content.
  • `data:`/`blob:`/`filesystem:` top-level documents
    Review code paths that assume the main frame has a standard, displayable origin; non-standard schemes with inherited precursors are a recurring spoofing surface.
  • Origin-only vs. URL-aware decisions
    Flag helpers that make display or trust decisions from url::Origin alone where the committed GURL (scheme) is needed to distinguish genuinely standard origins from opaque ones.
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