commit b4ac69ad68357407726e22d2f14b3674b02c3cc9 parent 2abbb73d5dbcbc48537d48fabe6f83a38f167361 Author: Simon Farre <simon.farre.cx@gmail.com> Date: Fri, 2 Jan 2026 21:07:00 +0000 Bug 1085214 - Implement location.ancestorOrigins r=necko-reviewers,webidl,dom-core,smaug,jesup,zcorpan,devtools-reviewers,nchevobbe This implements this attribute on Location and also adheres to the changes to the spec introduced in https://github.com/whatwg/html/pull/11560 Tentative tests are also added in this patch. Differential Revision: https://phabricator.services.mozilla.com/D273393 Diffstat:
30 files changed, 939 insertions(+), 56 deletions(-)
diff --git a/devtools/client/debugger/test/mochitest/browser_dbg-expressions-error.js b/devtools/client/debugger/test/mochitest/browser_dbg-expressions-error.js @@ -29,5 +29,5 @@ add_task(async function () { is(getWatchExpressionValue(dbg, 4), "2"); await toggleExpressionNode(dbg, 1); - is(findAllElements(dbg, "expressionNodes").length, 37); + is(findAllElements(dbg, "expressionNodes").length, 39); }); diff --git a/devtools/client/debugger/test/mochitest/browser_dbg-expressions.js b/devtools/client/debugger/test/mochitest/browser_dbg-expressions.js @@ -50,7 +50,7 @@ add_task(async function () { // can expand an expression await toggleExpressionNode(dbg, 2); - is(findAllElements(dbg, "expressionNodes").length, 35); + is(findAllElements(dbg, "expressionNodes").length, 37); is(dbg.selectors.getExpressions(dbg.store.getState()).length, 2); await deleteExpression(dbg, "foo"); @@ -71,7 +71,7 @@ add_task(async function () { is(findAllElements(dbg, "expressionNodes").length, 1); await toggleExpressionNode(dbg, 1); - is(findAllElements(dbg, "expressionNodes").length, 34); + is(findAllElements(dbg, "expressionNodes").length, 36); await deleteExpression(dbg, "location"); is(findAllElements(dbg, "expressionNodes").length, 0); diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp @@ -3752,6 +3752,100 @@ void CanonicalBrowsingContext::MaybeReconstructActiveEntryList() { } } +// https://html.spec.whatwg.org/#concept-internal-location-ancestor-origin-objects-list +// Creates the internal ancestor origins list (we store it on a canonical +// browsing context). `aThisDocumentPrincipal` represents the origin for the +// document who we are computing the list for, and +// `aFrameReferrerPolicyAttribute` is the referrer policy attribute on the frame +// that hosts the document. +// For normal navigations `aFrameReferrerPolicyAttribute` will have been +// snapshotted at a spec-appropriate time and passed in here, whereas +// about:blank can read the attribute directly without the attribute having time +// to change which makes the timing consistent with "normal" documents and for +// about:blank this happens in `ContentParent::RecvUpdateAncestorOriginsList`. +void CanonicalBrowsingContext::CreateRedactedAncestorOriginsList( + nsIPrincipal* aThisDocumentPrincipal, + ReferrerPolicy aFrameReferrerPolicyAttribute) { + MOZ_DIAGNOSTIC_ASSERT(aThisDocumentPrincipal); + nsTArray<nsCOMPtr<nsIPrincipal>> ancestorPrincipals; + // 4. if parentDoc is null, then return output + CanonicalBrowsingContext* parent = GetParent(); + if (!parent) { + mPossiblyRedactedAncestorOriginsList = std::move(ancestorPrincipals); + return; + } + MOZ_DIAGNOSTIC_ASSERT(!parent->IsChrome()); + // 7. Let ancestorOrigins be parentLocation's internal ancestor origin objects + // list. + const Span<const nsCOMPtr<nsIPrincipal>> parentAncestorOriginsList = + parent->GetPossiblyRedactedAncestorOriginsList(); + + // 8. Let container be innerDoc's node navigable's container. + WindowGlobalParent* ancestorWGP = GetParentWindowContext(); + + // 10. If container is an iframe element, then set referrerPolicy to + // container's referrerpolicy attribute's state's corresponding keyword. + // Note: becomes the empty string if there is none + auto referrerPolicy = aFrameReferrerPolicyAttribute; + + // 11. Let masked be false. + bool masked = false; + + if (referrerPolicy == ReferrerPolicy::No_referrer) { + // 12. If referrerPolicy is "no-referrer", then set masked to true. + masked = true; + } else if (referrerPolicy == ReferrerPolicy::Same_origin && + !ancestorWGP->DocumentPrincipal()->Equals( + aThisDocumentPrincipal)) { + // 13. Otherwise, if referrerPolicy is "same-origin" and parentDoc's + // origin is not same origin with innerDoc's origin, then set masked to + // true. + masked = true; + } + + if (masked) { + // 14. If masked is true, then append a new opaque origin to output. + ancestorPrincipals.AppendElement(nullptr); + } else { + // 15. Otherwise, append parentDoc's origin to output. + auto* principal = ancestorWGP->DocumentPrincipal(); + // when we serialize a "null principal", we leak information. Represent + // them as actual nullptr instead. + ancestorPrincipals.AppendElement( + principal->GetIsNullPrincipal() ? nullptr : principal); + } + + // 16. For each ancestorOrigin of ancestorOrigins: + for (const auto& ancestorOrigin : parentAncestorOriginsList) { + // 16.1 if masked is true + if (masked && ancestorOrigin && + ancestorOrigin->Equals(ancestorWGP->DocumentPrincipal())) { + // 16.1.1. If ancestorOrigin is same origin with parentDoc's origin, then + // append a new opaque origin to output. + ancestorPrincipals.AppendElement(nullptr); + } else { + // 16.1.2. Otherwise, append ancestorOrigin to output and set masked to + // false. or 16.2. Otherwise, append ancestorOrigin to output. + ancestorPrincipals.AppendElement(ancestorOrigin); + masked = false; + } + } + + // 17. Return output. + // Only we don't return it. We're in the parent process. + mPossiblyRedactedAncestorOriginsList = std::move(ancestorPrincipals); +} + +Span<const nsCOMPtr<nsIPrincipal>> +CanonicalBrowsingContext::GetPossiblyRedactedAncestorOriginsList() const { + return mPossiblyRedactedAncestorOriginsList; +} + +void CanonicalBrowsingContext::SetPossiblyRedactedAncestorOriginsList( + nsTArray<nsCOMPtr<nsIPrincipal>> aAncestorOriginsList) { + mPossiblyRedactedAncestorOriginsList = std::move(aAncestorOriginsList); +} + EntryList* CanonicalBrowsingContext::GetActiveEntries() { if (!mActiveEntryList) { auto* shistory = static_cast<nsSHistory*>(GetSessionHistory()); @@ -3762,6 +3856,11 @@ EntryList* CanonicalBrowsingContext::GetActiveEntries() { return mActiveEntryList; } +void CanonicalBrowsingContext::SetEmbedderFrameReferrerPolicy( + ReferrerPolicy aPolicy) { + mEmbedderFrameReferrerPolicy = aPolicy; +} + already_AddRefed<net::DocumentLoadListener> CanonicalBrowsingContext::GetCurrentLoad() { return do_AddRef(this->mCurrentLoad); diff --git a/docshell/base/CanonicalBrowsingContext.h b/docshell/base/CanonicalBrowsingContext.h @@ -455,6 +455,25 @@ class CanonicalBrowsingContext final : public BrowsingContext { // Get the load listener for the current load in this browsing context. already_AddRefed<net::DocumentLoadListener> GetCurrentLoad(); + // https://html.spec.whatwg.org/#concept-internal-location-ancestor-origin-objects-list + void CreateRedactedAncestorOriginsList( + nsIPrincipal* aThisDocumentPrincipal, + ReferrerPolicy aFrameReferrerPolicyAttribute); + + Span<const nsCOMPtr<nsIPrincipal>> GetPossiblyRedactedAncestorOriginsList() + const; + void SetPossiblyRedactedAncestorOriginsList( + nsTArray<nsCOMPtr<nsIPrincipal>> aAncestorOriginsList); + + void SetEmbedderFrameReferrerPolicy(ReferrerPolicy aPolicy); + + // Called when we need to snap shot referrer policy for ancestorOrigins + // and also when building the internal ancestor origins list for about:blank + // because it needs special handling. + ReferrerPolicy GetEmbedderFrameReferrerPolicy() const { + return mEmbedderFrameReferrerPolicy; + } + protected: // Called when the browsing context is being discarded. void CanonicalDiscard(); @@ -674,8 +693,14 @@ class CanonicalBrowsingContext final : public BrowsingContext { uint32_t mPendingDiscards = 0; bool mFullyDiscarded = false; + // the referrerPolicy attribute of the iframe hosting this browsing context + // defaults to the empty string + ReferrerPolicy mEmbedderFrameReferrerPolicy = ReferrerPolicy::_empty; nsTArray<std::function<void(uint64_t)>> mFullyDiscardedListeners; + + // https://html.spec.whatwg.org/#concept-internal-location-ancestor-origin-objects-list + nsTArray<nsCOMPtr<nsIPrincipal>> mPossiblyRedactedAncestorOriginsList; }; } // namespace dom diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp @@ -6702,6 +6702,80 @@ nsresult nsDocShell::CreateInitialDocumentViewer( return rv; } +// Location.ancestorOrigins for about:blank, need special case handling. +// Particularly in the case for initial about:blank, which does not go through +// normal code that happen with navigations. +static void CreateAboutBlankAncestorOriginsForNonTopLevel(Document* aDoc) { + BrowsingContext* bc = aDoc->GetBrowsingContext(); + MOZ_ASSERT(bc && !bc->IsDiscarded() && bc->GetEmbedderElement()); + // We're not even going to attempt to deal with location.ancestorOrigins stuff + // in the parent process. + if (!XRE_IsContentProcess()) { + return; + } + + const auto* frame = bc->GetEmbedderElement(); + const auto referrerPolicy = frame->GetReferrerPolicyAsEnum(); + // Inform the parent process that it needs to create an internal ancestor + // origins list for this browsing context `bc` + (void)ContentChild::GetSingleton()->SendUpdateAncestorOriginsList(bc); + + const bool masked = referrerPolicy == ReferrerPolicy::No_referrer; + BrowsingContext* parent = bc->GetParent(); + MOZ_DIAGNOSTIC_ASSERT(parent && parent->IsInProcess() && + parent->GetExtantDocument()); + + nsTArray<nsCOMPtr<nsIPrincipal>> ancestorPrincipals; + constexpr auto getPrincipal = + [](const BrowsingContext* ctx) -> nsIPrincipal* { + if (!ctx) { + return nullptr; + } + auto* doc = ctx->GetExtantDocument(); + return doc ? doc->GetPrincipal() : nullptr; + }; + BrowsingContext* ancestorContextToCopyAncestorListFrom = parent; + + // about:blank is different from normal docs. + // We only care about in-process, same-origin ancestors. + // Therefore run the algorithm all the way up to the last same-origin doc + // add that origin to the list, and then append that origin's ancestor origins + // list + if (masked) { + ancestorPrincipals.AppendElement(nullptr); + // 16.1.1. If ancestorOrigin is same origin with parentDoc's origin, then + // append a new opaque origin to output. + auto* parentDocPrincipal = getPrincipal(parent); + for (auto* ancestor = parent->GetParent(); ancestor; + ancestor = ancestor->GetParent()) { + auto* principal = getPrincipal(ancestor); + if (principal && principal->Equals(parentDocPrincipal)) { + // same principal, same process, adding nullptr for + // parentContextToCopyAncestorListFrom + ancestorContextToCopyAncestorListFrom = ancestor; + ancestorPrincipals.AppendElement(nullptr); + } else { + // 16.1.2 Otherwise, append ancestorOrigin to output and set masked to + // false. + // Note: But in the case of about:blank, ancestorOrigin can + // potentially live in another process. So we stop right before it, and + // just copy `ancestorContextToCopyAncestorListFrom` list, since the + // masking steps should finish here. + break; + } + } + } else { + ancestorPrincipals.AppendElement(getPrincipal(parent)); + } + + nsTArray<nsString> list = ProduceAncestorOriginsList(ancestorPrincipals); + Document* ancestorDoc = + ancestorContextToCopyAncestorListFrom->GetExtantDocument(); + MOZ_DIAGNOSTIC_ASSERT(ancestorDoc); + list.AppendElements(ancestorDoc->GetAncestorOriginsList()); + aDoc->SetAncestorOriginsList(std::move(list)); +} + nsresult nsDocShell::CreateAboutBlankDocumentViewer( nsIPrincipal* aPrincipal, nsIPrincipal* aPartitionedPrincipal, nsIPolicyContainer* aPolicyContainer, nsIURI* aBaseURI, @@ -6937,6 +7011,12 @@ nsresult nsDocShell::CreateAboutBlankDocumentViewer( } else { blankDoc->InitFeaturePolicy(AsVariant(Nothing{})); } + + // Perform redacted location.ancestorOrigins algorithm for about:blank + if (BrowsingContext* bc = GetBrowsingContext(); + bc && bc->GetEmbedderElement()) { + CreateAboutBlankAncestorOriginsForNonTopLevel(blankDoc); + } } } diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp @@ -3674,6 +3674,12 @@ nsresult Document::StartDocumentLoad(const char* aCommand, nsIChannel* aChannel, // If this is an error page, don't inherit sandbox flags nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + if (!IsTopLevelContentDocument()) { + SetAncestorOriginsList( + ProduceAncestorOriginsList(loadInfo->AncestorPrincipals())); + } + if (docShell && !loadInfo->GetLoadErrorPage()) { mSandboxFlags = loadInfo->GetSandboxFlags(); WarnIfSandboxIneffective(docShell, mSandboxFlags, GetChannel()); @@ -18062,6 +18068,23 @@ void Document::UpdateLastRememberedSizes() { } } +void Document::SetAncestorOriginsList( + nsTArray<nsString>&& aAncestorOriginsList) { + mAncestorOriginsList = std::move(aAncestorOriginsList); +} + +Span<const nsString> Document::GetAncestorOriginsList() const { + return mAncestorOriginsList; +} + +already_AddRefed<DOMStringList> Document::AncestorOrigins() const { + RefPtr<DOMStringList> list = new DOMStringList(); + for (const auto& origin : mAncestorOriginsList) { + list->Add(origin); + } + return list.forget(); +} + void Document::NotifyLayerManagerRecreated() { NotifyActivityChanged(); EnumerateSubDocuments([](Document& aSubDoc) { diff --git a/dom/base/Document.h b/dom/base/Document.h @@ -1986,6 +1986,11 @@ class Document : public nsINode, MOZ_CAN_RUN_SCRIPT bool TryAutoFocusCandidate(Element& aElement); public: + void SetAncestorOriginsList(nsTArray<nsString>&& aAncestorOriginsList); + Span<const nsString> GetAncestorOriginsList() const; + // https://html.spec.whatwg.org/#concept-location-ancestor-origins-list + already_AddRefed<DOMStringList> AncestorOrigins() const; + // Removes all the elements with fullscreen flag set from the top layer, and // clears their fullscreen flag. void CleanupFullscreenState(); @@ -5330,6 +5335,8 @@ class Document : public nsINode, private: nsCString mContentType; + nsTArray<nsString> mAncestorOriginsList; + protected: // The document's security info nsCOMPtr<nsITransportSecurityInfo> mSecurityInfo; diff --git a/dom/base/Location.cpp b/dom/base/Location.cpp @@ -41,6 +41,28 @@ namespace mozilla::dom { +nsTArray<nsString> ProduceAncestorOriginsList( + const nsTArray<nsCOMPtr<nsIPrincipal>>& aPrincipals) { + nsTArray<nsString> result; + + for (const auto& principal : aPrincipals) { + nsString origin; + if (principal == nullptr) { + origin.AssignLiteral(u"null"); + } else { + nsAutoCString originNoSuffix; + if (NS_WARN_IF(NS_FAILED(principal->GetOriginNoSuffix(originNoSuffix)))) { + origin.AssignLiteral(u"null"); + } else { + CopyUTF8toUTF16(originNoSuffix, origin); + } + } + result.AppendElement(std::move(origin)); + } + + return result; +} + Location::Location(nsPIDOMWindowInner* aWindow) : mCachedHash(VoidCString()), mInnerWindow(aWindow) { BrowsingContext* bc = GetBrowsingContext(); @@ -175,6 +197,27 @@ void Location::SetHash(const nsACString& aHash, nsIPrincipal& aSubjectPrincipal, Navigate(uri, aSubjectPrincipal, aRv); } +// https://html.spec.whatwg.org/#dom-location-ancestororigins +RefPtr<DOMStringList> Location::GetAncestorOrigins( + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + Document* doc = mInnerWindow->GetExtantDoc(); + // Step 1. If this's relevant Document is null, then return an empty list. + if (!doc || !doc->IsActive()) { + return MakeRefPtr<DOMStringList>(); + } + + // Step 2. If this's relevant Document's origin is not same origin-domain with + // the entry settings object's origin, then throw a "SecurityError" + // DOMException. + if (!CallerSubsumes(&aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + // Step 3. Otherwise, return this's ancestor origins list. + return doc->AncestorOrigins(); +} + void Location::GetHost(nsACString& aHost, nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { if (!CallerSubsumes(&aSubjectPrincipal)) { diff --git a/dom/base/Location.h b/dom/base/Location.h @@ -11,6 +11,7 @@ #include "mozilla/ErrorResult.h" #include "mozilla/LinkedList.h" #include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/DOMStringList.h" #include "mozilla/dom/LocationBase.h" #include "nsCycleCollectionParticipant.h" #include "nsString.h" @@ -23,6 +24,10 @@ class nsPIDOMWindowInner; namespace mozilla::dom { +// Serializes principals to strings for location.ancestorOrigin purposes. +nsTArray<nsString> ProduceAncestorOriginsList( + const nsTArray<nsCOMPtr<nsIPrincipal>>& aPrincipals); + //***************************************************************************** // Location: Script "location" object //***************************************************************************** @@ -102,6 +107,9 @@ class Location final : public nsISupports, void SetHash(const nsACString& aHash, nsIPrincipal& aSubjectPrincipal, ErrorResult& aError); + RefPtr<DOMStringList> GetAncestorOrigins(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + nsPIDOMWindowInner* GetParentObject() const { return mInnerWindow; } virtual JSObject* WrapObject(JSContext* aCx, diff --git a/dom/html/HTMLIFrameElement.cpp b/dom/html/HTMLIFrameElement.cpp @@ -71,6 +71,8 @@ NS_IMPL_ELEMENT_CLONE(HTMLIFrameElement) void HTMLIFrameElement::BindToBrowsingContext(BrowsingContext*) { RefreshFeaturePolicy(true /* parse the feature policy attribute */); + RefreshEmbedderReferrerPolicy( + ReferrerPolicyFromAttr(GetParsedAttr(nsGkAtoms::referrerpolicy))); } bool HTMLIFrameElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, @@ -198,6 +200,13 @@ void HTMLIFrameElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, } } + if (aName == nsGkAtoms::referrerpolicy) { + const auto newValue = ReferrerPolicyFromAttr(aValue); + if (newValue != ReferrerPolicyFromAttr(aOldValue)) { + RefreshEmbedderReferrerPolicy(newValue); + } + } + return nsGenericHTMLFrameElement::AfterSetAttr( aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); } @@ -312,6 +321,17 @@ void HTMLIFrameElement::RefreshFeaturePolicy(bool aParseAllowAttribute) { MaybeStoreCrossOriginFeaturePolicy(); } +void HTMLIFrameElement::RefreshEmbedderReferrerPolicy(ReferrerPolicy aPolicy) { + auto* browsingContext = GetExtantBrowsingContext(); + if (!browsingContext || !browsingContext->IsContentSubframe()) { + return; + } + + if (ContentChild* cc = ContentChild::GetSingleton()) { + (void)cc->SendSetReferrerPolicyForEmbedderFrame(browsingContext, aPolicy); + } +} + void HTMLIFrameElement::UpdateLazyLoadState() { // Store current base URI and referrer policy in the lazy load state. mLazyLoadState.mBaseURI = GetBaseURI(); diff --git a/dom/html/HTMLIFrameElement.h b/dom/html/HTMLIFrameElement.h @@ -193,6 +193,7 @@ class HTMLIFrameElement final : public nsGenericHTMLFrameElement { static const DOMTokenListSupportedToken sSupportedSandboxTokens[]; void RefreshFeaturePolicy(bool aParseAllowAttribute); + void RefreshEmbedderReferrerPolicy(ReferrerPolicy aPolicy); // If this iframe has a 'srcdoc' attribute, the document's origin will be // returned. Otherwise, if this iframe has a 'src' attribute, the origin will diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp @@ -7989,6 +7989,32 @@ mozilla::ipc::IPCResult ContentParent::RecvSetContainerFeaturePolicy( return IPC_OK(); } +mozilla::ipc::IPCResult ContentParent::RecvUpdateAncestorOriginsList( + const MaybeDiscardedBrowsingContext& aContext) { + if (!aContext.IsNullOrDiscarded()) { + auto* canonical = aContext.get_canonical(); + if (WindowGlobalParent* windowGlobal = + canonical->GetCurrentWindowGlobal()) { + canonical->CreateRedactedAncestorOriginsList( + windowGlobal->DocumentPrincipal(), + canonical->GetEmbedderFrameReferrerPolicy()); + } + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ContentParent::RecvSetReferrerPolicyForEmbedderFrame( + const MaybeDiscardedBrowsingContext& aContext, + const ReferrerPolicy& aPolicy) { + if (!aContext.IsNullOrDiscarded()) { + auto* canonical = aContext.get_canonical(); + canonical->SetEmbedderFrameReferrerPolicy(aPolicy); + } + + return IPC_OK(); +} + NS_IMETHODIMP ContentParent::GetCanSend(bool* aCanSend) { *aCanSend = CanSend(); return NS_OK; diff --git a/dom/ipc/ContentParent.h b/dom/ipc/ContentParent.h @@ -1404,6 +1404,13 @@ class ContentParent final : public PContentParent, const MaybeDiscardedBrowsingContext& aContainerContext, MaybeFeaturePolicyInfo&& aContainerFeaturePolicyInfo); + mozilla::ipc::IPCResult RecvUpdateAncestorOriginsList( + const MaybeDiscardedBrowsingContext& aContext); + + mozilla::ipc::IPCResult RecvSetReferrerPolicyForEmbedderFrame( + const MaybeDiscardedBrowsingContext& aContext, + const ReferrerPolicy& aPolicy); + mozilla::ipc::IPCResult RecvGetSystemIcon(nsIURI* aURI, GetSystemIconResolver&& aResolver); diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl @@ -175,6 +175,7 @@ using mozilla::dom::UserActivation::Modifiers from "mozilla/dom/UserActivation.h using mozilla::dom::PrivateAttributionImpressionType from "mozilla/dom/PrivateAttributionIPCUtils.h"; using nsIClipboard::ClipboardType from "nsIClipboard.h"; using nsIUrlClassifierFeature::listType from "nsIUrlClassifierFeature.h"; +using mozilla::dom::ReferrerPolicy from "mozilla/dom/ReferrerPolicyBinding.h"; #ifdef MOZ_WMF_CDM using nsIOriginStatusEntry from "nsIWindowsMediaFoundationCDMOriginsListService.h"; @@ -1970,6 +1971,14 @@ parent: async SetContainerFeaturePolicy(MaybeDiscardedBrowsingContext aContainerContext, MaybeFeaturePolicyInfo aContainerFeaturePolicyInfo); + // Notify parent process that aContext must update its ancestor origins list + // This is only used for about:blank, because for normal navigations + // the internal ancestor origins list will be created during load. + async UpdateAncestorOriginsList(MaybeDiscardedBrowsingContext aContext); + + // Bookkeep referrer policy of the embedding iframe (if any) in the parent process for browsing contexts + async SetReferrerPolicyForEmbedderFrame(MaybeDiscardedBrowsingContext aBrowsingContext, ReferrerPolicy aPolicy); + // Obtain an icon from the system widget toolkit, in nsIconDecoder // format. Not supported (or needed) on all platforms; see the // implementation in ContentParent::RecvGetSystemIcon for details. diff --git a/dom/webidl/Location.webidl b/dom/webidl/Location.webidl @@ -44,5 +44,7 @@ interface Location { [Throws, NeedsSubjectPrincipal] undefined reload(optional boolean forceget = false); - // Bug 1085214 [SameObject] readonly attribute USVString[] ancestorOrigins; + // https://html.spec.whatwg.org/#dom-location-ancestororigins + [Throws, LegacyUnforgeable, GetterNeedsSubjectPrincipal, Pref="dom.location.ancestorOrigins.enabled"] + readonly attribute DOMStringList ancestorOrigins; }; diff --git a/ipc/glue/BackgroundUtils.cpp b/ipc/glue/BackgroundUtils.cpp @@ -612,7 +612,7 @@ nsresult LoadInfoToLoadInfoArgs(nsILoadInfo* aLoadInfo, aLoadInfo->GetIsOriginTrialCoepCredentiallessEnabledForTopLevel(), unstrippedURI, interceptionInfoArg, aLoadInfo->GetIsNewWindowTarget(), aLoadInfo->GetUserNavigationInvolvement(), - aLoadInfo->GetContainerFeaturePolicyInfo()); + aLoadInfo->GetContainerFeaturePolicyInfo(), {}); return NS_OK; } @@ -763,6 +763,18 @@ nsresult LoadInfoArgsToLoadInfo(const LoadInfoArgs& loadInfoArgs, LoadInfo::ComputeAncestors(parentBC->Canonical(), ancestorPrincipals, ancestorBrowsingContextIDs); } + } else { + // Fill out (possibly redacted) ancestor principals for + // Location.ancestorOrigins + for (const auto& principalInfo : loadInfoArgs.ancestorOrigins()) { + if (principalInfo.isNothing()) { + ancestorPrincipals.AppendElement(nullptr); + } else { + auto principal = PrincipalInfoToPrincipal(principalInfo.value()); + // If this operation fail, we censor the origin. + ancestorPrincipals.AppendElement(principal.unwrapOr(nullptr)); + } + } } Maybe<ClientInfo> clientInfo; diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -3573,6 +3573,14 @@ value: true mirror: always +# If this is set to true, location.ancestorOrigins feature defined +# by the spec https://html.spec.whatwg.org/#concept-location-ancestor-origins-list +# is enabled +- name: dom.location.ancestorOrigins.enabled + type: bool + value: true + mirror: always + # Whether "W3C Web Manifest" processing is enabled - name: dom.manifest.enabled type: bool diff --git a/netwerk/base/LoadInfo.cpp b/netwerk/base/LoadInfo.cpp @@ -1769,6 +1769,14 @@ void LoadInfo::SetIsFromProcessingFrameAttributes() { mIsFromProcessingFrameAttributes = true; } +dom::ReferrerPolicy LoadInfo::GetFrameReferrerPolicySnapshot() const { + return mFrameReferrerPolicySnapshot; +} + +void LoadInfo::SetFrameReferrerPolicySnapshot(dom::ReferrerPolicy aPolicy) { + mFrameReferrerPolicySnapshot = aPolicy; +} + NS_IMETHODIMP LoadInfo::GetResultPrincipalURI(nsIURI** aURI) { *aURI = do_AddRef(mResultPrincipalURI).take(); diff --git a/netwerk/base/LoadInfo.h b/netwerk/base/LoadInfo.h @@ -8,6 +8,7 @@ #define mozilla_LoadInfo_h #include "mozilla/dom/FeaturePolicy.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" #include "mozilla/dom/UserNavigationInvolvement.h" #include "nsIInterceptionInfo.h" #include "nsILoadInfo.h" @@ -351,6 +352,9 @@ class LoadInfo final : public nsILoadInfo { void SetBrowserWouldUpgradeInsecureRequests(); void SetIsFromProcessingFrameAttributes(); + dom::ReferrerPolicy GetFrameReferrerPolicySnapshot() const; + void SetFrameReferrerPolicySnapshot(dom::ReferrerPolicy aPolicy); + // Hands off from the cspToInherit functionality! // // For navigations, GetCSPToInherit returns what the spec calls the @@ -502,6 +506,8 @@ class LoadInfo final : public nsILoadInfo { nsWeakPtr mContextForTopLevelLoad; nsSecurityFlags mSecurityFlags; uint32_t mSandboxFlags; + dom::ReferrerPolicy mFrameReferrerPolicySnapshot = + dom::ReferrerPolicy::_empty; nsContentPolicyType mInternalContentPolicyType; LoadTainting mTainting = LoadTainting::Basic; diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp @@ -16,6 +16,7 @@ #include "mozilla/DynamicFpiNavigationHeuristic.h" #include "mozilla/Components.h" #include "mozilla/LoadInfo.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" #include "mozilla/NullPrincipal.h" #include "mozilla/RefPtr.h" #include "mozilla/ResultVariant.h" @@ -700,6 +701,11 @@ auto DocumentLoadListener::Open(nsDocShellLoadState* aLoadState, nsresult* aRv) -> RefPtr<OpenPromise> { auto* loadingContext = GetLoadingBrowsingContext(); + // Snapshot the referrer policy to be used when running the "create internal + // ancestor origins list". + aLoadInfo->SetFrameReferrerPolicySnapshot( + loadingContext->GetEmbedderFrameReferrerPolicy()); + MOZ_DIAGNOSTIC_ASSERT_IF(loadingContext->GetParent(), loadingContext->GetParentWindowContext()); @@ -1690,6 +1696,43 @@ void DocumentLoadListener::SerializeRedirectData( MOZ_ALWAYS_SUCCEEDS( ipc::LoadInfoToLoadInfoArgs(redirectLoadInfo, &aArgs.loadInfo())); + if (StaticPrefs::dom_location_ancestorOrigins_enabled()) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (RefPtr bc = redirectLoadInfo->GetFrameBrowsingContext()) { + nsCOMPtr<nsIPrincipal> resultPrincipal; + // If this fails, we get an empty location.ancestorOrigins list + if (NS_SUCCEEDED( + nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + mChannel, getter_AddRefs(resultPrincipal)))) { + const auto referrerPolicy = + static_cast<LoadInfo*>(channelLoadInfo.get()) + ->GetFrameReferrerPolicySnapshot(); + bc->Canonical()->CreateRedactedAncestorOriginsList(resultPrincipal, + referrerPolicy); + } + + // convert principals to IPC data + constexpr auto prepareInfo = + [](nsIPrincipal* aPrincipal) -> Maybe<ipc::PrincipalInfo> { + if (aPrincipal == nullptr) { + return Nothing(); + } + ipc::PrincipalInfo data; + return NS_SUCCEEDED(PrincipalToPrincipalInfo(aPrincipal, &data)) + ? Some(std::move(data)) + : Nothing(); + }; + + // The ancestorOrigins list the document should ultimately have, that we + // send down with load args. + auto& ancestorOrigins = aArgs.loadInfo().ancestorOrigins(); + for (const auto& ancestorPrincipal : + bc->Canonical()->GetPossiblyRedactedAncestorOriginsList()) { + ancestorOrigins.AppendElement(prepareInfo(ancestorPrincipal)); + } + } + } + mChannel->GetOriginalURI(getter_AddRefs(aArgs.originalURI())); // mChannel can be a nsHttpChannel as well as InterceptedHttpChannel so we diff --git a/netwerk/ipc/NeckoChannelParams.ipdlh b/netwerk/ipc/NeckoChannelParams.ipdlh @@ -224,6 +224,8 @@ struct LoadInfoArgs bool isNewWindowTarget; UserNavigationInvolvement userNavigationInvolvement; FeaturePolicyInfo? containerFeaturePolicyInfo; + // https://html.spec.whatwg.org/#dom-location-ancestororigins + PrincipalInfo?[] ancestorOrigins; }; /** diff --git a/testing/web-platform/meta/html/browsers/history/the-location-interface/location-ancestor-origins-inactive-document.sub.html.ini b/testing/web-platform/meta/html/browsers/history/the-location-interface/location-ancestor-origins-inactive-document.sub.html.ini @@ -0,0 +1 @@ +prefs: [dom.location.ancestorOrigins.enabled:true] diff --git a/testing/web-platform/meta/html/browsers/history/the-location-interface/location-ancestor-origins.sub.html.ini b/testing/web-platform/meta/html/browsers/history/the-location-interface/location-ancestor-origins.sub.html.ini @@ -0,0 +1 @@ +prefs: [dom.location.ancestorOrigins.enabled:true] diff --git a/testing/web-platform/meta/html/browsers/history/the-location-interface/no-browsing-context.window.js.ini b/testing/web-platform/meta/html/browsers/history/the-location-interface/no-browsing-context.window.js.ini @@ -70,9 +70,6 @@ [Invoking `reload` with `test:test` on a `Location` object sans browsing context is a no-op] expected: FAIL - [Getting `ancestorOrigins` of a `Location` object sans browsing context should be [\]] - expected: FAIL - [Invoking `assign` with `http://test:test/` on a `Location` object sans browsing context is a no-op] expected: FAIL diff --git a/testing/web-platform/meta/html/dom/idlharness.https.html.ini b/testing/web-platform/meta/html/dom/idlharness.https.html.ini @@ -33,18 +33,12 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu [SVGSVGElement interface: attribute onbeforeprint] expected: FAIL - [Location interface: window.location must have own property "ancestorOrigins"] - expected: FAIL - [VideoTrackList interface: existence and properties of interface object] expected: FAIL [External interface: existence and properties of interface prototype object's "constructor" property] expected: FAIL - [DOMStringList interface: calling item(unsigned long) on location.ancestorOrigins with too few arguments must throw TypeError] - expected: FAIL - [SVGSVGElement interface: attribute onoffline] expected: FAIL @@ -90,9 +84,6 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu [CanvasRenderingContext2D interface: attribute imageSmoothingQuality] expected: FAIL - [DOMStringList interface: calling contains(DOMString) on location.ancestorOrigins with too few arguments must throw TypeError] - expected: FAIL - [SVGSVGElement interface: attribute onbeforeunload] expected: FAIL @@ -111,21 +102,12 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu [VideoTrack interface: existence and properties of interface prototype object's @@unscopables property] expected: FAIL - [DOMStringList interface: location.ancestorOrigins must inherit property "contains(DOMString)" with the proper type] - expected: FAIL - [SVGSVGElement interface: attribute onmessage] expected: FAIL [VideoTrackList interface: attribute onchange] expected: FAIL - [Stringification of location.ancestorOrigins] - expected: FAIL - - [DOMStringList interface: location.ancestorOrigins must inherit property "item(unsigned long)" with the proper type] - expected: FAIL - [AudioTrack interface: attribute id] expected: FAIL @@ -147,9 +129,6 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu [SVGSVGElement interface: attribute onhashchange] expected: FAIL - [DOMStringList must be primary interface of location.ancestorOrigins] - expected: FAIL - [AudioTrackList interface: attribute onchange] expected: FAIL @@ -189,9 +168,6 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu [VideoTrack interface: existence and properties of interface object] expected: FAIL - [DOMStringList interface: location.ancestorOrigins must inherit property "length" with the proper type] - expected: FAIL - [External interface: existence and properties of interface prototype object] expected: FAIL @@ -1064,27 +1040,6 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu [idlharness.https.html?exclude=(Document|Window|HTML.+)] - [DOMStringList must be primary interface of location.ancestorOrigins] - expected: FAIL - - [Stringification of location.ancestorOrigins] - expected: FAIL - - [DOMStringList interface: location.ancestorOrigins must inherit property "length" with the proper type] - expected: FAIL - - [DOMStringList interface: location.ancestorOrigins must inherit property "item(unsigned long)" with the proper type] - expected: FAIL - - [DOMStringList interface: calling item(unsigned long) on location.ancestorOrigins with too few arguments must throw TypeError] - expected: FAIL - - [DOMStringList interface: location.ancestorOrigins must inherit property "contains(DOMString)" with the proper type] - expected: FAIL - - [DOMStringList interface: calling contains(DOMString) on location.ancestorOrigins with too few arguments must throw TypeError] - expected: FAIL - [AudioTrackList interface: existence and properties of interface object] expected: FAIL @@ -1307,9 +1262,6 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu [Location interface: stringifier] expected: FAIL - [Location interface: window.location must have own property "ancestorOrigins"] - expected: FAIL - [Navigation interface: existence and properties of interface object] expected: if not sessionHistoryInParent: FAIL diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-ancestor-origins-inactive-document.sub.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-ancestor-origins-inactive-document.sub.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Location.ancestorOrigins lifetime behavior</title> + <link rel="help" href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#dom-location-ancestororigins"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script> + function createIframeAndNavigate(test, src) { + return new Promise(resolve => { + const iframe = document.createElement("iframe"); + iframe.onload = () => { + resolve(iframe); + } + iframe.src = src; + document.body.appendChild(iframe); + test.add_cleanup(() => { + iframe.remove(); + }); + }); + } + + + promise_test(async t => { + assert_implements(location.ancestorOrigins, "location.ancestorOrigins not implemented"); + const iframe = await createIframeAndNavigate(t, "about:blank"); + assert_array_equals( + iframe.contentWindow.location.ancestorOrigins, + [location.origin], + "Initial ancestorOrigins should match expected placeholder value" + ); + + const loc = iframe.contentWindow.location; + iframe.remove(); + + assert_array_equals( + Array.from(loc.ancestorOrigins), + [], + "ancestorOrigins should be empty after iframe removal" + ); + }, "location.ancestorOrigins returns empty list after iframe is removed and referenced Location's relevant document is null"); + + promise_test(async t => { + assert_implements(location.ancestorOrigins, "location.ancestorOrigins not implemented"); + const iframe = await createIframeAndNavigate(t, "about:blank"); + assert_array_equals( + iframe.contentWindow.location.ancestorOrigins, + [location.origin], + "Initial ancestorOrigins should match expected placeholder value" + ); + + const loc = iframe.contentWindow.location; + await new Promise(resolve => { + iframe.onload = resolve; + iframe.src = "http://{{hosts[alt][]}}:{{ports[http][0]}}/common/blank.html"; + }); + + assert_array_equals( + Array.from(loc.ancestorOrigins), + [], + "ancestorOrigins should be empty after iframe navigation" + ); + }, "location.ancestorOrigins returns empty list when iframe navigated away and referenced Location's relevant document is null"); + </script> + </body> + +</html> diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-ancestor-origins-referrerpolicy-snapshot.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-ancestor-origins-referrerpolicy-snapshot.html @@ -0,0 +1,42 @@ +<!doctype html> +<title>location.ancestorOrigins snapshot timing of referrerpolicy</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +promise_test(async () => { + assert_implements(location.ancestorOrigins); + const iframe = document.createElement('iframe'); + iframe.src = '/common/blank.html?pipe=trickle(d1)'; + iframe.referrerPolicy = 'no-referrer'; + const loaded = new Promise(resolve => iframe.onload = resolve); + document.body.append(iframe); + // initial about:blank should see 'no-referrer' results + assert_array_equals(Array.from(iframe.contentWindow.location.ancestorOrigins), ['null']); + iframe.referrerPolicy = ''; + await loaded; + // The referrerpolicy attribute get snapshotted at step 16 of "beginning navigation" + // https://html.spec.whatwg.org/#beginning-navigation and result should therefore, + // still be ["null"], here. + assert_array_equals(Array.from(iframe.contentWindow.location.ancestorOrigins), ["null"]); +}); + +promise_test(async () => { + assert_implements(location.ancestorOrigins); + const iframe = document.createElement('iframe'); + + iframe.referrerPolicy = 'no-referrer'; + const loaded = new Promise(resolve => iframe.onload = resolve); + document.body.append(iframe); + // initial about:blank should see 'no-referrer' results + assert_array_equals(Array.from(iframe.contentWindow.location.ancestorOrigins), ['null']); + iframe.referrerPolicy = ''; + await loaded; + + await new Promise(resolve => { + iframe.onload = resolve; + iframe.src = '/common/blank.html?pipe=trickle(d1)'; + }); + assert_array_equals(Array.from(iframe.contentWindow.location.ancestorOrigins), [window.origin]); +}); +</script> diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/location-ancestor-origins.sub.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/location-ancestor-origins.sub.html @@ -0,0 +1,328 @@ +<!DOCTYPE html> +<title> + Location#ancestorOrigins test +</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="./resources/location-ancestor-origins-create-iframe.js"></script> + +<body> + <script> + function waitFor(action, frameName) { + return new Promise((resolve) => { + window.addEventListener("message", function listener(e) { + if (e.data.event === action && e.data.name === frameName) { + window.removeEventListener("message", listener); + resolve(event.data); + } + }); + }); + } + + const frameCreatorPath = "html/browsers/history/the-location-interface/resources/location-ancestor-origins-recursive-iframe.html" + + const createOrigin = (src) => { + const url = new URL(src); + return { + src: src, + origin: url.origin, + // change referrer policy value in header + withHeaders: (policyValue) => { + if (!policyValue) + return src; + return `${src}&pipe=header(Referrer-Policy,${policyValue})`; + } + } + } + + // iframeSources[0] origins shares origin with top level + let iframeSources = [ + createOrigin(`http://{{hosts[][]}}:{{ports[http][0]}}/${frameCreatorPath}?a`), + createOrigin(`http://{{hosts[alt][]}}:{{ports[http][0]}}/${frameCreatorPath}?b`), + createOrigin(`http://{{hosts[][www]}}:{{ports[http][0]}}/${frameCreatorPath}?c`) + ]; + + const A = iframeSources[0] + const B = iframeSources[1]; + const C = iframeSources[2]; + + const policy = { + Default: "", + NoRef: "no-referrer", + SameOrigin: "same-origin" + } + + // options takes optional values for iframe attribute, or a pre-configure + // step before creating the iframe that a call to `config` configures, + // like changing iframe referrer policy of a frame in a specific target. + // options is created by calling mutatePolicyOf, insertMetaForWithPolicyOf + // or an object containing { sandbox: boolean } + // The options argument can specify something that should happen before + // the creation of the frame this configures. + + // for example: config("B2", B.src, policy.Default, mutatePolicyOf("B1", "")) means + // create a frame named B2 in the inner-most frame, with src=B.src, referrerPolicy = the default + // but before the frame is bound to the tree, + // perform an action in "B1" that mutates B1's referrer policy attribute (in this case to the default, i.e. the empty string) + const config = (name, src, iframeRefPolicyAttr, options = null) => ({ name: name, src, referrerPolicy: iframeRefPolicyAttr, options }) + + const mutatePolicyOf = (target, value) => { + return { target, action: Actions.changeReferrerPolicy, value } + } + + const insertMetaForWithPolicyOf = (target, value) => { + return { target, action: Actions.insertMetaElement, value } + } + + // Top-level === A1 + // Only referrerpolicy on the element (e.g. iframe) should affect the ancestorOrigins list. + // Some of these tests may seem counterintuitive/not useful, but they are to make sure that nothing but + // that attribute, has effect. + const testCases = [ + { + description: "test A1 -> B1 (no-referrer in response header) -> C1 -> B2", + frames: [ + config("B1", B.withHeaders(policy.NoRef), policy.Default), + config("C1", C.src, policy.Default), + config("B2", B.src, policy.Default), + ], + expected: [ + [A.origin], + [B.origin, A.origin], + [C.origin, B.origin, A.origin], + ] + }, + { + description: "test A1 -> A2 (no-referrer in response header) -> B1 -> C1", + frames: [ + config("A2", A.withHeaders(policy.NoRef), policy.Default), + config("B1", B.src, policy.Default), + config("C1", C.src, policy.Default), + ], + expected: [ + [A.origin], + [A.origin, A.origin], + [B.origin, A.origin, A.origin], + ] + }, + { + description: "test no-referrer attribute on iframe for iframe containing B1", + frames: [ + config("B1", B.src, policy.NoRef) + ], + expected: [ + ["null"] + ] + }, + { + description: "test default referrer policy, A1 -> B1 -> A2", + frames: [ + config("B1", B.src, policy.Default), + config("A2", A.src, policy.Default) + ], + expected: [ + [A.origin], + [B.origin, A.origin] + ] + }, + { + description: "test toggle of masked A1 -> B1 iframe for A2 sets no-referrer -> A2", + frames: [ + config("B1", B.src, policy.Default), + config("A2", A.src, policy.NoRef) + ], + expected: [ + [A.origin], + ["null", A.origin] + ] + }, + { + description: "test of toggle of masked at first origin != parentDocOrigin, A1 -> B1 -> B2 -> A2", + frames: [ + config("B1", B.src, policy.Default), + config("B2", B.src, policy.Default), + config("A2", A.src, policy.NoRef) + ], + expected: [ + [A.origin], + [B.origin, A.origin], + ["null", "null", A.origin] + ] + }, + { + description: "test sandboxed iframe in A1, A1 iframe attribute sandbox -> C1 -> B1 -> C2", + frames: [ + config("C1", C.src, policy.Default, { sandbox: true }), + config("B1", B.src, policy.Default), + config("C2", C.src, policy.Default), + ], + expected: [ + [A.origin], + ["null", A.origin], + ["null", "null", A.origin], + ] + }, + { + description: "Test same-origin referrer policy, A1 -> B1 -> A2 iframe attribute same-origin -> B2", + frames: [ + config("B1", B.src, policy.Default), + config("A2", A.src, policy.Default), + config("B2", B.src, policy.SameOrigin) + ], + expected: [ + [A.origin], + [B.origin, A.origin], + ["null", B.origin, A.origin] + ] + }, + { + description: "Test that mutating the iframe referrerpolicy attribute of B1 before creation of B2 does not impact the hiding/non-hiding when creating a grandchild browsing context (keep null)", + frames: [ + config("B1", B.src, policy.Default), + config("C1", C.src, policy.NoRef), + // change <iframe src=C1> in B1, referrer policy to default, before creating this context + config("B2", B.src, policy.Default, mutatePolicyOf("B1", "")), + ], + expected: [ + [A.origin], + ["null", A.origin], + [C.origin, "null", A.origin] + ], + }, + { + description: "Test that mutating the iframe referrerpolicy attribute does not impact the hiding/non-hiding when creating a grandchild browsing context (keep origins)", + frames: [ + config("B1", B.src, policy.Default), + config("C1", C.src, policy.Default), + // change <iframe src=C1> in B1, referrer policy to no-referrer, before creating this context + config("B2", B.src, policy.Default, mutatePolicyOf("B1", "no-referrer")), + ], + expected: [ + [A.origin], + [B.origin, A.origin], + [C.origin, B.origin, A.origin] + ], + }, + { + description: "Test that tightening policy using meta, does not affect list", + frames: [ + config("B1", B.src, policy.Default), + config("C1", C.src, policy.Default, insertMetaForWithPolicyOf("B1", "no-referrer")), + ], + expected: [ + [A.origin], + [B.origin, A.origin], + ], + }, + { + description: "Test that loosening policy in policy container has no effect.", + frames: [ + config("B1", B.withHeaders("no-referrer"), policy.Default), + config("C1", C.src, policy.Default, insertMetaForWithPolicyOf("B1", "strict-origin-when-cross-origin")), + ], + expected: [ + [A.origin], + [B.origin, A.origin], + ], + }, + ] + + testCases.forEach((cfg, index) => { + promise_test(async (t) => { + assert_implements(location.ancestorOrigins, "location.ancestorOrigins not implemented"); + const iframeDetails = cfg.frames[0]; + + const topFrameAncestorOrigins = waitFor("sentOrigins", iframeDetails.name); + const iframe = document.createElement("iframe"); + await configAndNavigateIFrame(iframe, iframeDetails); + const res = await topFrameAncestorOrigins; + + t.add_cleanup(() => { + iframe.remove(); + }); + + let results = [res]; + + for (let i = 1; i < cfg.frames.length; ++i) { + // name of frame, that shall create a new frame + const parentName = cfg.frames[i - 1].name; + const { target, action, value } = cfg.frames[i].options ?? {}; + if (target) { + iframe.contentWindow.postMessage({ action: action, request: { value: value }, name: target }, "*"); + } + iframe.contentWindow.postMessage({ action: Actions.addFrame, request: cfg.frames[i], name: parentName }, "*"); + const res = await waitFor("sentOrigins", cfg.frames[i].name); + results.push(res); + } + + let i = 0; + for (const { event, name, origins } of results) { + assert_equals(origins.length, cfg.expected[i].length, `[test configuration ${index}, frame ${name}]`); + assert_array_equals(origins, cfg.expected[i]); + i += 1; + } + }, cfg.description); + }); + + [{ policy: null, expected: [A.origin] }, { policy: "no-referrer", expected: ["null"] }].forEach(test => { + promise_test(async t => { + assert_implements(location.ancestorOrigins, "location.ancestorOrigins not implemented"); + const iframe = document.createElement("iframe"); + t.add_cleanup(() => { + iframe.remove(); + }) + iframe.referrerPolicy = test.policy; + document.body.appendChild(iframe); + assert_array_equals(Array.from(iframe.contentWindow.location.ancestorOrigins), test.expected); + }, `about:blank's location.ancestorOrigin policy=${test.policy}`) + }) + + // Tests bottom-most frame's results only. + const expectedAboutBlankResults = [ + { + description: "A -> about:blank -> B1 -> B2", + expected: [B.origin, A.origin, A.origin], + setNoreferrer: false + }, + { + description: "A -> about:blank, that set iframe referrer policy=no-referrer -> B1 -> B2", + expected: [B.origin, "null", "null"], + setNoreferrer: true + } + ].forEach(test => { + promise_test(async (t) => { + assert_implements(location.ancestorOrigins, "location.ancestorOrigins not implemented"); + + const iframe1 = document.createElement("iframe"); + // top level document's origin = A1. + iframe1.name = "A2"; + t.add_cleanup(() => { + iframe1.remove(); + }); + document.body.appendChild(iframe1); + + let iframe2 = document.createElement("iframe"); + iframe2.name = "B1"; + + if (test.setNoreferrer) { + iframe2.referrerPolicy = "no-referrer"; + } + + let p = new Promise((resolve) => { + iframe2.addEventListener("load", resolve, { once: true }); + iframe2.src = B.src; + }); + iframe1.contentDocument.body.appendChild(iframe2); + await p; + + let resPromise = waitFor("sentOrigins", "B2"); + iframe2.contentWindow.postMessage({ action: Actions.addFrame, request: config("B2", B.src, policy.Default), name: "B1" }, "*") + let res = await resPromise; + + assert_array_equals(Array.from(res.origins), test.expected); + }, test.description); + }) + </script> +</body> diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/location-ancestor-origins-create-iframe.js b/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/location-ancestor-origins-create-iframe.js @@ -0,0 +1,20 @@ +// The different actions that we send to a cross-origin document via postMessage (since they may be/are site isolated) +const Actions = { + addFrame: "addFrame", + changeReferrerPolicy: "changeReferrerPolicy", + insertMetaElement: "insertMetaElement", +} + +async function configAndNavigateIFrame(iframe, { src, name, referrerPolicy, options }) { + iframe.name = name; + iframe.referrerPolicy = referrerPolicy; + if (options?.sandbox) { + // postMessage needs to work + iframe.setAttribute("sandbox", "allow-scripts"); + } + document.body.appendChild(iframe); + await new Promise((resolve) => { + iframe.addEventListener("load", resolve, { once: true }); + iframe.src = src; + }); +} diff --git a/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/location-ancestor-origins-recursive-iframe.html b/testing/web-platform/tests/html/browsers/history/the-location-interface/resources/location-ancestor-origins-recursive-iframe.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<meta charset="utf-8" /> +<title>Recursive iframe document used for Location.ancestorOrigins tests</title> +<!--- This file is used for the creation of the hosted pages inside the iframes. ---> + +<body> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src="./location-ancestor-origins-create-iframe.js"></script> + <script> + let iframe = null; + + window.addEventListener("message", async (e) => { + // Message is not for us, try to pass it on... + if (e.data.name !== window.name) { + iframe?.contentWindow.postMessage(e.data, "*"); + return; + } + switch (e.data.action) { + case Actions.addFrame: { + iframe = document.createElement("iframe"); + await configAndNavigateIFrame(iframe, e.data.request); + break; + } + case Actions.changeReferrerPolicy: { + iframe.referrerPolicy = e.data.request.value; + break; + } + case Actions.insertMetaElement: { + const meta = document.createElement("meta"); + meta.name = "referrer"; + meta.content = e.data.request.value; + document.head.appendChild(meta); + break; + } + default: + window.top.postMessage(e.data, "*"); + } + }); + + window.top.postMessage({ event: "sentOrigins", name: window.name, origins: Array.from(location.ancestorOrigins) }, "*"); + </script> +</body>