High CVSS 9.1 firefox Policy Bypass 🔧 Commit mapped

Overview

High
Severity
9.1
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionA process isolation vulnerability in Firefox stemmed from improper handling of javascript: URIs, which could allow content to execute in the top-level document's process instead of the intended frame, potentially enabling a sandbox escape.
ComponentCore
Bug ClassPolicy Bypass
Tracker1958350
Fix commit0e460c11eca8 (firefox) +34/-4
CWECWE-653
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
CISA KEVNot listed
CreditedNika Layzell
Disclosed2025-04-29

Background

Process isolation / `remoteType`
Firefox assigns each content document to a content process identified by a remoteType (e.g. webIsolated=..., privilegedabout, extension). Keeping a document’s content in the correct process is the core of site isolation (Fission).
`openWindowInfo` / pop-up creation
When content opens a new window (window.open, pop-ups), tabbrowser.js constructs the new browser and must decide its remoteType. The opener’s process is the relevant trust context for what will load there.
`triggeringRemoteType`
A new property (threaded via an nsIPropertyBag2 in window.arguments[1]) carrying the remote type of the process that caused the pop-up to be created, so the new window can inherit it.
`javascript:` URIs
A javascript: load executes in the context/process of the document it targets. If a pop-up that should run in the opener’s (web content) process is instead created in the default or top-level/privileged process, a javascript: URI can execute there.
`nsContextMenu` `remoteType`
The context menu records which process it is operating against; it previously read this from this.actor?.domProcess?.remoteType.

Root Cause Analysis

When a remote-opener pop-up was created, tabbrowser.js hard-coded the new browser’s remote type to E10SUtils.DEFAULT_REMOTE_TYPE (remoteType = E10SUtils.DEFAULT_REMOTE_TYPE; for openWindowInfo.isRemote, and DEFAULT_REMOTE_TYPE again in the createBrowser/updateBrowserRemoteness path) rather than the remote type of the process responsible for opening it. Because the default remote type need not match the opener’s process, content that should have been confined to the opener’s content process could be placed in a different process (the advisory describes content executing in the top-level document’s process instead of the intended frame). A javascript: URI loaded into such a pop-up then runs in that wrong, potentially more-privileged, process — a process-isolation break that the advisory frames as enabling a sandbox escape.

The patch threads the opener’s process identity through to the new window. It reads triggeringRemoteType from the nsIPropertyBag2 extra options (extraOptions.getPropertyAsACString("triggeringRemoteType")) and uses it as the fallback remote type wherever DEFAULT_REMOTE_TYPE was previously assumed: remoteType = triggeringRemoteType ?? E10SUtils.DEFAULT_REMOTE_TYPE; and the createBrowser(... triggeringRemoteType ?? E10SUtils.DEFAULT_REMOTE_TYPE ...) call; it also propagates triggeringRemoteType into the createBrowser options and into updateBrowserRemoteness, where if (!preferredRemoteType && triggeringRemoteType) preferredRemoteType = triggeringRemoteType; ensures the opener’s process wins over the generic default. A ChromeUtils.releaseAssert(!triggeringRemoteType ...) guards the no-uriToLoad case in automation. Separately, nsContextMenu.sys.mjs is corrected to read this.remoteType = this.actor.manager.domProcess.remoteType; from the actor’s manager (and later in setup) instead of the earlier this.actor?.domProcess?.remoteType, so the context menu reflects the actual document process. The invariant restored: a pop-up/new document loads in the process of the content that triggered it, not a hard-coded default.

Key insight
Pop-up windows opened by remote content were assigned the hard-coded DEFAULT_REMOTE_TYPE instead of the opener’s triggeringRemoteType, so content (and javascript: URIs) could execute in the wrong, more-privileged process; the fix propagates the triggering process’s remote type as the preferred/fallback remote type.

Attack Path

  1. Content foothold Attacker content runs in a content process and opens a new window/pop-up with a load it controls (inferred standard window.open reachability; the diff is chrome tab-creation logic).
  2. Mis-assigned process Because the new browser defaults to DEFAULT_REMOTE_TYPE rather than the opener’s remote type, the pop-up is created in a process that does not match the opener’s intended isolation.
  3. `javascript:` load The attacker navigates the pop-up to a javascript: URI (or similar) that executes in that wrong process — per the advisory, the top-level document’s process rather than the intended frame.
  4. Isolation break Attacker script now runs with the identity/privileges of the wrong process, crossing the site-isolation boundary.
  5. Primitive Execution of content in an unintended, more-privileged process, which the advisory says could enable a sandbox escape (escalation to full escape inferred; the diff substantiates the process-selection flaw).

Impact Assessment

