Medium CVSS 5.4 firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionThe WebChannel API, which is used to transport various information across processes, did not check the sending principal but rather accepted the principal being sent. This could have led to privilege escalation attacks.
ComponentToolkit
Bug ClassLogic Error
Tracker1915257
Fix commit882e164a6408 (firefox) +13/-26
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedAndrew McCreight
Disclosed2025-01-07

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 (WebChannelChild in the content process, WebChannelParent in the parent process) that relays WebChannel events across the process boundary.
`documentPrincipal`
A parent-process-derived property on the actor’s manager giving 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.

Key insight
The single core mistake was letting the content process supply the security 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

  1. Reach the WebChannel API A malicious or compromised content process dispatches a WebChannelMessageToChrome event so that WebChannelChild._onMessageToChrome fires and packages a cross-process message.
  2. Forge the principal The attacker arranges the event target so the principal placed into the IPC payload reflects a privileged or trusted origin rather than the attacker’s real origin.
  3. Cross the boundary with a lie sendAsyncMessage("WebChannelMessageToChrome", ...) carries the forged msg.data.principal to WebChannelParent.receiveMessage in the parent process.
  4. Bypass channel scoping The parent builds sendingContext from the forged principal and passes it to WebChannelBroker.tryToDeliver, which matches the message to a privileged channel it should not have access to.
  5. 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

An attacker executing in a content process could spoof the sending principal and thereby address WebChannels reserved for privileged/trusted origins, gaining a privilege-escalation foothold across the content-to-parent trust boundary. The gain manifests in the parent (chrome) process’s handling of WebChannel messages, where the forged identity is used for channel authorization. The precondition is the ability to run script or otherwise dispatch WebChannel events in a content process, e.g. via a compromised or malicious page, which is the intended threat model for this cross-process API.

Changed Functions

FunctionChangeNotes
_onMessageToChrome
toolkit/actors/WebChannelChild.sys.mjs
modified
if
toolkit/actors/WebChannelChild.sys.mjs
modified
if
toolkit/actors/WebChannelParent.sys.mjs
modified

Files Changed

  • toolkit/actors/WebChannelChild.sys.mjs
  • toolkit/actors/WebChannelParent.sys.mjs

Audit Directions

  • Content-supplied security objects
    Search for sendAsyncMessage/actor IPC payloads that ship a principal, origin, or capability and are trusted by the parent, and replace them with parent-derived values like this.manager.documentPrincipal.
  • Cross-process authorization decisions
    Review 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 branches
    Audit code where removing a caller-supplied field leaves prior validation branches (e.g. missing-principal checks) that may mask missing authoritative re-derivation elsewhere.
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");
Loading diff…