CVE-2025-0237
Overview
Background
- WebChannel
- A Firefox mechanism for bidirectional, principal-scoped messaging between privileged chrome code and specific trusted web content (e.g. Firefox Accounts).
- Principal
- A security object representing the origin/authority of content, used to decide what a message is allowed to do or which channel it may reach.
- `JSWindowActor`
- The process-spanning actor pair (
WebChannelChildin the content process,WebChannelParentin the parent process) that relays WebChannel events across the process boundary. - `documentPrincipal`
- A parent-process-derived property on the actor’s
managergiving the authoritative principal of the document that actually sent the message.
Root Cause Analysis
In the vulnerable code path, WebChannelChild._onMessageToChrome computed a principal from the content-side event target and shipped it inside the IPC payload via sendAsyncMessage, and WebChannelParent.receiveMessage then trusted that attacker-reachable msg.data.principal field verbatim when building sendingContext. This violated the invariant that a security principal used for authorization decisions must be derived in the trusted parent process and never accepted from a less-privileged content process, since a compromised or malicious content process could forge the principal value to impersonate a privileged origin.
The fix removes the principal from the child’s outgoing message entirely and has the parent set principal: this.manager.documentPrincipal, so the principal is inferred authoritatively from the actor’s own document rather than taken from the wire. Because the parent no longer relies on a caller-supplied principal, the old ERRNO_MISSING_PRINCIPAL missing-principal branch becomes dead and delivery falls through directly to WebChannelBroker.tryToDeliver, which validates the channel against the now-trustworthy principal.
principal that the parent used for channel authorization, i.e. trusting attacker-controlled data as an identity; the fix eliminates that trust by deriving the principal from this.manager.documentPrincipal in the parent.Attack Path
- Reach the WebChannel API
A malicious or compromised content process dispatches a
WebChannelMessageToChromeevent so thatWebChannelChild._onMessageToChromefires and packages a cross-process message. - Forge the principal
The attacker arranges the event target so the
principalplaced into the IPC payload reflects a privileged or trusted origin rather than the attacker’s real origin. - Cross the boundary with a lie
sendAsyncMessage("WebChannelMessageToChrome", ...)carries the forgedmsg.data.principaltoWebChannelParent.receiveMessagein the parent process. - Bypass channel scoping
The parent builds
sendingContextfrom the forged principal and passes it toWebChannelBroker.tryToDeliver, which matches the message to a privileged channel it should not have access to. - Escalate The attacker interacts with a chrome-privileged WebChannel endpoint under a spoofed trusted identity, achieving privilege escalation across the content/parent trust boundary.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
_onMessageToChrometoolkit/actors/WebChannelChild.sys.mjs |
modified | |
iftoolkit/actors/WebChannelChild.sys.mjs |
modified | |
iftoolkit/actors/WebChannelParent.sys.mjs |
modified |
Files Changed
toolkit/actors/WebChannelChild.sys.mjstoolkit/actors/WebChannelParent.sys.mjs
Audit Directions
- Content-supplied security objectsSearch for
sendAsyncMessage/actor IPC payloads that ship aprincipal, origin, or capability and are trusted by the parent, and replace them with parent-derived values likethis.manager.documentPrincipal. - Cross-process authorization decisionsReview any parent-process code that authorizes access (channel matching, permission checks) using fields read from
msg.data, since those fields are attacker-influenced. - Deprecated-but-live error branchesAudit code where removing a caller-supplied field leaves prior validation branches (e.g. missing-principal checks) that may mask missing authoritative re-derivation elsewhere.
Patch
diff --git a/toolkit/actors/WebChannelChild.sys.mjs b/toolkit/actors/WebChannelChild.sys.mjs
index d84659bcde7..b7a37238aa8 100644
--- a/toolkit/actors/WebChannelChild.sys.mjs
+++ b/toolkit/actors/WebChannelChild.sys.mjs
@@ -21,11 +21,6 @@ export class WebChannelChild extends JSWindowActorChild {
}
_onMessageToChrome(e) {
- // If target is window then we want the document principal, otherwise fallback to target itself.
- let principal = e.target.nodePrincipal
- ? e.target.nodePrincipal
- : e.target.document.nodePrincipal;
-
if (e.detail) {
if (typeof e.detail != "string") {
console.error("WebChannelMessageToChrome must only send strings");
@@ -39,7 +34,6 @@ export class WebChannelChild extends JSWindowActorChild {
this.sendAsyncMessage("WebChannelMessageToChrome", {
contentData: e.detail,
eventTarget,
- principal,
});
} else {
console.error("WebChannel message failed. No message detail.");
diff --git a/toolkit/actors/WebChannelParent.sys.mjs b/toolkit/actors/WebChannelParent.sys.mjs
index 98b3ad093a3..c5d78235cc4 100644
--- a/toolkit/actors/WebChannelParent.sys.mjs
+++ b/toolkit/actors/WebChannelParent.sys.mjs
@@ -5,7 +5,9 @@
import { WebChannelBroker } from "resource://gre/modules/WebChannel.sys.mjs";
-const ERRNO_MISSING_PRINCIPAL = 1;
+// Note: ERRNO 1 deprecated and unused.
+// We used to err for cases where the child did not send a principal,
+// but now we infer it from the actor.
const ERRNO_NO_SUCH_CHANNEL = 2;
export class WebChannelParent extends JSWindowActorParent {
@@ -15,7 +17,7 @@ export class WebChannelParent extends JSWindowActorParent {
browsingContext: this.browsingContext,
browser: this.browsingContext.top.embedderElement,
eventTarget: msg.data.eventTarget,
- principal: msg.data.principal,
+ principal: this.manager.documentPrincipal,
};
// data must be a string except for a few legacy origins allowed by browser-content.js.
if (typeof data == "string") {
@@ -28,28 +30,19 @@ export class WebChannelParent extends JSWindowActorParent {
}
if (data && data.id) {
- if (!msg.data.principal) {
+ let validChannelFound = WebChannelBroker.tryToDeliver(
+ data,
+ sendingContext
+ );
+
+ // if no valid origins send an event that there is no such valid channel
+ if (!validChannelFound) {
this._sendErrorEventToContent(
data.id,
sendingContext,
- ERRNO_MISSING_PRINCIPAL,
- "Message principal missing"
- );
- } else {
- let validChannelFound = WebChannelBroker.tryToDeliver(
- data,
- sendingContext
+ ERRNO_NO_SUCH_CHANNEL,
+ "No Such Channel"
);
-
- // if no valid origins send an event that there is no such valid channel
- if (!validChannelFound) {
- this._sendErrorEventToContent(
- data.id,
- sendingContext,
- ERRNO_NO_SUCH_CHANNEL,
- "No Such Channel"
- );
- }
}
} else {
console.error("WebChannel channel id missing");