CVE-2024-9392
Overview
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.
Attack Path
- 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.
- 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.
- Bypass existing checks
Because
doContentSecurityCheckonly validated the triggering principal and load flags, none of the prior checks noticed that the result origin disagreed with the channel URI’s origin. - 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
Files Changed
caps/nsScriptSecurityManager.hdom/security/nsContentSecurityManager.cppdom/security/nsContentSecurityManager.h
Audit Directions
- Content-process-supplied principalsAudit 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 coherenceLook 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 principalsReview comparisons involving null/opaque principals to ensure they follow
GetPrecursorPrincipaland use site-origin comparison, since naive checks can miss the real origin behind an opaque principal.
Patch
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;
};