Moderate firefox Logic Error 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionPrivilege escalation in the Address Bar component
ComponentToolkit
Bug ClassLogic Error
Tracker2071645
Fix commitee0057543d74 (firefox) +98/-48
CISA KEVNot listed
CreditedTran Quac
Disclosed2026-09-29

Changed Functions

FunctionChangeNotes
escapeHtmlEntities
browser/components/urlbar/content/UrlbarShared.mjs
modified
if
toolkit/components/places/BookmarkHTMLUtils.sys.mjs
modified
_writeDateAttributes
toolkit/components/places/BookmarkHTMLUtils.sys.mjs
modified
escapeHtmlEntities
toolkit/components/places/PlacesUtils.sys.mjs
modified
add_task
browser/components/urlbar/tests/browser/browser_dragdropURL.js
modified

Files Changed

  • browser/components/urlbar/content/SmartbarInput.mjs
  • browser/components/urlbar/content/UrlbarInputBase.mjs
  • browser/components/urlbar/content/UrlbarShared.mjs
  • browser/components/urlbar/tests/browser/browser_dragdropURL.js
  • toolkit/components/places/BookmarkHTMLUtils.sys.mjs
  • toolkit/components/places/PlacesUtils.sys.mjs
diff --git a/browser/components/urlbar/content/SmartbarInput.mjs b/browser/components/urlbar/content/SmartbarInput.mjs
index 44b94f65f72..9c0875bb7bd 100644
--- a/browser/components/urlbar/content/SmartbarInput.mjs
+++ b/browser/components/urlbar/content/SmartbarInput.mjs
@@ -7083,7 +7083,10 @@ ${
 
     event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`);
     event.dataTransfer.setData("text/plain", href);
-    event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`);
+    event.dataTransfer.setData(
+      "text/html",
+      `<a href="${UrlbarShared.escapeHtmlEntities(href)}">${UrlbarShared.escapeHtmlEntities(title)}</a>`
+    );
     event.dataTransfer.effectAllowed = "copyLink";
     event.stopPropagation();
   }
diff --git a/browser/components/urlbar/content/UrlbarInputBase.mjs b/browser/components/urlbar/content/UrlbarInputBase.mjs
index 71fd8de3e91..4d71f37d9ef 100644
--- a/browser/components/urlbar/content/UrlbarInputBase.mjs
+++ b/browser/components/urlbar/content/UrlbarInputBase.mjs
@@ -6077,7 +6077,10 @@ ${
 
     event.dataTransfer.setData("text/x-moz-url", `${href}\n${title}`);
     event.dataTransfer.setData("text/plain", href);
-    event.dataTransfer.setData("text/html", `<a href="${href}">${title}</a>`);
+    event.dataTransfer.setData(
+      "text/html",
+      `<a href="${UrlbarShared.escapeHtmlEntities(href)}">${UrlbarShared.escapeHtmlEntities(title)}</a>`
+    );
     event.dataTransfer.effectAllowed = "copyLink";
     event.stopPropagation();
   }
diff --git a/browser/components/urlbar/content/UrlbarShared.mjs b/browser/components/urlbar/content/UrlbarShared.mjs
index 6d03b9a0b28..d2e3acf7c6e 100644
--- a/browser/components/urlbar/content/UrlbarShared.mjs
+++ b/browser/components/urlbar/content/UrlbarShared.mjs
@@ -1816,6 +1816,21 @@ export const UrlbarShared = {
     }
     return this.__firstDayOfWeek;
   },
