CVE-2025-6425
Overview
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
[email protected], a built-in extension shipped with Firefox whoseabout:compatpage 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 [email protected] 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.
Attack Path
- Probe
Attacker web page enumerates/resolves resources served from the WebCompat extension to recover its
moz-extension://<UUID>/origin. - Harvest UUID The recovered UUID is a high-entropy value unique to this browser profile.
- 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.
- Correlate The same UUID is re-read on later visits and from other containers, linking otherwise-isolated browsing contexts to one identifier.
- Primitive Persistent cross-context browser fingerprinting / tracking identifier (does not cross separate profiles).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
gettoolkit/components/extensions/Extension.sys.mjs |
modified | |
iftoolkit/components/extensions/Extension.sys.mjs |
modified | |
add_taskbrowser/extensions/webcompat/tests/browser/browser_uuid_migration.js |
modified |
Files Changed
browser/extensions/webcompat/moz.buildbrowser/extensions/webcompat/tests/browser/browser_aboutcompat.jsbrowser/extensions/webcompat/tests/browser/browser_uuid_migration.jsbrowser/extensions/webcompat/tests/browser/browser_uuid_migration.tomltoolkit/components/extensions/Extension.sys.mjs
Audit Directions
- Other built-in/system extensionsReview whether any other shipped add-on with content-reachable resources still receives a random persistent UUID that could serve as a fingerprint.
- UUID enumeration surfaceAudit how content can discover
moz-extension://origins (error messages, timing, resource probing) for built-in components. - Persisted-UUID assumptionsVerify the comment’s premise (WebCompat does not store its origin in profile data) holds, so the forced migration cannot orphan persisted associations.
Patch
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 = "[email protected]";
+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("[email protected]")
+ .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("[email protected]").mozExtensionHostname,
+ expectedWebCompatUUID,
+ "webcompat add-on has fixed UUID"
+ );
+
+ let uuids = Services.prefs.getStringPref("extensions.webextensions.uuids");
+ ok(
+ uuids.includes(`"[email protected]":"${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={"[email protected]":"f00df00d-1111-f00d-8888-012345678900","[email protected]":"f00df00d-2222-f00d-8888-012345678900"}']
+
+["browser_uuid_migration.js"]
Regression Test / PoC
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("[email protected]")
+ .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("[email protected]").mozExtensionHostname,
+ expectedWebCompatUUID,
+ "webcompat add-on has fixed UUID"
+ );
+
+ let uuids = Services.prefs.getStringPref("extensions.webextensions.uuids");
+ ok(
+ uuids.includes(`"[email protected]":"${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={"[email protected]":"f00df00d-1111-f00d-8888-012345678900","[email protected]":"f00df00d-2222-f00d-8888-012345678900"}']
+
+["browser_uuid_migration.js"]