This is a site/process-isolation (Fission) logic flaw affecting content-process confinement; it is web-reachable with no special privilege (CVSS 9.1, PR:N). The substantiated primitive is loading attacker-influenced content, including javascript: URIs, in the wrong content process, undermining process isolation; the advisory’s “sandbox escape” is the downstream consequence and is (inferred) beyond what the diff alone proves. Preconditions: the attacker must open a pop-up/new window whose remote-type decision hits the patched default path.

Changed Functions

FunctionChangeNotes
if
browser/components/tabbrowser/content/tabbrowser.js
modified

Files Changed

  • browser/base/content/nsContextMenu.sys.mjs
  • browser/components/tabbrowser/content/tabbrowser.js

Audit Directions

  • Hard-coded `DEFAULT_REMOTE_TYPE`
    Audit other window/tab/browser creation paths (session restore, view-source, printing, extension pop-ups) that assign DEFAULT_REMOTE_TYPE without considering the triggering/opener process.
  • `javascript:` and about: loads into new browsers
    Review where newly created browsers can receive javascript:/about: loads before remoteness is finalized.
  • Opener-process propagation
    Check all nsIPropertyBag2/openWindowInfo plumbing to ensure triggeringRemoteType reaches every remoteness decision, including nested/chained pop-ups.
diff --git a/browser/base/content/nsContextMenu.sys.mjs b/browser/base/content/nsContextMenu.sys.mjs
index dfcbb381792..3bd0ee8af8b 100644
--- a/browser/base/content/nsContextMenu.sys.mjs
+++ b/browser/base/content/nsContextMenu.sys.mjs
@@ -176,8 +176,6 @@ export class nsContextMenu {
       nsContextMenu.contentData = null;
     }
 
-    this.remoteType = this.actor?.domProcess?.remoteType;
-
     const { gBrowser } = this.window;
 
     this.shouldDisplay = context.shouldDisplay;
@@ -276,6 +274,8 @@ export class nsContextMenu {
         );
     }
 
+    this.remoteType = this.actor.manager.domProcess.remoteType;
+
     this.selectedText = this.selectionInfo.text;
     this.isTextSelected = !!this.selectedText.length;
     this.webExtBrowserType = this.browser.getAttribute(
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index 3a1d02bca2c..08895d9c30d 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -457,6 +457,21 @@
         openWindowInfo = window.arguments[11];
       }
 
+      let extraOptions;
+      if (window.arguments?.[1] instanceof Ci.nsIPropertyBag2) {
+        extraOptions = window.arguments[1];
+      }
+
+      // If our opener provided a remoteType which was responsible for creating
+      // this pop-up window, we'll fall back to using that remote type when no
+      // other remote type is available.
+      let triggeringRemoteType;
+      if (extraOptions?.hasKey("triggeringRemoteType")) {
+        triggeringRemoteType = extraOptions.getPropertyAsACString(
+          "triggeringRemoteType"
+        );
+      }
+
       let tabArgument = gBrowserInit.getTabToAdopt();
 
       // If we have a tab argument with browser, we use its remoteType. Otherwise,
@@ -484,7 +499,7 @@
       } else if (openWindowInfo) {
         userContextId = openWindowInfo.originAttributes.userContextId;
         if (openWindowInfo.isRemote) {
-          remoteType = E10SUtils.DEFAULT_REMOTE_TYPE;
+          remoteType = triggeringRemoteType ?? E10SUtils.DEFAULT_REMOTE_TYPE;
         } else {
           remoteType = E10SUtils.NOT_REMOTE;
         }
@@ -503,7 +518,7 @@
             uriToLoad,
             gMultiProcessBrowser,
             gFissionBrowser,
-            E10SUtils.DEFAULT_REMOTE_TYPE,
+            triggeringRemoteType ?? E10SUtils.DEFAULT_REMOTE_TYPE,
             null,
             oa
           );
@@ -514,6 +529,13 @@
           // would mean that `gBrowser.selectedBrowser` might not always exist,
           // which is the current assumption.
 
+          if (Cu.isInAutomation) {
+            ChromeUtils.releaseAssert(
+              !triggeringRemoteType,
+              "Unexpected triggeringRemoteType with no uriToLoad"
+            );
+          }
+
           // In this case we default to the privileged about process as that's
           // the best guess we can make, and we'll likely need it eventually.
           remoteType = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
@@ -2756,6 +2778,7 @@
           initialBrowsingContextGroupId,
           openWindowInfo,
           skipLoad,
+          triggeringRemoteType,
         }));
 
         if (focusUrlBar) {
@@ -3318,8 +3341,15 @@
         initialBrowsingContextGroupId,
         openWindowInfo,
         skipLoad,
+        triggeringRemoteType,
       }
     ) {
+      // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
+      // we have a remote triggering remote type, use that instead.
+      if (!preferredRemoteType && triggeringRemoteType) {
+        preferredRemoteType = triggeringRemoteType;
+      }
+
       // If we don't have a preferred remote type, and we have a remote
       // opener, use the opener's remote type.
       if (!preferredRemoteType && openerBrowser) {
Loading diff…