tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit d93f1bc4f1f1e6a52cf15e1693bef39fa799df9b
parent d016b0eb74c60d745f5abe22035754c3ad4e619b
Author: Simon Farre <simon.farre.cx@gmail.com>
Date:   Thu, 11 Dec 2025 14:52:23 +0000

Bug 1085214 - Implement location.ancestorOrigins r=necko-reviewers,webidl,dom-core,smaug,jesup,zcorpan

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:
Mdocshell/base/CanonicalBrowsingContext.cpp | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocshell/base/CanonicalBrowsingContext.h | 19+++++++++++++++++++
Mdocshell/base/nsDocShell.cpp | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdom/base/Document.cpp | 23+++++++++++++++++++++++
Mdom/base/Document.h | 7+++++++
Mdom/base/Location.cpp | 43+++++++++++++++++++++++++++++++++++++++++++
Mdom/base/Location.h | 8++++++++
Mdom/html/HTMLIFrameElement.cpp | 20++++++++++++++++++++
Mdom/html/HTMLIFrameElement.h | 1+
Mdom/ipc/ContentParent.cpp | 19+++++++++++++++++++
Mdom/ipc/ContentParent.h | 7+++++++
Mdom/ipc/PContent.ipdl | 7+++++++
Mdom/webidl/Location.webidl | 4+++-
Mipc/glue/BackgroundUtils.cpp | 14+++++++++++++-
Mmodules/libpref/init/StaticPrefList.yaml | 8++++++++
Mnetwerk/ipc/DocumentLoadListener.cpp | 26++++++++++++++++++++++++++
Mnetwerk/ipc/NeckoChannelParams.ipdlh | 2++
Atesting/web-platform/tests/html/browsers/history/the-location-interface/location-ancestor-origins.sub.html | 328+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atesting/web-platform/tests/html/browsers/history/the-location-interface/resources/location-ancestor-origins-create-iframe.js | 20++++++++++++++++++++
Atesting/web-platform/tests/html/browsers/history/the-location-interface/resources/location-ancestor-origins-recursive-iframe.html | 43+++++++++++++++++++++++++++++++++++++++++++
20 files changed, 754 insertions(+), 2 deletions(-)

diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp @@ -3702,6 +3702,87 @@ void CanonicalBrowsingContext::MaybeReconstructActiveEntryList() { } } +// https://html.spec.whatwg.org/#concept-internal-location-ancestor-origin-objects-list +void CanonicalBrowsingContext::CreateRedactedAncestorOriginsList() { + 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 = GetEmbedderFrameReferrerPolicy(); + + // 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( + GetCurrentWindowGlobal()->DocumentPrincipal())) { + // 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()); diff --git a/docshell/base/CanonicalBrowsingContext.h b/docshell/base/CanonicalBrowsingContext.h @@ -342,6 +342,14 @@ class CanonicalBrowsingContext final : public BrowsingContext { return mContainerFeaturePolicyInfo; } + void SetEmbedderFrameReferrerPolicy(ReferrerPolicy aPolicy) { + mEmbedderFrameReferrerPolicy = aPolicy; + } + + ReferrerPolicy GetEmbedderFrameReferrerPolicy() const { + return mEmbedderFrameReferrerPolicy; + } + void SetRestoreData(SessionStoreRestoreData* aData, ErrorResult& aError); void ClearRestoreState(); MOZ_CAN_RUN_SCRIPT_BOUNDARY void RequestRestoreTabContent( @@ -455,6 +463,14 @@ 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(); + + Span<const nsCOMPtr<nsIPrincipal>> GetPossiblyRedactedAncestorOriginsList() + const; + void SetPossiblyRedactedAncestorOriginsList( + nsTArray<nsCOMPtr<nsIPrincipal>> aAncestorOriginsList); + protected: // Called when the browsing context is being discarded. void CanonicalDiscard(); @@ -639,6 +655,7 @@ class CanonicalBrowsingContext final : public BrowsingContext { RefPtr<nsBrowserStatusFilter> mStatusFilter; Maybe<FeaturePolicyInfo> mContainerFeaturePolicyInfo; + ReferrerPolicy mEmbedderFrameReferrerPolicy = ReferrerPolicy::_empty; friend class BrowserSessionStore; WeakPtr<SessionStoreFormData>& GetSessionStoreFormDataRef() { @@ -674,6 +691,8 @@ class CanonicalBrowsingContext final : public BrowsingContext { bool mFullyDiscarded = false; nsTArray<std::function<void(uint64_t)>> mFullyDiscardedListeners; + + nsTArray<nsCOMPtr<nsIPrincipal>> mPossiblyRedactedAncestorOriginsList; }; } // namespace dom diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp @@ -6670,6 +6670,75 @@ nsresult nsDocShell::CreateInitialDocumentViewer( return rv; } +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; + } + + (void)ContentChild::GetSingleton()->SendUpdateAncestorOriginsList(bc); + const auto* frame = bc->GetEmbedderElement(); + + auto referrerPolicy = frame->GetReferrerPolicyAsEnum(); + 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, @@ -6905,6 +6974,13 @@ nsresult nsDocShell::CreateAboutBlankDocumentViewer( } else { blankDoc->InitFeaturePolicy(AsVariant(Nothing{})); } + + // Perform redacted location.ancestorOrigins algorithm for about:blank + if (BrowsingContext* bc = GetBrowsingContext(); + bc && bc->GetEmbedderElement()) { + // This call requires bc && bc->GetEmbedderElement() != nullptr. + CreateAboutBlankAncestorOriginsForNonTopLevel(blankDoc); + } } } diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp @@ -3757,6 +3757,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()); @@ -17941,6 +17947,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 @@ -1983,6 +1983,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(); @@ -5312,6 +5317,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, @@ -196,6 +198,13 @@ void HTMLIFrameElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, } else if (aName == nsGkAtoms::allowfullscreen) { RefreshFeaturePolicy(false /* parse the feature policy attribute */); } + + if (aName == nsGkAtoms::referrerpolicy) { + const auto newValue = ReferrerPolicyFromAttr(aValue); + if (newValue != ReferrerPolicyFromAttr(aOldValue)) { + RefreshEmbedderReferrerPolicy(newValue); + } + } } return nsGenericHTMLFrameElement::AfterSetAttr( @@ -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->SendSetEmbedderFrameReferrerPolicy(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,25 @@ mozilla::ipc::IPCResult ContentParent::RecvSetContainerFeaturePolicy( return IPC_OK(); } +mozilla::ipc::IPCResult ContentParent::RecvSetEmbedderFrameReferrerPolicy( + const MaybeDiscardedBrowsingContext& aContainerContext, + ReferrerPolicy&& aPolicy) { + if (!aContainerContext.IsNullOrDiscarded()) { + aContainerContext.get_canonical()->SetEmbedderFrameReferrerPolicy(aPolicy); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ContentParent::RecvUpdateAncestorOriginsList( + const MaybeDiscardedBrowsingContext& aContext) { + if (!aContext.IsNullOrDiscarded()) { + aContext.get_canonical()->CreateRedactedAncestorOriginsList(); + } + + 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 RecvSetEmbedderFrameReferrerPolicy( + const MaybeDiscardedBrowsingContext& aContainerContext, + ReferrerPolicy&& aPolicy); + + mozilla::ipc::IPCResult RecvUpdateAncestorOriginsList( + const MaybeDiscardedBrowsingContext& aContext); + 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,12 @@ parent: async SetContainerFeaturePolicy(MaybeDiscardedBrowsingContext aContainerContext, MaybeFeaturePolicyInfo aContainerFeaturePolicyInfo); + async SetEmbedderFrameReferrerPolicy(MaybeDiscardedBrowsingContext aContainerContext, + ReferrerPolicy aPolicy); + + // Notify parent process that aContext must update its ancestor origins list + async UpdateAncestorOriginsList(MaybeDiscardedBrowsingContext aContext); + // 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 @@ -611,7 +611,7 @@ nsresult LoadInfoToLoadInfoArgs(nsILoadInfo* aLoadInfo, aLoadInfo->GetIsMetaRefresh(), aLoadInfo->GetLoadingEmbedderPolicy(), aLoadInfo->GetIsOriginTrialCoepCredentiallessEnabledForTopLevel(), unstrippedURI, interceptionInfoArg, aLoadInfo->GetIsNewWindowTarget(), - aLoadInfo->GetUserNavigationInvolvement()); + aLoadInfo->GetUserNavigationInvolvement(), {}); return NS_OK; } @@ -762,6 +762,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 @@ -3557,6 +3557,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: false + mirror: always + # Whether "W3C Web Manifest" processing is enabled - name: dom.manifest.enabled type: bool 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" @@ -1690,6 +1691,31 @@ 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()) { + auto* ctx = bc->Canonical(); + ctx->CreateRedactedAncestorOriginsList(); + // The ancestorOrigins list the document should ultimately have, that we + // send down with load args. + auto& ancestorOrigins = aArgs.loadInfo().ancestorOrigins(); + 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(); + }; + for (const auto& ancestorPrincipal : + ctx->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 @@ -223,6 +223,8 @@ struct LoadInfoArgs InterceptionInfoArg? interceptionInfo; bool isNewWindowTarget; UserNavigationInvolvement userNavigationInvolvement; + // https://html.spec.whatwg.org/#dom-location-ancestororigins + PrincipalInfo?[] ancestorOrigins; }; /** 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>