High CVSS 7.5 firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionA permission leak could have occurred from a trusted site to an untrusted site via <code>embed</code> or <code>object</code> elements.
ComponentDOM
Bug ClassLogic Error
Tracker1921733
Fix commite2c68ead0a29 (firefox) +100/-7
CWECWE-281
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedJames Lee
Disclosed2024-10-29

Background

Feature Policy
a per-document set of permission directives (represented by the FeaturePolicy type) that governs which powerful features a document and its subframes may use.
Container Feature Policy
the policy that an embedding element (<iframe>, <object>, <embed>) imposes on the browsing context it hosts, stored on the child’s BrowsingContext so the loaded document inherits the correct restrictions.
`nsObjectLoadingContent`
the shared implementation backing <object> and <embed> elements that manages loading, type resolution, and browsing-context setup for embedded content.
Process Switch
the moment during a top-level or subframe load (driven by DocumentLoadListener::MaybeTriggerProcessSwitch) when Gecko may move a document into a different content process and must carry the correct security context, including the container feature policy, into the new load info.

Root Cause Analysis

The vulnerable path stored the container feature policy for <object> and <embed> elements only inside nsObjectLoadingContent::SetupDocShell, which called MaybeStoreCrossOriginFeaturePolicy once at docshell creation using the raw OwnerDoc()->FeaturePolicy() of the embedding document. That approach violated the invariant that the child browsing context’s stored policy must reflect the embedded element’s own default origin (its data or src URL) at the time the document is actually committed, and it never recomputed the policy when the element was upgraded to a document container or when the load crossed a process boundary. Consequently, when DocumentLoadListener::MaybeTriggerProcessSwitch picked up the load info, the container feature policy was either stale, missing, or scoped to the wrong (trusted embedder) origin, so permissions granted to the trusted embedding site could leak into an untrusted embedded document.

The fix introduces a dedicated RefreshFeaturePolicy step that builds a fresh FeaturePolicy keyed to the element’s default origin via GetFeaturePolicyDefaultOrigin, inherits from the owner document’s policy, and stores it on the browsing context at the correct point in UpgradeLoadToDocument, before the process switch reads it. Because the policy is now recomputed and re-stored whenever the element becomes a container or its URI changes, the child context always carries a policy scoped to the embedded origin rather than the embedder’s.

Key insight
The core mistake was computing and storing the container feature policy exactly once at docshell setup using the embedder document’s origin, instead of deriving it from the embedded element’s own default origin and refreshing it when the element is upgraded to a document container; the fix adds RefreshFeaturePolicy (invoked from UpgradeLoadToDocument) to rebuild the policy with the correct origin before the load’s process switch consumes it.

Attack Path

  1. Embed cross-origin content An attacker gets a trusted page to host an <object> or <embed> element whose data/src points at an untrusted, attacker-controlled origin.
  2. Skip policy refresh During load, SetupDocShell stores the container feature policy just once using the trusted embedder’s FeaturePolicy, and it is never recomputed for the actual embedded origin.
  3. Upgrade to document container The element is upgraded via UpgradeLoadToDocument, creating a browsing context that lacks a policy scoped to the embedded origin.
  4. Process switch reads stale policy DocumentLoadListener::MaybeTriggerProcessSwitch starts the document load with the trusted embedder’s permissions baked into the load info instead of the untrusted origin’s restrictions.
  5. Inherit leaked permissions The untrusted embedded document loads with feature permissions granted to the trusted site, achieving a cross-origin permission leak.

Impact Assessment

An attacker gains the ability to have an untrusted, embedded document inherit permission-policy grants belonging to a trusted embedding site, letting the untrusted origin exercise powerful features it should not have access to. This occurs in the content process handling the embedded browsing context created by an <object> or <embed> element. The precondition is that a trusted page embeds attacker-controlled cross-origin content through these elements and that the load reaches the document-container upgrade and process-switch path.

