High firefox Sandbox Escape 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionSandbox escape in Firefox and Firefox Focus for Android
ComponentCore
Bug ClassSandbox Escape
Tracker2003171
Fix commit80b6d771b4a1 (firefox) +171/-175
CISA KEVNot listed
CreditedDaisuke Hatakeyama and Shota Matsuda
Disclosed2026-05-19

Changed Functions

FunctionChangeNotes
getMediaPermission
mobile/shared/actors/GeckoViewPermissionChild.sys.mjs
modified
addCameraPermission
mobile/shared/actors/GeckoViewPermissionChild.sys.mjs
modified
getAppPermissions
mobile/shared/actors/GeckoViewPermissionChild.sys.mjs
modified
mediaRecordingStatusChanged
mobile/shared/actors/GeckoViewPermissionChild.sys.mjs
modified
checkIfGrantedByExtensionPermissions
mobile/shared/actors/GeckoViewPermissionChild.sys.mjs
modified
if
mobile/shared/actors/GeckoViewPermissionChild.sys.mjs
modified
switch
mobile/shared/actors/GeckoViewPermissionParent.sys.mjs
modified
constructor
mobile/shared/components/geckoview/GeckoViewPermission.sys.mjs
modified
if
mobile/shared/components/geckoview/GeckoViewPermission.sys.mjs
modified
checkIfGrantedByExtensionPermissions
mobile/shared/components/geckoview/GeckoViewPermission.sys.mjs
modified

Files Changed

  • mobile/shared/actors/GeckoViewPermissionChild.sys.mjs
  • mobile/shared/actors/GeckoViewPermissionParent.sys.mjs
  • mobile/shared/components/geckoview/GeckoViewPermission.sys.mjs
diff --git a/mobile/shared/actors/GeckoViewPermissionChild.sys.mjs b/mobile/shared/actors/GeckoViewPermissionChild.sys.mjs
index d454250bb56..6ca281f084f 100644
--- a/mobile/shared/actors/GeckoViewPermissionChild.sys.mjs
+++ b/mobile/shared/actors/GeckoViewPermissionChild.sys.mjs
@@ -4,20 +4,6 @@
 
 import { GeckoViewActorChild } from "resource://gre/modules/GeckoViewActorChild.sys.mjs";
 
