Low firefox Spoofing 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionSpoofing issue in Firefox for Android
ComponentCore
Bug ClassSpoofing
Tracker2020253
Fix commit594f5a94549b (firefox) +36/-1
CISA KEVNot listed
CreditedKhiem Tran
Disclosed2026-07-21

Background

GeckoView
Mozilla’s embeddable web engine component that renders web content inside Firefox for Android and other Android apps.
JSWindowActor
a paired parent/child messaging abstraction (GeckoViewPrompt here) that lets privileged parent-process code and content-process code exchange async messages across the process boundary.
`triggeringPrincipal`
the security principal recorded as the initiator of a navigation, used to attribute and authorize a load.
`BrowsingContext`
the object representing a browsing context (tab/frame); browsingContext.top refers to the top-level frame of that tree.

Root Cause Analysis

The vulnerable path lived in PromptFactory inside GeckoViewPrompt.sys.mjs, where accepting a redirect prompt performed the navigation directly in content-reachable code via dwi.top.location.href = redirectURISpec. This assignment drove the top-level location without routing through the trusted parent process and without an explicit, correct triggering principal, so the navigation was performed in a context where the redirect could be attributed to the wrong origin and used to spoof the displayed content.

The fix moves the actual navigation into the parent process: the child now sends a GeckoView:UnblockRedirect message and the new GeckoViewPromptParent actor performs this.browsingContext.top.loadURI(uri, { triggeringPrincipal: sourceWG.documentPrincipal }). This restores the invariant that a top-level navigation is initiated by the parent with the source document’s principal as the explicit triggering principal, rather than by an implicit location.href write. Because loadURI is now driven from the parent with a correct principal, the redirect is properly attributed and can no longer be leveraged to spoof.

Key insight
The single core mistake was performing a top-level redirect via a content-side location.href assignment that lacked an explicit, correct triggering principal, allowing origin confusion; the fix relocates the navigation to a parent-process actor that calls loadURI with the source window global’s documentPrincipal as the triggering principal.

Attack Path

  1. Trigger a redirect prompt A malicious page causes GeckoView to present a redirect confirmation prompt whose redirectURISpec points at content the attacker wants to display.
  2. User accepts the prompt When the user confirms, the old code executes dwi.top.location.href = redirectURISpec in content-reachable context.
  3. Mis-attributed navigation The top-level location changes without a properly asserted triggering principal, so the resulting load is attributed in a way that misrepresents its true origin.
  4. Spoofed presentation The attacker leverages the mis-attributed top-level navigation to spoof what the user believes they are viewing in Firefox for Android.

Impact Assessment

An attacker gains a user-interface/origin spoofing primitive in Firefox for Android, misleading the user about the origin or content of the top-level page after a redirect prompt is accepted. The issue manifests in the GeckoView content/prompt handling flow and requires the user to interact with (accept) a redirect prompt initiated by attacker-controlled content. Impact is rated low, consistent with a spoofing rather than a memory-safety or code-execution issue.

Changed Functions

FunctionChangeNotes
receiveMessage
mobile/shared/actors/GeckoViewPromptParent.sys.mjs
modified
if
mobile/shared/actors/GeckoViewPromptParent.sys.mjs
modified
unblockRedirect
mobile/shared/actors/GeckoViewPromptParent.sys.mjs
modified
if
mobile/shared/components/geckoview/GeckoViewPrompt.sys.mjs
modified

Files Changed

  • mobile/shared/actors/GeckoViewPromptParent.sys.mjs
  • mobile/shared/actors/moz.build
  • mobile/shared/components/geckoview/GeckoViewPrompt.sys.mjs
  • mobile/shared/components/geckoview/GeckoViewStartup.sys.mjs