Changed Functions

FunctionChangeNotes
if
dom/base/nsObjectLoadingContent.cpp
modified
GetFeaturePolicyDefaultOrigin
dom/base/nsObjectLoadingContent.cpp
modified
FeaturePolicy
dom/base/nsObjectLoadingContent.h
modified
Sequence
dom/base/nsObjectLoadingContent.h
modified
HTMLIFrameElement
dom/base/nsObjectLoadingContent.h
modified
if
dom/html/HTMLEmbedElement.cpp
modified
if
dom/html/HTMLObjectElement.cpp
modified

Files Changed

  • dom/base/nsObjectLoadingContent.cpp
  • dom/base/nsObjectLoadingContent.h
  • dom/html/HTMLEmbedElement.cpp
  • dom/html/HTMLObjectElement.cpp
  • netwerk/ipc/DocumentLoadListener.cpp

Audit Directions

  • Once-computed security context
    Look for feature-policy, principal, or origin state that is captured a single time at setup and never refreshed when the element’s URI or role changes.
  • Cross-origin embedding elements
    Audit <object>, <embed>, and <iframe> code paths for places where the embedder’s document policy, rather than the embedded content’s default origin, is propagated into the child browsing context.
  • Process-switch load info
    Review DocumentLoadListener::MaybeTriggerProcessSwitch and related process-switch consumers to confirm every security attribute they read is recomputed and stored before the switch, not left stale from an earlier load stage.
diff --git a/dom/base/nsObjectLoadingContent.cpp b/dom/base/nsObjectLoadingContent.cpp
index 866bcb9abcb..98c1272f8a6 100644
--- a/dom/base/nsObjectLoadingContent.cpp
+++ b/dom/base/nsObjectLoadingContent.cpp
@@ -218,8 +218,6 @@ already_AddRefed<nsIDocShell> nsObjectLoadingContent::SetupDocShell(
     return nullptr;
   }
 
-  MaybeStoreCrossOriginFeaturePolicy();
-
   return docShell.forget();
 }
 
@@ -1554,6 +1552,7 @@ void nsObjectLoadingContent::Destroy() {
 void nsObjectLoadingContent::Traverse(nsObjectLoadingContent* tmp,
                                       nsCycleCollectionTraversalCallback& cb) {
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameLoader);
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFeaturePolicy);
 }
 
 /* static */
@@ -1562,6 +1561,7 @@ void nsObjectLoadingContent::Unlink(nsObjectLoadingContent* tmp) {
     tmp->mFrameLoader->Destroy();
   }
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mFrameLoader);
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mFeaturePolicy);
 }
 
 void nsObjectLoadingContent::UnloadObject(bool aResetState) {
@@ -1720,6 +1720,13 @@ nsObjectLoadingContent::UpgradeLoadToDocument(
     return NS_ERROR_FAILURE;
   }
 
+  // At this point we know that we have a browsing context, so it's time to make
+  // sure that that browsing context gets the correct container feature policy.
+  // This is needed for `DocumentLoadListener::MaybeTriggerProcessSwitch` to be
+  // able to start loading the document with the correct container feature
+  // policy in the load info.
+  RefreshFeaturePolicy();
+
   bc.forget(aBrowsingContext);
   return NS_OK;
 }
