Firefox · Toolkit
CVE-2026-100820
Logic Error in Toolkit
Overview
Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
escapeHtmlEntitiesbrowser/components/urlbar/content/UrlbarShared.mjs |
modified | |
iftoolkit/components/places/BookmarkHTMLUtils.sys.mjs |
modified | |
_writeDateAttributestoolkit/components/places/BookmarkHTMLUtils.sys.mjs |
modified | |
escapeHtmlEntitiestoolkit/components/places/PlacesUtils.sys.mjs |
modified | |
add_taskbrowser/components/urlbar/tests/browser/browser_dragdropURL.js |
modified |
Files Changed
browser/components/urlbar/content/SmartbarInput.mjsbrowser/components/urlbar/content/UrlbarInputBase.mjsbrowser/components/urlbar/content/UrlbarShared.mjsbrowser/components/urlbar/tests/browser/browser_dragdropURL.jstoolkit/components/places/BookmarkHTMLUtils.sys.mjstoolkit/components/places/PlacesUtils.sys.mjs
Patch
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, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ },
};
/**
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, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
-}
-
-/**
- * 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, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ },
+
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, "&")
- .replace(/>/g, ">")
- .replace(/</g, "<")
- .replace(/"/g, """)
- .replace(/'/g, "'");
-
// 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 & B < C > D "E" 'F'";
+ 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 & B < C > D "E" 'F'";
+ 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…
References
On This Page