+
+  /**
+   * Escapes a string for safe interpolation into an HTML document.
+   *
+   * @param {string} s
+   * @returns {string}
+   */
+  escapeHtmlEntities(s) {
+    return (s || "")
+      .replace(/&/g, "&amp;")
+      .replace(/</g, "&lt;")
+      .replace(/>/g, "&gt;")
+      .replace(/"/g, "&quot;")
+      .replace(/'/g, "&#39;");
+  },
 };
 
 /**
diff --git a/toolkit/components/places/BookmarkHTMLUtils.sys.mjs b/toolkit/components/places/BookmarkHTMLUtils.sys.mjs
index fc56bd28c63..0bc0bdee44d 100644
--- a/toolkit/components/places/BookmarkHTMLUtils.sys.mjs
+++ b/toolkit/components/places/BookmarkHTMLUtils.sys.mjs
@@ -75,31 +75,6 @@ const MICROSEC_PER_SEC = 1000000;
 
 const EXPORT_INDENT = "    "; // four spaces
 
-/**
- * Provides HTML escaping for use in HTML attributes and body of the bookmarks
- * file, compatible with the old bookmarks system.
- *
- * @param {string} aText
- */
-function escapeHtmlEntities(aText) {
-  return (aText || "")
-    .replace(/&/g, "&amp;")
-    .replace(/</g, "&lt;")
-    .replace(/>/g, "&gt;")
-    .replace(/"/g, "&quot;")
-    .replace(/'/g, "&#39;");
-}
-
-/**
- * Provides URL escaping for use in HTML attributes of the bookmarks file,
- * compatible with the old bookmarks system.
- *
- * @param {string} aText
- */
-function escapeUrl(aText) {
-  return (aText || "").replace(/"/g, "%22");
-}
-
 function notifyObservers(aTopic, aInitialImport) {
   Services.obs.notifyObservers(
     null,
@@ -945,7 +920,9 @@ BookmarkExporter.prototype = {
 
   async _writeContainer(aItem, aIndent = "") {
     if (aItem == this._root) {
-      this._writeLine("<H1>" + escapeHtmlEntities(this._root.title) + "</H1>");
+      this._writeLine(
+        "<H1>" + PlacesUtils.escapeHtmlEntities(this._root.title) + "</H1>"
+      );
       this._writeLine("");
     } else {
       this._write(aIndent + "<DT><H3");
@@ -956,7 +933,9 @@ BookmarkExporter.prototype = {
       } else if (aItem.root === "unfiledBookmarksFolder") {
         this._writeAttribute("UNFILED_BOOKMARKS_FOLDER", "true");
       }
-      this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</H3>");
+      this._writeLine(
+        ">" + PlacesUtils.escapeHtmlEntities(aItem.title) + "</H3>"
+      );
     }
 
     this._writeLine(aIndent + "<DL><p>");
@@ -988,7 +967,7 @@ BookmarkExporter.prototype = {
     this._write(aIndent + "<HR");
     // We keep exporting separator titles, but don't support them anymore.
     if (aItem.title) {
-      this._writeAttribute("NAME", escapeHtmlEntities(aItem.title));
+      this._writeAttribute("NAME", PlacesUtils.escapeHtmlEntities(aItem.title));
     }
     this._write(">");
   },
@@ -1002,24 +981,33 @@ BookmarkExporter.prototype = {
     }
 
     this._write(aIndent + "<DT><A");
-    this._writeAttribute("HREF", escapeUrl(aItem.uri));
+    this._writeAttribute("HREF", PlacesUtils.escapeHtmlEntities(aItem.uri));
     this._writeDateAttributes(aItem);
     await this._writeFaviconAttribute(aItem);
 
     if (aItem.keyword) {
-      this._writeAttribute("SHORTCUTURL", escapeHtmlEntities(aItem.keyword));
+      this._writeAttribute(
+        "SHORTCUTURL",
+        PlacesUtils.escapeHtmlEntities(aItem.keyword)
+      );
       if (aItem.postData) {
-        this._writeAttribute("POST_DATA", escapeHtmlEntities(aItem.postData));
+        this._writeAttribute(
+          "POST_DATA",
+          PlacesUtils.escapeHtmlEntities(aItem.postData)
+        );
       }
     }
 
     if (aItem.charset) {
-      this._writeAttribute("LAST_CHARSET", escapeHtmlEntities(aItem.charset));
+      this._writeAttribute(
+        "LAST_CHARSET",
+        PlacesUtils.escapeHtmlEntities(aItem.charset)
+      );
     }
     if (aItem.tags) {
-      this._writeAttribute("TAGS", escapeHtmlEntities(aItem.tags));
+      this._writeAttribute("TAGS", PlacesUtils.escapeHtmlEntities(aItem.tags));
     }
-    this._writeLine(">" + escapeHtmlEntities(aItem.title) + "</A>");
+    this._writeLine(">" + PlacesUtils.escapeHtmlEntities(aItem.title) + "</A>");
   },
 
   _writeDateAttributes(aItem) {
@@ -1047,10 +1035,16 @@ BookmarkExporter.prototype = {
         PlacesUtils.toURI(aItem.uri)
       );
 
-      this._writeAttribute("ICON_URI", escapeUrl(favicon.uri.spec));
+      this._writeAttribute(
+        "ICON_URI",
+        PlacesUtils.escapeHtmlEntities(favicon.uri.spec)
+      );
 
       if (favicon?.rawData.length && !favicon.uri.schemeIs("chrome")) {
-        this._writeAttribute("ICON", favicon.dataURI.spec);
+        this._writeAttribute(
+          "ICON",
+          PlacesUtils.escapeHtmlEntities(favicon.dataURI.spec)
+        );
       }
     } catch (ex) {
       console.error("Unexpected Error trying to fetch icon data");
diff --git a/toolkit/components/places/PlacesUtils.sys.mjs b/toolkit/components/places/PlacesUtils.sys.mjs
index e4e97b4d6a5..75f7e0536db 100644
--- a/toolkit/components/places/PlacesUtils.sys.mjs
+++ b/toolkit/components/places/PlacesUtils.sys.mjs
@@ -436,6 +436,21 @@ export var PlacesUtils = {
   // Deprecated: This is only used for supporting import from older datasets.
   MOBILE_ROOT_ANNO: "mobile/bookmarksRoot",
 
+  /**
+   * Escapes a string for safe use in HTML text content or attribute values.
+   *
+   * @param {string} text
+   * @returns {string}
+   */
+  escapeHtmlEntities(text) {
+    return (text || "")
+      .replace(/&/g, "&amp;")
+      .replace(/</g, "&lt;")
+      .replace(/>/g, "&gt;")
+      .replace(/"/g, "&quot;")
+      .replace(/'/g, "&#39;");
+  },
+
   TOPIC_SHUTDOWN: "places-shutdown",
   TOPIC_INIT_COMPLETE: "places-init-complete",
   TOPIC_DATABASE_LOCKED: "places-database-locked",
@@ -1108,16 +1123,8 @@ export var PlacesUtils = {
     }
 
     function gatherDataHtml(node, recursiveOpen = true) {
-      let htmlEscape = s =>
-        s
-          .replace(/&/g, "&amp;")
-          .replace(/>/g, "&gt;")
-          .replace(/</g, "&lt;")
-          .replace(/"/g, "&quot;")
-          .replace(/'/g, "&apos;");
-
       // escape out potential HTML in the title
-      let escapedTitle = node.title ? htmlEscape(node.title) : "";
+      let escapedTitle = PlacesUtils.escapeHtmlEntities(node.title);
 
       if (PlacesUtils.nodeIsContainer(node)) {
         asContainer(node);
@@ -1145,7 +1152,7 @@ export var PlacesUtils = {
         return childString + "</DL>" + NEWLINE;
       }
       if (PlacesUtils.nodeIsURI(node)) {
-        return `<A HREF="${node.uri}">${escapedTitle}</A>${NEWLINE}`;
+        return `<A HREF="${PlacesUtils.escapeHtmlEntities(node.uri)}">${escapedTitle}</A>${NEWLINE}`;
       }
       if (PlacesUtils.nodeIsSeparator(node)) {
         return "<HR>" + NEWLINE;
diff --git a/browser/components/urlbar/tests/browser/browser_dragdropURL.js b/browser/components/urlbar/tests/browser/browser_dragdropURL.js
index dd8e2a63624..c33fb199fab 100644
--- a/browser/components/urlbar/tests/browser/browser_dragdropURL.js
+++ b/browser/components/urlbar/tests/browser/browser_dragdropURL.js
@@ -110,3 +110,31 @@ add_task(async function checkDragText() {
     await promiseLoad;
   });
 });
+
+add_task(async function checkDragFromURLBarHTMLEscaping() {
+  info(
+    "Check that title and href are HTML-escaped in the text/html drag flavor"
+  );
+  await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+    const TITLE = `A & B < C > D "E" 'F'`;
+    await SpecialPowers.spawn(browser, [TITLE], title => {
+      content.document.title = title;
+    });
+
+    gURLBar.focus();
+    gURLBar.select();
+
+    let dataTransfer = new DataTransfer();
+    gURLBar.inputField.dispatchEvent(
+      new DragEvent("dragstart", { dataTransfer, bubbles: true })
+    );
+
+    const ESCAPED_TITLE = "A &amp; B &lt; C &gt; D &quot;E&quot; &#39;F&#39;";
+    let html = dataTransfer.getData("text/html");
+    Assert.ok(html.length, "text/html drag data was set");
+    Assert.ok(
+      html.includes(ESCAPED_TITLE),
+      "title is HTML-escaped in drag data"
+    );
+  });
+});
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/browser/components/urlbar/tests/browser/browser_dragdropURL.js b/browser/components/urlbar/tests/browser/browser_dragdropURL.js
index dd8e2a63624..c33fb199fab 100644
--- a/browser/components/urlbar/tests/browser/browser_dragdropURL.js
+++ b/browser/components/urlbar/tests/browser/browser_dragdropURL.js
@@ -110,3 +110,31 @@ add_task(async function checkDragText() {
     await promiseLoad;
   });
 });
+
+add_task(async function checkDragFromURLBarHTMLEscaping() {
+  info(
+    "Check that title and href are HTML-escaped in the text/html drag flavor"
+  );
+  await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+    const TITLE = `A & B < C > D "E" 'F'`;
+    await SpecialPowers.spawn(browser, [TITLE], title => {
+      content.document.title = title;
+    });
+
+    gURLBar.focus();
+    gURLBar.select();
+
+    let dataTransfer = new DataTransfer();
+    gURLBar.inputField.dispatchEvent(
+      new DragEvent("dragstart", { dataTransfer, bubbles: true })
+    );
+
+    const ESCAPED_TITLE = "A &amp; B &lt; C &gt; D &quot;E&quot; &#39;F&#39;";
+    let html = dataTransfer.getData("text/html");
+    Assert.ok(html.length, "text/html drag data was set");
+    Assert.ok(
+      html.includes(ESCAPED_TITLE),
+      "title is HTML-escaped in drag data"
+    );
+  });
+});
Loading diff…