Audit Directions

  • Content-side top-level navigation
    Flag any location.href/location.assign write to top (or another cross-context frame) performed in content-reachable code, and require it be routed through a parent actor with an explicit triggeringPrincipal.
  • Principal-less loadURI
    Audit loadURI/fixupAndLoadURIString call sites for a missing or incorrect triggeringPrincipal, which can cause origin mis-attribution and spoofing.
  • Prompt/redirect callbacks
    Review GeckoView prompt-response handlers that navigate on user acceptance to confirm the navigation is executed in the trusted parent process with the originating document’s principal.
diff --git a/mobile/shared/actors/GeckoViewPromptParent.sys.mjs b/mobile/shared/actors/GeckoViewPromptParent.sys.mjs
new file mode 100644
index 00000000000..9cc3eb684ad
--- /dev/null
+++ b/mobile/shared/actors/GeckoViewPromptParent.sys.mjs
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { GeckoViewActorParent } from "resource://gre/modules/GeckoViewActorParent.sys.mjs";
+
+export class GeckoViewPromptParent extends GeckoViewActorParent {
+  receiveMessage({ name, data }) {
+    if (name === "GeckoView:UnblockRedirect") {
+      this.unblockRedirect(data.redirectURISpec);
+      return null;
+    }
+
+    return super.receiveMessage({ name, data });
+  }
+
+  unblockRedirect(aRedirectURISpec) {
+    const sourceWG = this.browsingContext.currentWindowGlobal;
+    if (!sourceWG) {
+      return;
+    }
+
+    const uri = Services.io.newURI(aRedirectURISpec);
+    this.browsingContext.top.loadURI(uri, {
+      triggeringPrincipal: sourceWG.documentPrincipal,
+    });
+  }
+}
diff --git a/mobile/shared/actors/moz.build b/mobile/shared/actors/moz.build
index f9572acc3d1..56ed7942f65 100644
--- a/mobile/shared/actors/moz.build
+++ b/mobile/shared/actors/moz.build
@@ -23,6 +23,7 @@ FINAL_TARGET_FILES.actors += [
     "GeckoViewPromptChild.sys.mjs",
     "GeckoViewPrompterChild.sys.mjs",
     "GeckoViewPrompterParent.sys.mjs",
+    "GeckoViewPromptParent.sys.mjs",
     "GeckoViewPushParent.sys.mjs",
     "GeckoViewSettingsChild.sys.mjs",
     "LoadURIDelegateChild.sys.mjs",
diff --git a/mobile/shared/components/geckoview/GeckoViewPrompt.sys.mjs b/mobile/shared/components/geckoview/GeckoViewPrompt.sys.mjs
index e195b48ca75..82622affebf 100644
--- a/mobile/shared/components/geckoview/GeckoViewPrompt.sys.mjs
+++ b/mobile/shared/components/geckoview/GeckoViewPrompt.sys.mjs
@@ -345,7 +345,10 @@ export class PromptFactory {
       },
       ({ response }) => {
         if (response && dwi) {
-          dwi.top.location.href = redirectURISpec;
+          const actor = dwi.windowGlobalChild.getActor("GeckoViewPrompt");
+          actor.sendAsyncMessage("GeckoView:UnblockRedirect", {
+            redirectURISpec,
+          });
         }
       }
     );
diff --git a/mobile/shared/components/geckoview/GeckoViewStartup.sys.mjs b/mobile/shared/components/geckoview/GeckoViewStartup.sys.mjs
index 6996d0f77a2..9567abab0db 100644
--- a/mobile/shared/components/geckoview/GeckoViewStartup.sys.mjs
+++ b/mobile/shared/components/geckoview/GeckoViewStartup.sys.mjs
@@ -68,6 +68,9 @@ const JSWINDOWACTORS = {
     includeChrome: true,
   },
   GeckoViewPrompt: {
+    parent: {
+      esModuleURI: "resource:///actors/GeckoViewPromptParent.sys.mjs",
+    },
     child: {
       esModuleURI: "resource:///actors/GeckoViewPromptChild.sys.mjs",
       events: {
Loading diff…