CVE-2025-4083
Overview
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.jsconstructs the new browser and must decide itsremoteType. The opener’s process is the relevant trust context for what will load there. - `triggeringRemoteType`
- A new property (threaded via an
nsIPropertyBag2inwindow.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, ajavascript: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.
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
- Content foothold
Attacker content runs in a content process and opens a new window/pop-up with a load it controls (inferred standard
window.openreachability; the diff is chrome tab-creation logic). - Mis-assigned process
Because the new browser defaults to
DEFAULT_REMOTE_TYPErather than the opener’s remote type, the pop-up is created in a process that does not match the opener’s intended isolation. - `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. - Isolation break Attacker script now runs with the identity/privileges of the wrong process, crossing the site-isolation boundary.
- 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
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
| Function | Change | Notes |
|---|---|---|
ifbrowser/components/tabbrowser/content/tabbrowser.js |
modified |
Files Changed
browser/base/content/nsContextMenu.sys.mjsbrowser/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_TYPEwithout considering the triggering/opener process. - `javascript:` and about: loads into new browsersReview where newly created browsers can receive
javascript:/about:loads before remoteness is finalized. - Opener-process propagationCheck all
nsIPropertyBag2/openWindowInfoplumbing to ensuretriggeringRemoteTypereaches every remoteness decision, including nested/chained pop-ups.
Patch
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) {