Moderate firefox Cross Origin 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionSame-origin policy bypass in the Address Bar component
ComponentCore
Bug ClassCross Origin
Tracker2075126
Fix commit7c610ba8039f (firefox) +40/-1
CISA KEVNot listed
CreditedTakashi Suzuki
Disclosed2026-10-27

Background

Identity block / "drag the URL"
The site-identity box left of the address bar (browser/base/content/browser-siteIdentity.js) can be dragged; on dragstart Firefox populates the drag DataTransfer with the current page’s URL both as text/plain and as a text/html anchor (<a href="...">...</a>), so dropping it into a rich editor yields a link.
`gBrowser.currentURI.displaySpec`
The displayed URL string of the current page, placed into both the plain-text and HTML drag payloads.
`text/html` drag payload
When dropped into an HTML context (another document/editor), the browser parses this string as markup. Any unescaped HTML metacharacters in the URL are interpreted as markup, not text.
`BrowserUtils.escapeHtmlEntities`
A helper that HTML-entity-encodes a string (e.g. &→&amp;, and other metacharacters) so it is safe to embed in HTML.

Root Cause Analysis

The dragstart handler built the text/html payload by string-concatenating the raw URL into an anchor: let htmlString = '<a href="' + value + '">' + value + "</a>";, where value = gBrowser.currentURI.displaySpec. The URL was inserted into both the href attribute and the link text with no HTML-entity encoding. A page can influence its own displayed URL (e.g. query string, fragment, path) to contain HTML-significant characters, so the generated text/html fragment could carry metacharacters that, when the drag data is dropped into an HTML context, are parsed as markup rather than as part of the URL — classic HTML injection via an unescaped interpolation (missing output encoding).

The patch encodes the URL before building the fragment: let escapedValue = BrowserUtils.escapeHtmlEntities(value); and then uses it in both positions via a template literal: `<a href="${escapedValue}">${escapedValue}</a>`. The new browser_identityBlock_drag.js test confirms the resulting text/html data is the ampersand-escaped anchor and that text/plain remains the raw URL. On classification: the advisory calls this a “Same-origin policy bypass in the Address Bar.” The diff does not show any SOP/origin-check change; it substantiates a missing-HTML-escaping (output-encoding) defect in the drag payload. The security consequence the advisory names — markup from one origin’s URL being injected into the document the user drops it onto (a different origin) — is a cross-origin content-injection effect that follows from that defect, but the code-level root cause is the unescaped interpolation. I classify from the diff (missing escaping) while noting the advisory’s cross-origin framing.

Key insight
The identity-block drag handler concatenated the page URL into an <a> HTML string without entity-encoding it, so a page-controlled URL could inject markup into the text/html drag payload.

Attack Path

  1. Craft URL Attacker’s page arranges its displayed URL (displaySpec) to contain HTML-significant characters in a path/query/fragment the page can control (inferred: page influence over its own URL).
  2. Induce drag The victim drags the site-identity block (social-engineered as “drag this link”), firing dragstart.
  3. Injected payload built (pre-patch) The handler produces <a href="<raw-url>"><raw-url></a> with the metacharacters unescaped, so the text/html payload contains attacker markup rather than a benign link.
  4. Drop into HTML context The victim drops the data into a rich-text/HTML editor or another document that parses text/html.
  5. Primitive Attacker-controlled HTML is injected into the drop target’s document (cross-origin content injection), the effect the advisory describes as an address-bar same-origin-policy bypass.

Impact Assessment

Front-end (chrome) drag-and-drop handling; no memory-safety impact and no code execution in Firefox itself. The substantiated defect is missing HTML-entity encoding of the page URL in the text/html drag payload, enabling injection of attacker markup into whatever document the user drops it into — a cross-origin content-injection / SOP-bypass effect per the advisory, rated “moderate.” Exploitation requires user interaction (dragging the identity block and dropping it into an HTML-accepting target) and a page-controlled URL; the strength of the resulting injection depends entirely on the drop target’s handling of text/html.

Changed Functions

FunctionChangeNotes
add_task
browser/base/content/test/browser-siteIdentity/browser_identityBlock_drag.js
modified

Files Changed

  • browser/base/content/browser-siteIdentity.js
  • browser/base/content/test/browser-siteIdentity/browser.toml
  • browser/base/content/test/browser-siteIdentity/browser_identityBlock_drag.js

Audit Directions

  • Other drag/clipboard `text/html` builders
    Audit other places that assemble text/html clipboard/drag payloads by concatenating URLs, titles, or user/page-controlled strings without escapeHtmlEntities.
  • URL-into-HTML interpolations in chrome JS
    Grep chrome JavaScript for '<a href="' + / template literals embedding currentURI/displaySpec or contentTitle directly into markup.
  • Output-encoding at the HTML boundary
    Review any feature that takes a page-influenced URL/title and later re-parses it as HTML (notifications, share sheets, bookmarks export) for the same missing-escaping pattern.
diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js
index 16b222280f8..1d331baef48 100644
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -1387,7 +1387,8 @@ var gIdentityHandler = {
 
     let value = gBrowser.currentURI.displaySpec;
     let urlString = value + "\n" + gBrowser.contentTitle;
-    let htmlString = '<a href="' + value + '">' + value + "</a>";
+    let escapedValue = BrowserUtils.escapeHtmlEntities(value);
+    let htmlString = `<a href="${escapedValue}">${escapedValue}</a>`;
 
     let scale = window.devicePixelRatio;
     let canvas = document.createElementNS(
diff --git a/browser/base/content/test/browser-siteIdentity/browser.toml b/browser/base/content/test/browser-siteIdentity/browser.toml
index f9b4a36d81f..2da91a44b71 100644
--- a/browser/base/content/test/browser-siteIdentity/browser.toml
+++ b/browser/base/content/test/browser-siteIdentity/browser.toml
@@ -41,6 +41,8 @@ support-files = [
 https_first_disabled = true
 support-files = ["dummy_iframe_page.html"]
 
+["browser_identityBlock_drag.js"]
+
 ["browser_identityBlock_flicker.js"]
 
 ["browser_identityBlock_focus.js"]
diff --git a/browser/base/content/test/browser-siteIdentity/browser_identityBlock_drag.js b/browser/base/content/test/browser-siteIdentity/browser_identityBlock_drag.js
new file mode 100644
index 00000000000..f3a3cb9ffe4
--- /dev/null
+++ b/browser/base/content/test/browser-siteIdentity/browser_identityBlock_drag.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL =
+  getRootDirectory(gTestPath).replace(
+    "chrome://mochitests/content",
+    "https://example.com"
+  ) + "dummy_page.html?a=1&b=2";
+
+add_task(async function test_drag_identity_box_data() {
+  await BrowserTestUtils.withNewTab(TEST_URL, async () => {
+    let url = gBrowser.currentURI.displaySpec;
+    Assert.ok(url.includes("&"), "The URL contains an ampersand");
+
+    let dataTransfer = new DataTransfer();
+    document
+      .getElementById("identity-box")
+      .dispatchEvent(
+        new DragEvent("dragstart", { dataTransfer, bubbles: true })
+      );
+
+    let escapedURL = url.replaceAll("&", "&amp;");
+    Assert.equal(
+      dataTransfer.getData("text/html"),
+      `<a href="${escapedURL}">${escapedURL}</a>`,
+      "Should set the expected text/html drag data"
+    );
+    Assert.equal(
+      dataTransfer.getData("text/plain"),
+      url,
+      "Should set the expected text/plain drag data"
+    );
+  });
+});
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/browser/base/content/test/browser-siteIdentity/browser.toml b/browser/base/content/test/browser-siteIdentity/browser.toml
index f9b4a36d81f..2da91a44b71 100644
--- a/browser/base/content/test/browser-siteIdentity/browser.toml
+++ b/browser/base/content/test/browser-siteIdentity/browser.toml
@@ -41,6 +41,8 @@ support-files = [
 https_first_disabled = true
 support-files = ["dummy_iframe_page.html"]
 
+["browser_identityBlock_drag.js"]
+
 ["browser_identityBlock_flicker.js"]
 
 ["browser_identityBlock_focus.js"]
diff --git a/browser/base/content/test/browser-siteIdentity/browser_identityBlock_drag.js b/browser/base/content/test/browser-siteIdentity/browser_identityBlock_drag.js
new file mode 100644
index 00000000000..f3a3cb9ffe4
--- /dev/null
+++ b/browser/base/content/test/browser-siteIdentity/browser_identityBlock_drag.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL =
+  getRootDirectory(gTestPath).replace(
+    "chrome://mochitests/content",
+    "https://example.com"
+  ) + "dummy_page.html?a=1&b=2";
+
+add_task(async function test_drag_identity_box_data() {
+  await BrowserTestUtils.withNewTab(TEST_URL, async () => {
+    let url = gBrowser.currentURI.displaySpec;
+    Assert.ok(url.includes("&"), "The URL contains an ampersand");
+
+    let dataTransfer = new DataTransfer();
+    document
+      .getElementById("identity-box")
+      .dispatchEvent(
+        new DragEvent("dragstart", { dataTransfer, bubbles: true })
+      );
+
+    let escapedURL = url.replaceAll("&", "&amp;");
+    Assert.equal(
+      dataTransfer.getData("text/html"),
+      `<a href="${escapedURL}">${escapedURL}</a>`,
+      "Should set the expected text/html drag data"
+    );
+    Assert.equal(
+      dataTransfer.getData("text/plain"),
+      url,
+      "Should set the expected text/plain drag data"
+    );
+  });
+});
Loading diff…