-const lazy = {};
-
-ChromeUtils.defineESModuleGetters(lazy, {
-  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
-});
-
-const PERM_ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION";
-
-const MAPPED_TO_EXTENSION_PERMISSIONS = [
-  "persistent-storage",
-  // TODO(Bug 1336194): support geolocation manifest permission
-  // (see https://bugzilla.mozilla.org/show_bug.cgi?id=1336194#c17)l
-];
-
 export class GeckoViewPermissionChild extends GeckoViewActorChild {
   getMediaPermission(aPermission) {
     return this.sendQuery("GeckoView:MediaPermission", {
@@ -26,11 +12,7 @@ export class GeckoViewPermissionChild extends GeckoViewActorChild {
   }
 
   addCameraPermission() {
-    return this.sendQuery("AddCameraPermission");
-  }
-
-  getAppPermissions(aPermissions) {
-    return this.sendQuery("GetAppPermissions", aPermissions);
+    return this.sendQuery("GeckoView:AddCameraPermission");
   }
 
   mediaRecordingStatusChanged(aDevices) {
@@ -38,146 +20,6 @@ export class GeckoViewPermissionChild extends GeckoViewActorChild {
       devices: aDevices,
     });
   }
-
-  // Some WebAPI permissions can be requested and granted to extensions through the
-  // the extension manifest.json, which the user have been already prompted for
-  // (e.g. at install time for the one listed in the manifest.json permissions property,
-  // or at runtime through the optional_permissions property and the permissions.request
-  // WebExtensions API method).
-  //
-  // WebAPI permission that are expected to be mapped to extensions permissions are listed
-  // in the MAPPED_TO_EXTENSION_PERMISSIONS array.
-  //
-  // @param {nsIContentPermissionType} perm
-  //        The WebAPI permission being requested
-  // @param {nsIContentPermissionRequest} aRequest
-  //        The nsIContentPermissionRequest as received by the promptPermission method.
-  //
-  // @returns {null | { allow: boolean, permission: Object }
-  //          Returns null if the request was not handled and should continue with the
-  //          regular permission prompting flow, otherwise it returns an object to
-  //          allow or disallow the permission request right away.
-  checkIfGrantedByExtensionPermissions(perm, aRequest) {
-    if (!aRequest.principal.addonPolicy) {
-      // Not an extension, continue with the regular permission prompting flow.
-      return null;
-    }
-
-    // Return earlier and continue with the regular permission prompting flow if the
-    // the permission is no one that can be requested from the extension manifest file.
-    if (!MAPPED_TO_EXTENSION_PERMISSIONS.includes(perm.type)) {
-      return null;
-    }
-
-    // Disallow if the extension is not active anymore.
-    if (!aRequest.principal.addonPolicy.active) {
-      return { allow: false };
-    }
-
-    // Check if the permission have been already granted to the extension, if it is allow it right away.
-    const isGranted =
-      Services.perms.testPermissionFromPrincipal(
-        aRequest.principal,
-        perm.type
-      ) === Services.perms.ALLOW_ACTION;
-    if (isGranted) {
-      return {
-        allow: true,
-        permission: { [perm.type]: Services.perms.ALLOW_ACTION },
-      };
-    }
-
-    // continue with the regular permission prompting flow otherwise.
-    return null;
-  }
-
-  async promptPermission(aRequest) {
-    // Only allow exactly one permission request here.
-    const types = aRequest.types.QueryInterface(Ci.nsIArray);
-    if (types.length !== 1) {
-      return { allow: false };
-    }
-
-    const perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
-
-    // Check if the request principal is an extension principal and if the permission requested
-    // should be already granted based on the extension permissions (or disallowed right away
-    // because the extension is not enabled anymore.
-    const extensionResult = this.checkIfGrantedByExtensionPermissions(
-      perm,
-      aRequest
-    );
-    if (extensionResult) {
-      return extensionResult;
-    }
-
-    if (
-      perm.type === "desktop-notification" &&
-      !aRequest.hasValidTransientUserGestureActivation &&
-      Services.prefs.getBoolPref(
-        "dom.webnotifications.requireuserinteraction",
-        true
-      )
-    ) {
-      // We need user interaction and don't have it.
-      return { allow: false };
-    }
-
-    const principal =
-      perm.type === "storage-access"
-        ? aRequest.principal
-        : aRequest.topLevelPrincipal;
-
-    let allowOrDeny;
-    try {
-      allowOrDeny = await this.sendQuery("GeckoView:ContentPermission", {
-        uri: principal.URI.displaySpec,
-        thirdPartyOrigin: aRequest.principal.origin,
-        principal: lazy.E10SUtils.serializePrincipal(principal),
-        perm: perm.type,
-        value: perm.capability,
-        contextId: principal.originAttributes.geckoViewSessionContextId ?? null,
-        privateMode: principal.privateBrowsingId != 0,
-      });
-
-      if (allowOrDeny === Services.perms.ALLOW_ACTION) {
-        // Ask for app permission after asking for content permission.
-        if (perm.type === "geolocation") {
-          const granted = await this.getAppPermissions([
-            PERM_ACCESS_FINE_LOCATION,
-          ]);
-          allowOrDeny = granted
-            ? Services.perms.ALLOW_ACTION
-            : Services.perms.DENY_ACTION;
-        }
-      }
-    } catch (error) {
-      console.error("Permission error:", error);
-      allowOrDeny = Services.perms.DENY_ACTION;
-    }
-
-    // Manually release the target request here to facilitate garbage collection.
-    aRequest = undefined;
-
-    const allow = allowOrDeny === Services.perms.ALLOW_ACTION;
-
-    // The storage access code adds itself to the perm manager; no need for us to do it.
-    if (perm.type === "storage-access") {
-      if (allow) {
-        return { allow, permission: { "storage-access": "allow" } };
-      }
-      return { allow };
-    }
-
-    Services.perms.addFromPrincipal(
-      principal,
-      perm.type,
-      allowOrDeny,
-      Services.perms.EXPIRE_NEVER
-    );
-
-    return { allow };
-  }
 }
 
 const { debug, warn } = GeckoViewPermissionChild.initLogging(
diff --git a/mobile/shared/actors/GeckoViewPermissionParent.sys.mjs b/mobile/shared/actors/GeckoViewPermissionParent.sys.mjs
index 3735721bd17..efd6d1e8dd9 100644
--- a/mobile/shared/actors/GeckoViewPermissionParent.sys.mjs
+++ b/mobile/shared/actors/GeckoViewPermissionParent.sys.mjs
@@ -50,10 +50,7 @@ export class GeckoViewPermissionParent extends GeckoViewActorParent {
     debug`receiveMessage ${aMessage.name}`;
 
     switch (aMessage.name) {
-      case "GetAppPermissions": {
-        return this.getAppPermissions(aMessage.data);
-      }
-      case "AddCameraPermission": {
+      case "GeckoView:AddCameraPermission": {
         return this.addCameraPermission();
       }
       case "GeckoView:MediaPermission": {
@@ -68,12 +65,6 @@ export class GeckoViewPermissionParent extends GeckoViewActorParent {
           aMessage.data
         );
       }
-      case "GeckoView:ContentPermission": {
-        return this.eventDispatcher.sendRequestForResult(
-          "GeckoView:ContentPermission",
-          aMessage.data
-        );
-      }
     }
 
     return super.receiveMessage(aMessage);
diff --git a/mobile/shared/components/geckoview/GeckoViewPermission.sys.mjs b/mobile/shared/components/geckoview/GeckoViewPermission.sys.mjs
index 51cc0f5588b..3e5107db427 100644
--- a/mobile/shared/components/geckoview/GeckoViewPermission.sys.mjs
+++ b/mobile/shared/components/geckoview/GeckoViewPermission.sys.mjs
@@ -4,18 +4,28 @@
 
 import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";
 
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
+  EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
+});
+
+const PERM_ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION";
+
+const MAPPED_TO_EXTENSION_PERMISSIONS = [
+  "persistent-storage",
+  // TODO(Bug 1336194): support geolocation manifest permission
+  // (see https://bugzilla.mozilla.org/show_bug.cgi?id=1336194#c17)l
+];
+
 export class GeckoViewPermission {
   constructor() {
     this.wrappedJSObject = this;
   }
 
   async prompt(aRequest) {
-    const window = aRequest.window
-      ? aRequest.window
-      : aRequest.element.ownerGlobal;
-
-    const actor = window.windowGlobalChild.getActor("GeckoViewPermission");
-    const result = await actor.promptPermission(aRequest);
+    const result = await this.promptPermission(aRequest);
     if (!result.allow) {
       aRequest.cancel();
     } else {
@@ -24,6 +34,159 @@ export class GeckoViewPermission {
       aRequest.allow(permission);
     }
   }
+
+  // Some WebAPI permissions can be requested and granted to extensions through the
+  // the extension manifest.json, which the user have been already prompted for
+  // (e.g. at install time for the one listed in the manifest.json permissions property,
+  // or at runtime through the optional_permissions property and the permissions.request
+  // WebExtensions API method).
+  //
+  // WebAPI permission that are expected to be mapped to extensions permissions are listed
+  // in the MAPPED_TO_EXTENSION_PERMISSIONS array.
+  //
+  // @param {nsIContentPermissionType} perm
+  //        The WebAPI permission being requested
+  // @param {nsIContentPermissionRequest} aRequest
+  //        The nsIContentPermissionRequest as received by the promptPermission method.
+  //
+  // @returns {null | { allow: boolean, permission: Object }
+  //          Returns null if the request was not handled and should continue with the
+  //          regular permission prompting flow, otherwise it returns an object to
+  //          allow or disallow the permission request right away.
+  checkIfGrantedByExtensionPermissions(perm, aRequest) {
+    if (!aRequest.principal.addonPolicy) {
+      // Not an extension, continue with the regular permission prompting flow.
+      return null;
+    }
+
+    // Return earlier and continue with the regular permission prompting flow if the
+    // the permission is no one that can be requested from the extension manifest file.
+    if (!MAPPED_TO_EXTENSION_PERMISSIONS.includes(perm.type)) {
+      return null;
+    }
+
+    // Disallow if the extension is not active anymore.
+    if (!aRequest.principal.addonPolicy.active) {
+      return { allow: false };
+    }
+
+    // Check if the permission have been already granted to the extension, if it is allow it right away.
+    const isGranted =
+      Services.perms.testPermissionFromPrincipal(
+        aRequest.principal,
+        perm.type
+      ) === Services.perms.ALLOW_ACTION;
+    if (isGranted) {
+      return {
+        allow: true,
+        permission: { [perm.type]: Services.perms.ALLOW_ACTION },
+      };
+    }
+
+    // continue with the regular permission prompting flow otherwise.
+    return null;
+  }
+
+  async promptPermission(aRequest) {
+    // Only allow exactly one permission request here.
+    const types = aRequest.types.QueryInterface(Ci.nsIArray);
+    if (types.length !== 1) {
+      return { allow: false };
+    }
+
+    const perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
+
+    // Check if the request principal is an extension principal and if the permission requested
+    // should be already granted based on the extension permissions (or disallowed right away
+    // because the extension is not enabled anymore.
+    const extensionResult = this.checkIfGrantedByExtensionPermissions(
+      perm,
+      aRequest
+    );
+    if (extensionResult) {
+      return extensionResult;
+    }
+
+    if (
+      perm.type === "desktop-notification" &&
+      !aRequest.hasValidTransientUserGestureActivation &&
+      Services.prefs.getBoolPref(
+        "dom.webnotifications.requireuserinteraction",
+        true
+      )
+    ) {
+      // We need user interaction and don't have it.
+      return { allow: false };
+    }
+
+    const principal =
+      perm.type === "storage-access"
+        ? aRequest.principal
+        : aRequest.topLevelPrincipal;
+
+    const window = aRequest.window
+      ? aRequest.window
+      : aRequest.element.ownerGlobal;
+
+    const windowEventDispatcher = lazy.EventDispatcher.for(window);
+
+    let allowOrDeny;
+    try {
+      allowOrDeny = await windowEventDispatcher.sendRequestForResult(
+        "GeckoView:ContentPermission",
+        {
+          uri: principal.URI.displaySpec,
+          thirdPartyOrigin: aRequest.principal.origin,
+          principal: lazy.E10SUtils.serializePrincipal(principal),
+          perm: perm.type,
+          value: perm.capability,
+          contextId:
+            principal.originAttributes.geckoViewSessionContextId ?? null,
+          privateMode: principal.privateBrowsingId != 0,
+        }
+      );
+
+      if (allowOrDeny === Services.perms.ALLOW_ACTION) {
+        // Ask for app permission after asking for content permission.
+        if (perm.type === "geolocation") {
+          const actor = window.browsingContext.currentWindowGlobal.getActor(
+            "GeckoViewPermission"
+          );
+          const granted = await actor.getAppPermissions([
+            PERM_ACCESS_FINE_LOCATION,
+          ]);
+          allowOrDeny = granted
+            ? Services.perms.ALLOW_ACTION
+            : Services.perms.DENY_ACTION;
+        }
+      }
+    } catch (error) {
+      console.error("Permission error:", error);
+      allowOrDeny = Services.perms.DENY_ACTION;
+    }
+
+    // Manually release the target request here to facilitate garbage collection.
+    aRequest = undefined;
+
+    const allow = allowOrDeny === Services.perms.ALLOW_ACTION;
+
+    // The storage access code adds itself to the perm manager; no need for us to do it.
+    if (perm.type === "storage-access") {
+      if (allow) {
+        return { allow, permission: { "storage-access": "allow" } };
+      }
+      return { allow };
+    }
... (file diff truncated)
Loading diff…