CVE-2024-10458
Overview
Background
- Feature Policy
- a per-document set of permission directives (represented by the
FeaturePolicytype) 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’sBrowsingContextso 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.
RefreshFeaturePolicy (invoked from UpgradeLoadToDocument) to rebuild the policy with the correct origin before the load’s process switch consumes it.Attack Path
- Embed cross-origin content
An attacker gets a trusted page to host an
<object>or<embed>element whosedata/srcpoints at an untrusted, attacker-controlled origin. - Skip policy refresh
During load,
SetupDocShellstores the container feature policy just once using the trusted embedder’sFeaturePolicy, and it is never recomputed for the actual embedded origin. - Upgrade to document container
The element is upgraded via
UpgradeLoadToDocument, creating a browsing context that lacks a policy scoped to the embedded origin. - Process switch reads stale policy
DocumentLoadListener::MaybeTriggerProcessSwitchstarts the document load with the trusted embedder’s permissions baked into the load info instead of the untrusted origin’s restrictions. - Inherit leaked permissions The untrusted embedded document loads with feature permissions granted to the trusted site, achieving a cross-origin permission leak.
Impact Assessment
<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
| Function | Change | Notes |
|---|---|---|
ifdom/base/nsObjectLoadingContent.cpp |
modified | |
GetFeaturePolicyDefaultOrigindom/base/nsObjectLoadingContent.cpp |
modified | |
FeaturePolicydom/base/nsObjectLoadingContent.h |
modified | |
Sequencedom/base/nsObjectLoadingContent.h |
modified | |
HTMLIFrameElementdom/base/nsObjectLoadingContent.h |
modified | |
ifdom/html/HTMLEmbedElement.cpp |
modified | |
ifdom/html/HTMLObjectElement.cpp |
modified |
Files Changed
dom/base/nsObjectLoadingContent.cppdom/base/nsObjectLoadingContent.hdom/html/HTMLEmbedElement.cppdom/html/HTMLObjectElement.cppnetwerk/ipc/DocumentLoadListener.cpp
Audit Directions
- Once-computed security contextLook 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 elementsAudit
<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 infoReview
DocumentLoadListener::MaybeTriggerProcessSwitchand 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.
Patch
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);