High CVSS 9.8 firefox Cross Origin 🔧 Commit mapped

Overview

High
Severity
9.8
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionA compromised content process could have allowed for the arbitrary loading of cross-origin pages.
ComponentDOM
Bug ClassCross Origin
Tracker1905843
Fix commit95ef09ee461a (firefox) +67/-2
CWECWE-346 (Origin validation error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedJan Drescher and David Klein from IAS, TU Braunschweig
Disclosed2024-10-01

Background

Result principal
The security principal Firefox assigns to a channel’s response, computed by GetChannelResultPrincipalIfNotSandboxed, which determines the origin the loaded content will run under.
Precursor principal
The origin embedded in an opaque/null principal via GetPrecursorPrincipal, recording where a null-origin document actually came from.
Site origin
The scheme-plus-registrable-domain identity of a principal (GetSiteOriginNoSuffix), used here to compare the intended load target against the result.
`ExtContentPolicyType`
The external content-policy classification of a load (e.g. TYPE_DOCUMENT, TYPE_SUBDOCUMENT, TYPE_OBJECT) that identifies top-level and framed navigations.

Root Cause Analysis

The channel load pipeline in nsContentSecurityManager::doContentSecurityCheck validated triggering principals and load flags but never confirmed that the result principal a channel would ultimately produce was coherent with the URI the channel was actually loading. Because the content process supplies principal and channel state over IPC, a compromised content process could construct a channel whose result principal (its origin) diverged from the channel URI being fetched, so a page from one cross-origin site could be committed under a different, attacker-chosen origin. The security invariant violated is that a document’s effective origin must match the origin of the resource retrieved by its channel; nothing enforced that these agree for document/subdocument/object loads.

The fix adds CheckForIncoherentResultPrincipal, which recomputes a principal directly from the channel URI, resolves the result principal (following any precursor), and blocks the load with NS_ERROR_CONTENT_BLOCKED when the two site origins are HTTP(S) cross-origin or when a non-HTTP result origin is paired with an HTTP(S) channel URI. This closes the gap by making origin coherence an explicit, non-bypassable check on every document-level channel rather than trusting content-process-supplied principals.

Key insight
The core mistake was trusting the content process’s result principal without cross-checking it against the origin implied by the channel’s own URI, allowing origin spoofing for cross-origin loads. The fix independently derives a principal from the channel URI and rejects any document/subdocument/object load whose result origin is incoherent with it.

Attack Path

  1. Compromise a content process An attacker first achieves code execution in a sandboxed content process, gaining the ability to forge IPC messages that drive channel and principal state.
  2. Craft a mismatched channel The compromised process initiates a document, subdocument, or object load whose result principal is set to a victim origin while the channel URI actually points at attacker-controlled cross-origin content.
  3. Bypass existing checks Because doContentSecurityCheck only validated the triggering principal and load flags, none of the prior checks noticed that the result origin disagreed with the channel URI’s origin.
  4. Commit under a spoofed origin The load completes and the fetched cross-origin page is committed under the incoherent result principal, letting attacker content run with a victim origin’s privileges or letting arbitrary cross-origin pages be loaded.

Impact Assessment

An attacker who has already compromised a content process gains the ability to load arbitrary cross-origin pages and have them committed under an incoherent, spoofed origin, breaking the same-origin boundary that the sandbox is meant to preserve. This occurs within the renderer/content-process context during document, subdocument, or object navigations, and its sole precondition is prior content-process compromise. The result is a high-severity same-origin-policy bypass usable as an escalation primitive from a partially compromised renderer.

Files Changed

  • caps/nsScriptSecurityManager.h
  • dom/security/nsContentSecurityManager.cpp
  • dom/security/nsContentSecurityManager.h

Audit Directions

  • Content-process-supplied principals
    Audit every load path that accepts a principal or origin from a content process over IPC and verify it is re-derived or cross-checked against the actual channel URI rather than trusted.
  • Result-vs-URI coherence
    Look for other channel or document-commit code that reads the result principal without confirming it matches the resource’s true origin, especially for object, frame, and navigation loads.
  • Precursor and opaque principals
    Review comparisons involving null/opaque principals to ensure they follow GetPrecursorPrincipal and use site-origin comparison, since naive checks can miss the real origin behind an opaque principal.
diff --git a/caps/nsScriptSecurityManager.h b/caps/nsScriptSecurityManager.h
index f97170b145e..5400c9f2254 100644
--- a/caps/nsScriptSecurityManager.h
+++ b/caps/nsScriptSecurityManager.h
@@ -81,8 +81,6 @@ class nsScriptSecurityManager final : public nsIScriptSecurityManager {
                               bool aFromPrivateWindow,
                               uint64_t aInnerWindowID = 0);
 
-  static uint32_t HashPrincipalByOrigin(nsIPrincipal* aPrincipal);
-
   static bool GetStrictFileOriginPolicy() { return sStrictFileOriginPolicy; }
 
   void DeactivateDomainPolicy();
diff --git a/dom/security/nsContentSecurityManager.cpp b/dom/security/nsContentSecurityManager.cpp
index 9181de82657..7ba51b28667 100644
--- a/dom/security/nsContentSecurityManager.cpp
+++ b/dom/security/nsContentSecurityManager.cpp
@@ -1427,6 +1427,9 @@ nsresult nsContentSecurityManager::doContentSecurityCheck(
   rv = CheckAllowLoadByTriggeringRemoteType(aChannel);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  rv = CheckForIncoherentResultPrincipal(aChannel);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   // if dealing with a redirected channel then we have already installed
   // streamlistener and redirect proxies and so we are done.
   if (loadInfo->GetInitialSecurityCheckDone()) {
@@ -1709,3 +1712,66 @@ nsContentSecurityManager::PerformSecurityCheck(
   inAndOutListener.forget(outStreamListener);
   return NS_OK;
 }
+
+nsresult nsContentSecurityManager::CheckForIncoherentResultPrincipal(
+    nsIChannel* aChannel) {
+  nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+  ExtContentPolicyType contentPolicyType =
+      loadInfo->GetExternalContentPolicyType();
+  if (contentPolicyType != ExtContentPolicyType::TYPE_DOCUMENT &&
+      contentPolicyType != ExtContentPolicyType::TYPE_SUBDOCUMENT &&
+      contentPolicyType != ExtContentPolicyType::TYPE_OBJECT) {
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIPrincipal> resultOrPrecursor;
+  nsresult rv = nsScriptSecurityManager::GetScriptSecurityManager()
+                    ->GetChannelResultPrincipalIfNotSandboxed(
+                        aChannel, getter_AddRefs(resultOrPrecursor));
+  NS_ENSURE_SUCCESS(rv, rv);
+  NS_ENSURE_STATE(resultOrPrecursor);
+
+  if (nsCOMPtr<nsIPrincipal> precursor =
+          resultOrPrecursor->GetPrecursorPrincipal()) {
+    resultOrPrecursor = precursor;
+  }
+
+  if (!resultOrPrecursor->GetIsContentPrincipal()) {
+    return NS_OK;
+  }
+
+  nsAutoCString resultSiteOriginNoSuffix;
+  rv = resultOrPrecursor->GetSiteOriginNoSuffix(resultSiteOriginNoSuffix);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsIURI> resultSiteOriginURI;
+  NS_NewURI(getter_AddRefs(resultSiteOriginURI), resultSiteOriginNoSuffix);
+  NS_ENSURE_STATE(resultSiteOriginURI);
+
+  nsCOMPtr<nsIURI> channelURI;
+  aChannel->GetURI(getter_AddRefs(channelURI));
+  NS_ENSURE_STATE(channelURI);
+
+  nsCOMPtr<nsIPrincipal> channelUriPrincipal =
+      BasePrincipal::CreateContentPrincipal(channelURI, {});
+  NS_ENSURE_STATE(channelUriPrincipal);
+
+  nsAutoCString channelUriSiteOrigin;
+  rv = channelUriPrincipal->GetSiteOriginNoSuffix(channelUriSiteOrigin);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsIURI> channelSiteOriginURI;
+  NS_NewURI(getter_AddRefs(channelSiteOriginURI), channelUriSiteOrigin);
+  NS_ENSURE_STATE(channelSiteOriginURI);
+
+  if (nsScriptSecurityManager::IsHttpOrHttpsAndCrossOrigin(
+          resultSiteOriginURI, channelSiteOriginURI) ||
+      (!net::SchemeIsHTTP(resultSiteOriginURI) &&
+       !net::SchemeIsHTTPS(resultSiteOriginURI) &&
+       (net::SchemeIsHTTP(channelSiteOriginURI) ||
+        net::SchemeIsHTTPS(channelSiteOriginURI)))) {
+    return NS_ERROR_CONTENT_BLOCKED;
+  }
+
+  return NS_OK;
+}
diff --git a/dom/security/nsContentSecurityManager.h b/dom/security/nsContentSecurityManager.h
index 17d42e9676f..45757a973c1 100644
--- a/dom/security/nsContentSecurityManager.h
+++ b/dom/security/nsContentSecurityManager.h
@@ -87,6 +87,7 @@ class nsContentSecurityManager : public nsIContentSecurityManager,
   static nsresult CheckAllowLoadInPrivilegedAboutContext(nsIChannel* aChannel);
   static nsresult CheckChannelHasProtocolSecurityFlag(nsIChannel* aChannel);
   static bool CrossOriginEmbedderPolicyAllowsCredentials(nsIChannel* aChannel);
+  static nsresult CheckForIncoherentResultPrincipal(nsIChannel* aChannel);
 
   virtual ~nsContentSecurityManager() = default;
 };
Loading diff…