@@ -1818,6 +1825,9 @@ void nsObjectLoadingContent::SubdocumentImageLoadComplete(nsresult aResult) {
 
 void nsObjectLoadingContent::MaybeStoreCrossOriginFeaturePolicy() {
   MOZ_DIAGNOSTIC_ASSERT(mFrameLoader);
+  if (!mFrameLoader) {
+    return;
+  }
 
   // If the browsingContext is not ready (because docshell is dead), don't try
   // to create one.
@@ -1831,15 +1841,54 @@ void nsObjectLoadingContent::MaybeStoreCrossOriginFeaturePolicy() {
     return;
   }
 
-  Element* el = AsElement();
+  auto* el = nsGenericHTMLElement::FromNode(AsElement());
   if (!el->IsInComposedDoc()) {
     return;
   }
 
-  FeaturePolicy* featurePolicy = el->OwnerDoc()->FeaturePolicy();
-
-  if (ContentChild* cc = ContentChild::GetSingleton(); cc && featurePolicy) {
+  if (ContentChild* cc = ContentChild::GetSingleton()) {
     Unused << cc->SendSetContainerFeaturePolicy(
-        browsingContext, Some(featurePolicy->ToFeaturePolicyInfo()));
+        browsingContext, Some(mFeaturePolicy->ToFeaturePolicyInfo()));
+  }
+}
+
+/* static */ already_AddRefed<nsIPrincipal>
+nsObjectLoadingContent::GetFeaturePolicyDefaultOrigin(nsINode* aNode) {
+  auto* el = nsGenericHTMLElement::FromNode(aNode);
+  nsCOMPtr<nsIURI> nodeURI;
+  // Different elements keep this in various locations
+  if (el->NodeInfo()->Equals(nsGkAtoms::object)) {
+    el->GetURIAttr(nsGkAtoms::data, nullptr, getter_AddRefs(nodeURI));
+  } else if (el->NodeInfo()->Equals(nsGkAtoms::embed)) {
+    el->GetURIAttr(nsGkAtoms::src, nullptr, getter_AddRefs(nodeURI));
   }
+
+  nsCOMPtr<nsIPrincipal> principal;
+  if (nodeURI) {
+    principal = BasePrincipal::CreateContentPrincipal(
+        nodeURI,
+        BasePrincipal::Cast(el->NodePrincipal())->OriginAttributesRef());
+  } else {
+    principal = el->NodePrincipal();
+  }
+
+  return principal.forget();
+}
+
+void nsObjectLoadingContent::RefreshFeaturePolicy() {
+  if (mType != ObjectType::Document) {
+    return;
+  }
+
+  if (!mFeaturePolicy) {
+    mFeaturePolicy = MakeAndAddRef<FeaturePolicy>(AsElement());
+  }
+
+  // The origin can change if 'src' or 'data' attributes change.
+  nsCOMPtr<nsIPrincipal> origin = GetFeaturePolicyDefaultOrigin(AsElement());
+  MOZ_ASSERT(origin);
+  mFeaturePolicy->SetDefaultOrigin(origin);
+
+  mFeaturePolicy->InheritPolicy(AsElement()->OwnerDoc()->FeaturePolicy());
+  MaybeStoreCrossOriginFeaturePolicy();
 }
diff --git a/dom/base/nsObjectLoadingContent.h b/dom/base/nsObjectLoadingContent.h
index 51973a8d7c5..f13f8fa3eb5 100644
--- a/dom/base/nsObjectLoadingContent.h
+++ b/dom/base/nsObjectLoadingContent.h
@@ -27,6 +27,7 @@ class nsFrameLoader;
 
 namespace mozilla::dom {
 struct BindContext;
+class FeaturePolicy;
 template <typename T>
 class Sequence;
 class HTMLIFrameElement;
@@ -213,6 +214,14 @@ class nsObjectLoadingContent : public nsIStreamListener,
    */
   bool BlockEmbedOrObjectContentLoading();
 
+  /**
+   * Updates and stores the container's feature policy in its canonical browsing
+   * context. This gets called whenever the feature policy has changed, which
+   * can happen when this element is upgraded to a container or when the URI of
+   * the element has changed.
+   */
+  void RefreshFeaturePolicy();
+
  private:
   // Object parameter changes returned by UpdateObjectParameters
   enum ParameterUpdateFlags {
@@ -393,6 +402,14 @@ class nsObjectLoadingContent : public nsIStreamListener,
    */
   void MaybeStoreCrossOriginFeaturePolicy();
 
+  /**
+   * Return the value of either `data` or `src`, depending on element type,
+   * parsed as a URL. If URL is invalid or the attribute is missing this returns
+   * the document's origin.
+   */
+  static already_AddRefed<nsIPrincipal> GetFeaturePolicyDefaultOrigin(
+      nsINode* aNode);
+
   // The final listener for mChannel (uriloader, pluginstreamlistener, etc.)
   nsCOMPtr<nsIStreamListener> mFinalListener;
 
@@ -464,6 +481,14 @@ class nsObjectLoadingContent : public nsIStreamListener,
   // our own frame.
   mozilla::Maybe<mozilla::IntrinsicSize> mSubdocumentIntrinsicSize;
   mozilla::Maybe<mozilla::AspectRatio> mSubdocumentIntrinsicRatio;
+
+  // This gets created on the first call of `RefreshFeaturePolicy`, and will be
+  // kept after that. Navigations of this element will use this if they're
+  // targetting documents, which is how iframe element works. If it's a
+  // non-document the feature policy isn't used, but it doesn't hurt to keep it
+  // around, and a subsequent document load will continue using it after
+  // refreshing it.
+  RefPtr<mozilla::dom::FeaturePolicy> mFeaturePolicy;
 };
 
 #endif
diff --git a/dom/html/HTMLEmbedElement.cpp b/dom/html/HTMLEmbedElement.cpp
index 7cd9fd916c4..c307838b33b 100644
--- a/dom/html/HTMLEmbedElement.cpp
+++ b/dom/html/HTMLEmbedElement.cpp
@@ -79,6 +79,10 @@ void HTMLEmbedElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
     AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
   }
 
+  if (aName == nsGkAtoms::src) {
+    RefreshFeaturePolicy();
+  }
+
   if (aNamespaceID == kNameSpaceID_None &&
       aName == nsGkAtoms::allowfullscreen && mFrameLoader) {
     if (auto* bc = mFrameLoader->GetExtantBrowsingContext()) {
diff --git a/dom/html/HTMLObjectElement.cpp b/dom/html/HTMLObjectElement.cpp
index d449e484074..7f7eca1e15d 100644
--- a/dom/html/HTMLObjectElement.cpp
+++ b/dom/html/HTMLObjectElement.cpp
@@ -100,6 +100,11 @@ void HTMLObjectElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
                                      nsIPrincipal* aSubjectPrincipal,
                                      bool aNotify) {
   AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
+
+  if (aName == nsGkAtoms::data) {
+    RefreshFeaturePolicy();
+  }
+
   return nsGenericHTMLFormControlElement::AfterSetAttr(
       aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
 }
@@ -126,6 +131,7 @@ void HTMLObjectElement::AfterMaybeChangeAttr(int32_t aNamespaceID,
       BlockEmbedOrObjectContentLoading()) {
     return;
   }
+
   nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
       "HTMLObjectElement::LoadObject",
       [self = RefPtr<HTMLObjectElement>(this), aNotify]() {
diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp
index c06f9a674d3..82c6137d8c5 100644
--- a/netwerk/ipc/DocumentLoadListener.cpp
+++ b/netwerk/ipc/DocumentLoadListener.cpp
@@ -2048,6 +2048,15 @@ bool DocumentLoadListener::MaybeTriggerProcessSwitch(
           return;
         }
 
+        // At this point the element has stored the container feature policy in
+        // the new browsing context, but we need to make sure that we copy it
+        // over to the load info.
+        nsCOMPtr<nsILoadInfo> loadInfo = self->mChannel->LoadInfo();
+        if (aBrowsingContext->GetContainerFeaturePolicy()) {
+          loadInfo->SetContainerFeaturePolicyInfo(
+              *aBrowsingContext->GetContainerFeaturePolicy());
+        }
+
         MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
                 ("Process Switch: Upgraded Object to Document Load"));
         self->TriggerProcessSwitch(aBrowsingContext, options);
Loading diff…