Medium CVSS 4.3 firefox Info Disclosure 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionAn attacker who enumerated resources from the WebCompat extension could have obtained a persistent UUID that identified the browser, and persisted between containers and normal/private browsing mode, but not profiles.
ComponentToolkit
Bug ClassInfo Disclosure
Tracker1717672
Fix commitacefe30a8b6f (firefox) +69/-1
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedRob Wu
Disclosed2025-06-24

Background

`moz-extension://` origins
Every installed WebExtension is served from a moz-extension://<UUID>/ origin whose host component is a machine-specific UUID, not the add-on ID. This indirection is intended to stop web content from trivially probing which extensions are installed.
WebCompat add-on
webcompat@mozilla.org, a built-in extension shipped with Firefox whose about:compat page is reachable from content.
Persistent identifier / fingerprint
Because the WebCompat UUID was randomly generated once per profile and then persisted, it became a stable value that uniquely identified the browser instance.

Root Cause Analysis

For a normal extension, UUIDMap.get must return a stable per-profile UUID because that UUID can leak into persisted locations (history, bookmarks, site permissions, principals) and changing it would break those associations. The flaw is that the built-in WebCompat add-on got the same treatment: it was assigned a random but permanent UUID. Since the WebCompat resources (e.g. about:compat, backed by moz-extension://<UUID>/) are enumerable/probeable from web content, a page that discovered the WebCompat origin obtained a high-entropy identifier that was identical across containers and across normal/private-browsing sessions within the same profile — a super-cookie usable for cross-context tracking.

The patch special-cases webcompat@mozilla.org in UUIDMap.get: it introduces constants WEBCOMPAT_ADDON_ID and a fixed WEBCOMPAT_UUID (9a310967-e580-48bf-b3e8-4eafebbc122d) and, when the requested ID is WebCompat, forces the map entry to that constant UUID (migrating/rewriting any previously-stored random value and persisting the correction), or returns null when create is false and no entry exists. Because the WebCompat add-on does not rely on a persisted-per-profile UUID, pinning it to a single global constant means every Firefox install exposes the same WebCompat origin, eliminating the distinguishing entropy. The advisory calls this an “information exposure” (CWE-200); the diff substantiates a privacy/fingerprinting fix — a persistent unique identifier was being exposed to content — rather than a classic memory-safety or data-leak bug.

Key insight
A built-in extension whose URLs are reachable from web content was given a randomly-generated, permanently-persisted per-profile UUID, turning that UUID into a stable cross-container, cross-private-mode fingerprint; the fix pins it to a single hard-coded UUID shared by all installs.

Attack Path

  1. Probe Attacker web page enumerates/resolves resources served from the WebCompat extension to recover its moz-extension://<UUID>/ origin.
  2. Harvest UUID The recovered UUID is a high-entropy value unique to this browser profile.
  3. Persist identity Because the UUID is stable across containers and across normal vs. private browsing within the profile, the attacker uses it as a durable super-cookie.
  4. Correlate The same UUID is re-read on later visits and from other containers, linking otherwise-isolated browsing contexts to one identifier.
  5. Primitive Persistent cross-context browser fingerprinting / tracking identifier (does not cross separate profiles).

Impact Assessment

This is a privacy/information-exposure issue reachable from ordinary web content; no elevated privilege is required, only the ability to probe the WebCompat extension’s resources (hence UI:R / low confidentiality impact in the CVSS vector). The attacker gains a stable identifier that defeats container and private-browsing isolation within a profile, enabling cross-context tracking. It does not grant memory corruption or code execution, and the identifier does not persist across distinct Firefox profiles.

Changed Functions

FunctionChangeNotes
get
toolkit/components/extensions/Extension.sys.mjs
modified
if
toolkit/components/extensions/Extension.sys.mjs
modified
add_task
browser/extensions/webcompat/tests/browser/browser_uuid_migration.js
modified

Files Changed

  • browser/extensions/webcompat/moz.build
  • browser/extensions/webcompat/tests/browser/browser_aboutcompat.js
  • browser/extensions/webcompat/tests/browser/browser_uuid_migration.js
  • browser/extensions/webcompat/tests/browser/browser_uuid_migration.toml
  • toolkit/components/extensions/Extension.sys.mjs

Audit Directions

  • Other built-in/system extensions
    Review whether any other shipped add-on with content-reachable resources still receives a random persistent UUID that could serve as a fingerprint.
  • UUID enumeration surface
    Audit how content can discover moz-extension:// origins (error messages, timing, resource probing) for built-in components.
  • Persisted-UUID assumptions
    Verify the comment’s premise (WebCompat does not store its origin in profile data) holds, so the forced migration cannot orphan persisted associations.
diff --git a/browser/extensions/webcompat/moz.build b/browser/extensions/webcompat/moz.build
index db7f2c6bb5b..19689b83a42 100644
--- a/browser/extensions/webcompat/moz.build
+++ b/browser/extensions/webcompat/moz.build
@@ -12,7 +12,10 @@ XPCOM_MANIFESTS += [
     "components.conf",
 ]
 
-BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+BROWSER_CHROME_MANIFESTS += [
+    "tests/browser/browser.toml",
+    "tests/browser/browser_uuid_migration.toml",
+]
 
 with Files("**"):
     BUG_COMPONENT = ("Web Compatibility", "Tooling & Investigations")
diff --git a/toolkit/components/extensions/Extension.sys.mjs b/toolkit/components/extensions/Extension.sys.mjs
index ff513e8674a..854ee8bcddf 100644
--- a/toolkit/components/extensions/Extension.sys.mjs
+++ b/toolkit/components/extensions/Extension.sys.mjs
@@ -391,6 +391,8 @@ const LOGGER_ID_BASE = "addons.webextension.";
 const UUID_MAP_PREF = "extensions.webextensions.uuids";
 const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
 const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
+const WEBCOMPAT_ADDON_ID = "webcompat@mozilla.org";
+const WEBCOMPAT_UUID = "9a310967-e580-48bf-b3e8-4eafebbc122d";
 
 // All moz-extension URIs use a machine-specific UUID rather than the
 // extension's own ID in the host component. This makes it more
@@ -416,6 +418,23 @@ var UUIDMap = {
   get(id, create = true) {
     let map = this._read();
 
+    // In general, the UUID should not change once assigned because it may be
+    // stored elsewhere within the profile directory, when the extension URL is
+    // exposed (e.g. history, bookmarks, site permissions, web or extension
+    // APIs that associate data with the extension principal or origin).
+    // The webcompat add-on does not rely on the persisted uuid, so we can
+    // simply migrate the uuid below, see bug 1717672.
+    if (id === WEBCOMPAT_ADDON_ID) {
+      if (!create && !(id in map)) {
+        return null;
+      }
+      if (map[id] !== WEBCOMPAT_UUID) {
+        map[id] = WEBCOMPAT_UUID;
+        this._write(map);
+      }
+      return WEBCOMPAT_UUID;
+    }
+
     if (id in map) {
       return map[id];
     }
diff --git a/browser/extensions/webcompat/tests/browser/browser_aboutcompat.js b/browser/extensions/webcompat/tests/browser/browser_aboutcompat.js
index ed89739c77d..110161bc165 100644
--- a/browser/extensions/webcompat/tests/browser/browser_aboutcompat.js
+++ b/browser/extensions/webcompat/tests/browser/browser_aboutcompat.js
@@ -8,6 +8,12 @@ add_task(async function test_about_compat_loads_properly() {
   });
 
   await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+    is(
+      content.origin,
+      "moz-extension://9a310967-e580-48bf-b3e8-4eafebbc122d",
+      "Expected origin of about:compat"
+    );
+
     await ContentTaskUtils.waitForCondition(
       () => content.document.querySelector("#interventions tr[data-id]"),
       "interventions are listed"
diff --git a/browser/extensions/webcompat/tests/browser/browser_uuid_migration.js b/browser/extensions/webcompat/tests/browser/browser_uuid_migration.js
new file mode 100644
index 00000000000..f3dc5df5949
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/browser_uuid_migration.js
@@ -0,0 +1,33 @@
+"use strict";
+
+// Sanity check: The setup in the toml file is effective at fixing the uuid
+// for at least one extension.
+add_task(async function test_sanity_check_uuid_fixed_by_pref() {
+  is(
+    WebExtensionPolicy.getByID("pictureinpicture@mozilla.org")
+      .mozExtensionHostname,
+    "f00df00d-2222-f00d-8888-012345678900",
+    "Non-webcompat uuid is fixed by pref in browser_uuid_migration.toml"
+  );
+});
+
+add_task(async function test_webcompat_migrates_existing_uuid_pref() {
+  const expectedWebCompatUUID = "9a310967-e580-48bf-b3e8-4eafebbc122d";
+  Assert.notEqual(
+    expectedWebCompatUUID,
+    "f00df00d-1111-f00d-8888-012345678900",
+    "Sanity check: Expected UUID differs from browser_uuid_migration.toml"
+  );
+
+  is(
+    WebExtensionPolicy.getByID("webcompat@mozilla.org").mozExtensionHostname,
+    expectedWebCompatUUID,
+    "webcompat add-on has fixed UUID"
+  );
+
+  let uuids = Services.prefs.getStringPref("extensions.webextensions.uuids");
+  ok(
+    uuids.includes(`"webcompat@mozilla.org":"${expectedWebCompatUUID}"`),
+    `Pref value (${uuids}) should contain: ${expectedWebCompatUUID}`
+  );
+});
diff --git a/browser/extensions/webcompat/tests/browser/browser_uuid_migration.toml b/browser/extensions/webcompat/tests/browser/browser_uuid_migration.toml
new file mode 100644
index 00000000000..3cd9869d6d5
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/browser_uuid_migration.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = "webextensions"
+# Must be set in [DEFAULT] to ensure that the pref is set before the browser starts.
+# We choose two built-in extensions here: webcompat and another built-in extension.
+prefs = ['extensions.webextensions.uuids={"webcompat@mozilla.org":"f00df00d-1111-f00d-8888-012345678900","pictureinpicture@mozilla.org":"f00df00d-2222-f00d-8888-012345678900"}']
+
+["browser_uuid_migration.js"]
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/browser/extensions/webcompat/tests/browser/browser_aboutcompat.js b/browser/extensions/webcompat/tests/browser/browser_aboutcompat.js
index ed89739c77d..110161bc165 100644
--- a/browser/extensions/webcompat/tests/browser/browser_aboutcompat.js
+++ b/browser/extensions/webcompat/tests/browser/browser_aboutcompat.js
@@ -8,6 +8,12 @@ add_task(async function test_about_compat_loads_properly() {
   });
 
   await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+    is(
+      content.origin,
+      "moz-extension://9a310967-e580-48bf-b3e8-4eafebbc122d",
+      "Expected origin of about:compat"
+    );
+
     await ContentTaskUtils.waitForCondition(
       () => content.document.querySelector("#interventions tr[data-id]"),
       "interventions are listed"
diff --git a/browser/extensions/webcompat/tests/browser/browser_uuid_migration.js b/browser/extensions/webcompat/tests/browser/browser_uuid_migration.js
new file mode 100644
index 00000000000..f3dc5df5949
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/browser_uuid_migration.js
@@ -0,0 +1,33 @@
+"use strict";
+
+// Sanity check: The setup in the toml file is effective at fixing the uuid
+// for at least one extension.
+add_task(async function test_sanity_check_uuid_fixed_by_pref() {
+  is(
+    WebExtensionPolicy.getByID("pictureinpicture@mozilla.org")
+      .mozExtensionHostname,
+    "f00df00d-2222-f00d-8888-012345678900",
+    "Non-webcompat uuid is fixed by pref in browser_uuid_migration.toml"
+  );
+});
+
+add_task(async function test_webcompat_migrates_existing_uuid_pref() {
+  const expectedWebCompatUUID = "9a310967-e580-48bf-b3e8-4eafebbc122d";
+  Assert.notEqual(
+    expectedWebCompatUUID,
+    "f00df00d-1111-f00d-8888-012345678900",
+    "Sanity check: Expected UUID differs from browser_uuid_migration.toml"
+  );
+
+  is(
+    WebExtensionPolicy.getByID("webcompat@mozilla.org").mozExtensionHostname,
+    expectedWebCompatUUID,
+    "webcompat add-on has fixed UUID"
+  );
+
+  let uuids = Services.prefs.getStringPref("extensions.webextensions.uuids");
+  ok(
+    uuids.includes(`"webcompat@mozilla.org":"${expectedWebCompatUUID}"`),
+    `Pref value (${uuids}) should contain: ${expectedWebCompatUUID}`
+  );
+});
diff --git a/browser/extensions/webcompat/tests/browser/browser_uuid_migration.toml b/browser/extensions/webcompat/tests/browser/browser_uuid_migration.toml
new file mode 100644
index 00000000000..3cd9869d6d5
--- /dev/null
+++ b/browser/extensions/webcompat/tests/browser/browser_uuid_migration.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = "webextensions"
+# Must be set in [DEFAULT] to ensure that the pref is set before the browser starts.
+# We choose two built-in extensions here: webcompat and another built-in extension.
+prefs = ['extensions.webextensions.uuids={"webcompat@mozilla.org":"f00df00d-1111-f00d-8888-012345678900","pictureinpicture@mozilla.org":"f00df00d-2222-f00d-8888-012345678900"}']
+
+["browser_uuid_migration.js"]
Loading diff…