CVE-2026-117965
Overview
Background
- Identity block / "drag the URL"
- The site-identity box left of the address bar (
browser/base/content/browser-siteIdentity.js) can be dragged; ondragstartFirefox populates the dragDataTransferwith the current page’s URL both astext/plainand as atext/htmlanchor (<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.
&→&, 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.
<a> HTML string without entity-encoding it, so a page-controlled URL could inject markup into the text/html drag payload.Attack Path
- 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). - Induce drag
The victim drags the site-identity block (social-engineered as “drag this link”), firing
dragstart. - Injected payload built (pre-patch)
The handler produces
<a href="<raw-url>"><raw-url></a>with the metacharacters unescaped, so thetext/htmlpayload contains attacker markup rather than a benign link. - Drop into HTML context
The victim drops the data into a rich-text/HTML editor or another document that parses
text/html. - 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
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
| Function | Change | Notes |
|---|---|---|
add_taskbrowser/base/content/test/browser-siteIdentity/browser_identityBlock_drag.js |
modified |
Files Changed
browser/base/content/browser-siteIdentity.jsbrowser/base/content/test/browser-siteIdentity/browser.tomlbrowser/base/content/test/browser-siteIdentity/browser_identityBlock_drag.js
Audit Directions
- Other drag/clipboard `text/html` buildersAudit other places that assemble
text/htmlclipboard/drag payloads by concatenating URLs, titles, or user/page-controlled strings withoutescapeHtmlEntities. - URL-into-HTML interpolations in chrome JSGrep chrome JavaScript for
'<a href="' +/ template literals embeddingcurrentURI/displaySpecorcontentTitledirectly into markup. - Output-encoding at the HTML boundaryReview 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.
Patch
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("&", "&");
+ 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"
+ );
+ });
+});
Regression Test / PoC
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("&", "&");
+ 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"
+ );
+ });
+});