Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionSpoofing issue in the Downloads Panel component
ComponentToolkit
Bug ClassLogic Error
Tracker1970743
Fix commit72fc2f83fe29 (firefox) +138/-133
CISA KEVNot listed
CreditedCaro Kann
Disclosed2025-12-09

Changed Functions

FunctionChangeNotes
formatURIForDisplay
browser/components/firefoxview/fxview-tab-list.mjs
modified
formatURIForDisplay
toolkit/modules/BrowserUtils.sys.mjs
modified
if
toolkit/modules/BrowserUtils.sys.mjs
modified
switch
toolkit/modules/BrowserUtils.sys.mjs
modified

Files Changed

  • browser/base/content/browser-fullScreenAndPointerLock.js
  • browser/components/downloads/DownloadsViewUI.sys.mjs
  • browser/components/firefoxview/fxview-tab-list.mjs
  • browser/components/firefoxview/helpers.mjs
  • toolkit/modules/BrowserUtils.sys.mjs
  • toolkit/modules/tests/xpcshell/test_BrowserUtils_urlFormatting.js
  • toolkit/mozapps/downloads/DownloadUtils.sys.mjs
  • toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js
diff --git a/browser/base/content/browser-fullScreenAndPointerLock.js b/browser/base/content/browser-fullScreenAndPointerLock.js
index bef746dc594..f459f3ca404 100644
--- a/browser/base/content/browser-fullScreenAndPointerLock.js
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
@@ -100,10 +100,9 @@ var PointerlockFsWarning = {
     } else {
       textElem.removeAttribute("hidden");
       // Document's principal's URI has a host. Display a warning including it.
-      let { DownloadUtils } = ChromeUtils.importESModule(
-        "resource://gre/modules/DownloadUtils.sys.mjs"
-      );
-      let displayHost = DownloadUtils.getURIHost(uri.spec)[0];
+      let displayHost = BrowserUtils.formatURIForDisplay(uri, {
+        onlyBaseDomain: true,
+      });
       let l10nString = {
         "fullscreen-warning": "fullscreen-warning-domain",
         "pointerlock-warning": "pointerlock-warning-domain",
diff --git a/browser/components/downloads/DownloadsViewUI.sys.mjs b/browser/components/downloads/DownloadsViewUI.sys.mjs
index f005501b4be..0d3291bd0f9 100644
--- a/browser/components/downloads/DownloadsViewUI.sys.mjs
+++ b/browser/components/downloads/DownloadsViewUI.sys.mjs
@@ -12,6 +12,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 const lazy = {};
 
 ChromeUtils.defineESModuleGetters(lazy, {
+  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
   DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
   Downloads: "resource://gre/modules/Downloads.sys.mjs",
@@ -603,7 +604,13 @@ DownloadsViewUI.DownloadElementShell.prototype = {
       this.showStatus(stateLabel, hoverStatus);
       return;
     }
-    let [displayHost] = lazy.DownloadUtils.getURIHost(this.download.source.url);
+    let uri = URL.parse(this.download.source.url)?.URI;
+    let displayHost = uri
+      ? lazy.BrowserUtils.formatURIForDisplay(uri, {
+          onlyBaseDomain: true,
+        })
+      : "";
+
     let [displayDate] = lazy.DownloadUtils.getReadableDates(
       new Date(this.download.endTime)
     );
diff --git a/browser/components/firefoxview/fxview-tab-list.mjs b/browser/components/firefoxview/fxview-tab-list.mjs
index e8340e7d45c..ca3070ee9b1 100644
--- a/browser/components/firefoxview/fxview-tab-list.mjs
+++ b/browser/components/firefoxview/fxview-tab-list.mjs
@@ -517,7 +517,9 @@ export class FxviewTabRowBase extends MozLitElement {
 
   formatURIForDisplay(uriString) {
     return !window.IS_STORYBOOK
-      ? lazy.BrowserUtils.formatURIStringForDisplay(uriString)
+      ? lazy.BrowserUtils.formatURIStringForDisplay(uriString, {
+          showFilenameForLocalURIs: true,
+        })
       : uriString;
   }
 
diff --git a/browser/components/firefoxview/helpers.mjs b/browser/components/firefoxview/helpers.mjs
index d949e1efffc..7c49eccb768 100644
--- a/browser/components/firefoxview/helpers.mjs
+++ b/browser/components/firefoxview/helpers.mjs
@@ -24,7 +24,9 @@ export const LOGGING_PREF = "browser.tabs.firefox-view.logLevel";
 export const MAX_TABS_FOR_RECENT_BROWSING = 5;
 
 export function formatURIForDisplay(uriString) {
-  return lazy.BrowserUtils.formatURIStringForDisplay(uriString);
+  return lazy.BrowserUtils.formatURIStringForDisplay(uriString, {
+    showFilenameForLocalURIs: true,
+  });
 }
 
 export function convertTimestamp(
diff --git a/toolkit/modules/BrowserUtils.sys.mjs b/toolkit/modules/BrowserUtils.sys.mjs
index 0136bd8461b..368bb7ba536 100644
--- a/toolkit/modules/BrowserUtils.sys.mjs
+++ b/toolkit/modules/BrowserUtils.sys.mjs
@@ -91,7 +91,10 @@ XPCOMUtils.defineLazyPreferenceGetter(
 );
 
 ChromeUtils.defineLazyGetter(lazy, "gLocalization", () => {
-  return new Localization(["toolkit/global/browser-utils.ftl"], true);
+  return new Localization(
+    ["toolkit/global/browser-utils.ftl", "toolkit/downloads/downloadUtils.ftl"],
+    true
+  );
 });
 
 function stringPrefToSet(prefVal) {
@@ -246,8 +249,36 @@ export var BrowserUtils = {
     }
   },
 
+  /**
+   * Show a URI in the UI in a user-friendly (but security-sensitive) way.
+   *
+   * @param {nsIURI} uri
+   * @param {object} [options={}]
+   * @param {boolean} [options.showInsecureHTTP=false]
+   *        Whether to show "http://" for insecure HTTP URLs.
+   * @param {boolean} [options.showWWW=false]
+   *        Whether to show "www." for URLs that have it.
+   * @param {boolean} [options.onlyBaseDomain=false]
+   *        Whether to show only the base domain (eTLD+1) for HTTP(S) URLs.
+   * @param {boolean} [options.showFilenameForLocalURIs=false]
+   *        If false (default), will show a protocol-specific label for local
+   *        URIs (file:, chrome:, resource:, moz-src:, jar:).
+   *        Otherwise, will show the filename for such URIs. Only use 'true' if
+   *        the context in which the URI is being represented is not security-
+   *        critical.
+   */
   formatURIForDisplay(uri, options = {}) {
-    let { showInsecureHTTP = false } = options;
+    let {
+      showInsecureHTTP = false,
+      showWWW = false,
+      onlyBaseDomain = false,
+      showFilenameForLocalURIs = false,
+    } = options;
+    // For moz-icon and jar etc. which wrap nsIURLs, if we want to show the
+    // actual filename, unwrap:
+    if (uri && uri instanceof Ci.nsINestedURI && showFilenameForLocalURIs) {
+      return this.formatURIForDisplay(uri.innermostURI, options);
+    }
     switch (uri.scheme) {
       case "view-source": {
         let innerURI = uri.spec.substring("view-source:".length);
@@ -257,8 +288,14 @@ export var BrowserUtils = {
       // Fall through.
       case "https": {
         let host = uri.displayHostPort;
-        if (!showInsecureHTTP && host.startsWith("www.")) {
+        let removeSubdomains =
+          !showInsecureHTTP &&
+          (onlyBaseDomain || (!showWWW && host.startsWith("www.")));
+        if (removeSubdomains) {
           host = Services.eTLD.getSchemelessSite(uri);
+          if (uri.port != -1) {
+            host += ":" + uri.port;
+          }
         }
         if (showInsecureHTTP && uri.scheme == "http") {
           return "http://" + host;
@@ -269,7 +306,7 @@ export var BrowserUtils = {
         return "about:" + uri.filePath;
       case "blob":
         try {
-          let url = new URL(uri.specIgnoringRef);
+          let url = URL.fromURI(uri);
           // _If_ we find a non-null origin, report that.
           if (url.origin && url.origin != "null") {
             return this.formatURIStringForDisplay(url.origin, options);
@@ -291,8 +328,22 @@ export var BrowserUtils = {
       }
       case "chrome":
       case "resource":
+      case "moz-icon":
+      case "moz-src":
       case "jar":
       case "file":
+        if (!showFilenameForLocalURIs) {
+          if (uri.scheme == "file") {
+            return lazy.gLocalization.formatValueSync(
+              "download-utils-done-file-scheme"
+            );
+          }
+          return lazy.gLocalization.formatValueSync(
+            "download-utils-done-scheme",
+            { scheme: uri.scheme }
+          );
+        }
+      // Otherwise, fall through to show filename...
       default:
         try {
           let url = uri.QueryInterface(Ci.nsIURL);
@@ -318,7 +369,7 @@ export var BrowserUtils = {
           console.error(ex);
         }
     }
-    return uri.asciiHost || uri.spec;
+    return uri.spec;
   },
 
   // Given a URL returns a (possibly transformed) URL suitable for sharing, or null if
diff --git a/toolkit/modules/tests/xpcshell/test_BrowserUtils_urlFormatting.js b/toolkit/modules/tests/xpcshell/test_BrowserUtils_urlFormatting.js
index 75035398f5a..73712a3764f 100644
--- a/toolkit/modules/tests/xpcshell/test_BrowserUtils_urlFormatting.js
+++ b/toolkit/modules/tests/xpcshell/test_BrowserUtils_urlFormatting.js
@@ -9,7 +9,10 @@ let tempFile = new FileUtils.File(PathUtils.tempDir);
 const TEST_LOCAL_FILE_NAME = "hello.txt";
 tempFile.append(TEST_LOCAL_FILE_NAME);
 
-const gL10n = new Localization(["toolkit/global/browser-utils.ftl"], true);
+const gL10n = new Localization(
+  ["toolkit/global/browser-utils.ftl", "toolkit/downloads/downloadUtils.ftl"],
+  true
+);
 const DATA_URL_EXPECTED_STRING = gL10n.formatValueSync(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/toolkit/modules/tests/xpcshell/test_BrowserUtils_urlFormatting.js b/toolkit/modules/tests/xpcshell/test_BrowserUtils_urlFormatting.js
index 75035398f5a..73712a3764f 100644
--- a/toolkit/modules/tests/xpcshell/test_BrowserUtils_urlFormatting.js
+++ b/toolkit/modules/tests/xpcshell/test_BrowserUtils_urlFormatting.js
@@ -9,7 +9,10 @@ let tempFile = new FileUtils.File(PathUtils.tempDir);
 const TEST_LOCAL_FILE_NAME = "hello.txt";
 tempFile.append(TEST_LOCAL_FILE_NAME);
 
-const gL10n = new Localization(["toolkit/global/browser-utils.ftl"], true);
+const gL10n = new Localization(
+  ["toolkit/global/browser-utils.ftl", "toolkit/downloads/downloadUtils.ftl"],
+  true
+);
 const DATA_URL_EXPECTED_STRING = gL10n.formatValueSync(
   "browser-utils-url-data"
 );
@@ -19,6 +22,10 @@ const EXTENSION_URL_EXPECTED_STRING = gL10n.formatValueSync(
   { extension: EXTENSION_NAME }
 );
 
+const FILE_URL_EXPECTED_STRING = gL10n.formatValueSync(
+  "download-utils-done-file-scheme"
+);
+
 const { AddonTestUtils } = ChromeUtils.importESModule(
   "resource://testing-common/AddonTestUtils.sys.mjs"
 );
@@ -88,7 +95,7 @@ const HTTP_TESTS = [
     output: "www.co.uk",
   },
 
-  // Other sudomains should be kept:
+  // Other subdomains should be kept:
   {
     input: "https://webmail.example.co.uk",
     output: "webmail.example.co.uk",
@@ -158,6 +165,15 @@ const TESTS = [
     input: "data:text/html,42",
     output: DATA_URL_EXPECTED_STRING,
   },
+
+  {
+    input: `moz-icon:${Services.io.newFileURI(tempFile).spec}`,
+    output: tempFile.leafName,
+  },
+  {
+    input: "moz-icon://.extension?size=16",
+    output: "moz-icon://.extension?size=16",
+  },
 ];
 
 add_setup(async () => {
@@ -220,7 +236,9 @@ const { BrowserUtils } = ChromeUtils.importESModule(
 add_task(async function test_checkStringFormatting() {
   for (let { input, output } of TESTS) {
     Assert.equal(
-      BrowserUtils.formatURIStringForDisplay(input),
+      BrowserUtils.formatURIStringForDisplay(input, {
+        showFilenameForLocalURIs: true,
+      }),
       output,
       `String ${input} formatted for output should match`
     );
@@ -231,13 +249,56 @@ add_task(async function test_checkURIFormatting() {
   for (let { input, output } of TESTS) {
     let uri = Services.io.newURI(input);
     Assert.equal(
-      BrowserUtils.formatURIForDisplay(uri),
+      BrowserUtils.formatURIForDisplay(uri, {
+        showFilenameForLocalURIs: true,
+      }),
       output,
       `URI ${input} formatted for output should match`
     );
   }
 });
 
+add_task(async function test_checkOnlyBaseDomain() {
+  for (let { input, output } of [
+    { input: "https://subdomain.example.com/", output: "example.com" },
+    {
+      input: "http://www.city.mikasa.hokkaido.jp/",
+      output: "city.mikasa.hokkaido.jp",
+    },
+    { input: "https://www.example.co.uk/", output: "example.co.uk" },
+    {
+      input: "mailto:[email protected]",
+      output: "mailto:[email protected]",
+    },
+  ]) {
+    let uri = Services.io.newURI(input);
+    Assert.equal(
+      BrowserUtils.formatURIForDisplay(uri, { onlyBaseDomain: true }),
+      output,
+      `URI ${input} formatted for output should match`
+    );
+  }
+});
+
+add_task(async function test_checkLocalFileFormatting() {
+  for (let { input } of TESTS) {
+    let uri = Services.io.newURI(input);
+    if (
+      ["file", "chrome", "moz-icon", "resource", "jar"].includes(uri.scheme)
+    ) {
+      Assert.equal(
+        BrowserUtils.formatURIForDisplay(uri, {
+          showFilenameForLocalURIs: false,
+        }),
+        uri.scheme == "file"
+          ? FILE_URL_EXPECTED_STRING
+          : `${uri.scheme} resource`,
+        `URI ${input} formatted for output should match`
+      );
+    }
+  }
+});
+
 add_task(async function test_checkViewSourceFormatting() {
   for (let { input, output } of HTTP_TESTS) {
     Assert.equal(
diff --git a/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js b/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js
index 1ce074f41a6..c6345e74866 100644
--- a/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js
+++ b/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js
@@ -83,16 +83,6 @@ function testFormattedTimeStatus(aSec, aExpected) {
   Assert.equal(status.l10n.id, aExpected);
 }
 
-function testURI(aURI, aDisp, aHost) {
-  dump("URI Test: " + [aURI, aDisp, aHost] + "\n");
-
-  let [disp, host] = DownloadUtils.getURIHost(aURI);
-
-  // Make sure we have the right display host and full host
-  Assert.equal(disp, aDisp);
-  Assert.equal(host, aHost);
-}
-
 function testGetReadableDates(aDate, aCompactValue) {
   const now = new Date(2000, 11, 31, 11, 59, 59);
 
@@ -373,26 +363,5 @@ function run_test() {
   testFormattedTimeStatus(0, "downloading-file-opens-in-seconds-2");
   testFormattedTimeStatus(30, "downloading-file-opens-in-seconds-2");
 
-  testURI("http://www.mozilla.org/", "mozilla.org", "www.mozilla.org");
-  testURI(
-    "http://www.city.mikasa.hokkaido.jp/",
-    "city.mikasa.hokkaido.jp",
-    "www.city.mikasa.hokkaido.jp"
-  );
-  testURI("data:text/html,Hello World", "data resource", "data resource");
-  testURI(
-    "jar:http://www.mozilla.com/file!/magic",
-    "mozilla.com",
-    "www.mozilla.com"
-  );
-  testURI("file:///C:/Cool/Stuff/", "local file", "local file");
-  // Don't test for moz-icon if we don't have a protocol handler for it (e.g. b2g):
-  if ("@mozilla.org/network/protocol;1?name=moz-icon" in Cc) {
-    testURI("moz-icon:file:///test.extension", "local file", "local file");
-    testURI("moz-icon://.extension", "moz-icon resource", "moz-icon resource");
-  }
-  testURI("about:config", "about resource", "about resource");
-  testURI("invalid.uri", "", "");
-
   testAllGetReadableDates();
 }
Loading diff…