From eece505eef00b1a15c0f0f1c43e72bbf41e4551d Mon Sep 17 00:00:00 2001 From: Nika Layzell Date: Wed, 8 Mar 2023 21:09:20 +0000 Subject: [PATCH] Bug 1810619 - Part 1: Be more precise in named lookup code, r=smaug,geckoview-reviewers,m_kato This makes various changes to the named lookup/navigation code to make them more precise, and avoid issues which could happen if a window is closed while script is still executing. This also should improve handling for inactive windows in some cases, by more frequently working off of the WindowContext tree rather than the BrowsingContext tree. As part of these changes, some behaviour was changed around e.g. the file URI exception to avoid the deprecated nsIPrincipal::GetURI method. I don't believe the behaviour should have changed in a meaningful way. Differential Revision: https://phabricator.services.mozilla.com/D171755 --- docshell/base/BrowsingContext.cpp | 162 ++--------------- docshell/base/BrowsingContext.h | 37 ++-- docshell/base/nsDocShell.cpp | 61 +------ docshell/base/nsDocShell.h | 5 - .../browser/browser_browsingContext-02.js | 12 +- dom/base/nsGlobalWindowOuter.cpp | 11 +- dom/chrome-webidl/BrowsingContext.webidl | 3 - dom/chrome-webidl/WindowGlobalActors.webidl | 2 + dom/clients/manager/ClientOpenWindowUtils.cpp | 33 ++-- dom/ipc/WindowGlobalChild.cpp | 164 ++++++++++++++++++ dom/ipc/WindowGlobalChild.h | 17 ++ .../modules/geckoview/GeckoViewNavigation.jsm | 5 +- .../windowwatcher/nsIWindowWatcher.idl | 11 +- .../windowwatcher/nsWindowWatcher.cpp | 95 +++++----- .../windowwatcher/nsWindowWatcher.h | 7 - toolkit/mozapps/extensions/AbuseReporter.jsm | 2 +- .../test/browser/head_abuse_report.js | 2 +- 17 files changed, 301 insertions(+), 328 deletions(-) diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp index b270badc02ec..63c7beea93f8 100644 --- a/docshell/base/BrowsingContext.cpp +++ b/docshell/base/BrowsingContext.cpp @@ -1194,104 +1194,15 @@ void BrowsingContext::GetAllBrowsingContextsInSubtree( }); } -// FindWithName follows the rules for choosing a browsing context, -// with the exception of sandboxing for iframes. The implementation -// for arbitrarily choosing between two browsing contexts with the -// same name is as follows: -// -// 1) The start browsing context, i.e. 'this' -// 2) Descendants in insertion order -// 3) The parent -// 4) Siblings and their children, both in insertion order -// 5) After this we iteratively follow the parent chain, repeating 3 -// and 4 until -// 6) If there is no parent, consider all other top level browsing -// contexts and their children, both in insertion order -// -// See -// https://html.spec.whatwg.org/multipage/browsers.html#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name -BrowsingContext* BrowsingContext::FindWithName( - const nsAString& aName, bool aUseEntryGlobalForAccessCheck) { - RefPtr requestingContext = this; - if (aUseEntryGlobalForAccessCheck) { - if (nsGlobalWindowInner* caller = nsContentUtils::EntryInnerWindow()) { - if (caller->GetBrowsingContextGroup() == Group()) { - requestingContext = caller->GetBrowsingContext(); - } else { - MOZ_RELEASE_ASSERT(caller->GetPrincipal()->IsSystemPrincipal(), - "caller must be either same-group or system"); - } - } - } - MOZ_ASSERT(requestingContext, "must have a requestingContext"); - - BrowsingContext* found = nullptr; - if (aName.IsEmpty()) { - // You can't find a browsing context with an empty name. - found = nullptr; - } else if (aName.LowerCaseEqualsLiteral("_blank")) { - // Just return null. Caller must handle creating a new window with - // a blank name. - found = nullptr; - } else if (nsContentUtils::IsSpecialName(aName)) { - found = FindWithSpecialName(aName, *requestingContext); - } else if (BrowsingContext* child = - FindWithNameInSubtree(aName, *requestingContext)) { - found = child; - } else { - BrowsingContext* current = this; - - do { - Span> siblings; - BrowsingContext* parent = current->GetParent(); - - if (!parent) { - // We've reached the root of the tree, consider browsing - // contexts in the same browsing context group. - siblings = mGroup->Toplevels(); - } else if (parent->NameEquals(aName) && - requestingContext->CanAccess(parent) && - parent->IsTargetable()) { - found = parent; - break; - } else { - siblings = parent->NonSyntheticChildren(); - } - - for (BrowsingContext* sibling : siblings) { - if (sibling == current) { - continue; - } - - if (BrowsingContext* relative = - sibling->FindWithNameInSubtree(aName, *requestingContext)) { - found = relative; - // Breaks the outer loop - parent = nullptr; - break; - } - } - - current = parent; - } while (current); - } - - // Helpers should perform access control checks, which means that we - // only need to assert that we can access found. - MOZ_DIAGNOSTIC_ASSERT(!found || requestingContext->CanAccess(found)); - - return found; -} - BrowsingContext* BrowsingContext::FindChildWithName( - const nsAString& aName, BrowsingContext& aRequestingContext) { + const nsAString& aName, WindowGlobalChild& aRequestingWindow) { if (aName.IsEmpty()) { // You can't find a browsing context with the empty name. return nullptr; } for (BrowsingContext* child : NonSyntheticChildren()) { - if (child->NameEquals(aName) && aRequestingContext.CanAccess(child) && + if (child->NameEquals(aName) && aRequestingWindow.CanNavigate(child) && child->IsTargetable()) { return child; } @@ -1301,7 +1212,7 @@ BrowsingContext* BrowsingContext::FindChildWithName( } BrowsingContext* BrowsingContext::FindWithSpecialName( - const nsAString& aName, BrowsingContext& aRequestingContext) { + const nsAString& aName, WindowGlobalChild& aRequestingWindow) { // TODO(farre): Neither BrowsingContext nor nsDocShell checks if the // browsing context pointed to by a special name is active. Should // it be? See Bug 1527913. @@ -1311,7 +1222,7 @@ BrowsingContext* BrowsingContext::FindWithSpecialName( if (aName.LowerCaseEqualsLiteral("_parent")) { if (BrowsingContext* parent = GetParent()) { - return aRequestingContext.CanAccess(parent) ? parent : nullptr; + return aRequestingWindow.CanNavigate(parent) ? parent : nullptr; } return this; } @@ -1319,24 +1230,25 @@ BrowsingContext* BrowsingContext::FindWithSpecialName( if (aName.LowerCaseEqualsLiteral("_top")) { BrowsingContext* top = Top(); - return aRequestingContext.CanAccess(top) ? top : nullptr; + return aRequestingWindow.CanNavigate(top) ? top : nullptr; } return nullptr; } BrowsingContext* BrowsingContext::FindWithNameInSubtree( - const nsAString& aName, BrowsingContext& aRequestingContext) { + const nsAString& aName, WindowGlobalChild* aRequestingWindow) { MOZ_DIAGNOSTIC_ASSERT(!aName.IsEmpty()); - if (NameEquals(aName) && aRequestingContext.CanAccess(this) && + if (NameEquals(aName) && + (!aRequestingWindow || aRequestingWindow->CanNavigate(this)) && IsTargetable()) { return this; } for (BrowsingContext* child : NonSyntheticChildren()) { if (BrowsingContext* found = - child->FindWithNameInSubtree(aName, aRequestingContext)) { + child->FindWithNameInSubtree(aName, aRequestingWindow)) { return found; } } @@ -1344,48 +1256,6 @@ BrowsingContext* BrowsingContext::FindWithNameInSubtree( return nullptr; } -// For historical context, see: -// -// Bug 13871: Prevent frameset spoofing -// Bug 103638: Targets with same name in different windows open in wrong -// window with javascript -// Bug 408052: Adopt "ancestor" frame navigation policy -// Bug 1570207: Refactor logic to rely on BrowsingContextGroups to enforce -// origin attribute isolation. -bool BrowsingContext::CanAccess(BrowsingContext* aTarget, - bool aConsiderOpener) { - MOZ_ASSERT( - mDocShell, - "CanAccess() may only be called in the process of the accessing window"); - MOZ_ASSERT(aTarget, "Must have a target"); - - MOZ_DIAGNOSTIC_ASSERT( - Group() == aTarget->Group(), - "A BrowsingContext should never see a context from a different group"); - - // A frame can navigate itself and its own root. - if (aTarget == this || aTarget == Top()) { - return true; - } - - // A frame can navigate any frame with a same-origin ancestor. - for (BrowsingContext* bc = aTarget; bc; bc = bc->GetParent()) { - if (bc->mDocShell && nsDocShell::ValidateOrigin(this, bc)) { - return true; - } - } - - // If the target is a top-level document, a frame can navigate it if it can - // navigate its opener. - if (aConsiderOpener && !aTarget->GetParent()) { - if (RefPtr opener = aTarget->GetOpener()) { - return CanAccess(opener, false); - } - } - - return false; -} - bool BrowsingContext::IsSandboxedFrom(BrowsingContext* aTarget) { // If no target then not sandboxed. if (!aTarget) { @@ -2070,13 +1940,12 @@ nsresult BrowsingContext::LoadURI(nsDocShellLoadState* aLoadState, MOZ_DIAGNOSTIC_ASSERT(!sourceBC || sourceBC->Group() == Group()); if (sourceBC && sourceBC->IsInProcess()) { - if (!sourceBC->CanAccess(this)) { - return NS_ERROR_DOM_PROP_ACCESS_DENIED; - } - nsCOMPtr win(sourceBC->GetDOMWindow()); if (WindowGlobalChild* wgc = win->GetCurrentInnerWindow()->GetWindowGlobalChild()) { + if (!wgc->CanNavigate(this)) { + return NS_ERROR_DOM_PROP_ACCESS_DENIED; + } wgc->SendLoadURI(this, aLoadState, aSetNavigating); } } else if (XRE_IsParentProcess()) { @@ -2175,16 +2044,15 @@ nsresult BrowsingContext::InternalLoad(nsDocShellLoadState* aLoadState) { MOZ_DIAGNOSTIC_ASSERT(sourceBC); MOZ_DIAGNOSTIC_ASSERT(sourceBC->Group() == Group()); - if (!sourceBC->CanAccess(this)) { - return NS_ERROR_DOM_PROP_ACCESS_DENIED; - } - nsCOMPtr win(sourceBC->GetDOMWindow()); WindowGlobalChild* wgc = win->GetCurrentInnerWindow()->GetWindowGlobalChild(); if (!wgc || !wgc->CanSend()) { return NS_ERROR_FAILURE; } + if (!wgc->CanNavigate(this)) { + return NS_ERROR_DOM_PROP_ACCESS_DENIED; + } MOZ_ALWAYS_SUCCEEDS( SetCurrentLoadIdentifier(Some(aLoadState->GetLoadIdentifier()))); diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h index 9fc591e0981d..43d4f8aa199e 100644 --- a/docshell/base/BrowsingContext.h +++ b/docshell/base/BrowsingContext.h @@ -75,6 +75,7 @@ class SessionHistoryInfo; class SessionStorageManager; class StructuredCloneHolder; class WindowContext; +class WindowGlobalChild; struct WindowPostMessageOptions; class WindowProxyHolder; @@ -659,32 +660,26 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { } } - // Using the rules for choosing a browsing context we try to find - // the browsing context with the given name in the set of - // transitively reachable browsing contexts. Performs access control - // checks with regard to this. - // See - // https://html.spec.whatwg.org/multipage/browsers.html#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name. - // - // BrowsingContext::FindWithName(const nsAString&) is equivalent to - // calling nsIDocShellTreeItem::FindItemWithName(aName, nullptr, - // nullptr, false, ). - BrowsingContext* FindWithName(const nsAString& aName, - bool aUseEntryGlobalForAccessCheck = true); - // Find a browsing context in this context's list of // children. Doesn't consider the special names, '_self', '_parent', // '_top', or '_blank'. Performs access control checks with regard to // 'this'. BrowsingContext* FindChildWithName(const nsAString& aName, - BrowsingContext& aRequestingContext); + WindowGlobalChild& aRequestingWindow); // Find a browsing context in the subtree rooted at 'this' Doesn't // consider the special names, '_self', '_parent', '_top', or - // '_blank'. Performs access control checks with regard to - // 'aRequestingContext'. + // '_blank'. + // + // If passed, performs access control checks with regard to + // 'aRequestingContext', otherwise performs no access checks. BrowsingContext* FindWithNameInSubtree(const nsAString& aName, - BrowsingContext& aRequestingContext); + WindowGlobalChild* aRequestingWindow); + + // Find the special browsing context if aName is '_self', '_parent', + // '_top', but not '_blank'. The latter is handled in FindWithName + BrowsingContext* FindWithSpecialName(const nsAString& aName, + WindowGlobalChild& aRequestingWindow); nsISupports* GetParentObject() const; JSObject* WrapObject(JSContext* aCx, @@ -791,9 +786,6 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { BrowsingContextGroup* aGroup, ContentParent* aOriginProcess); - // Performs access control to check that 'this' can access 'aTarget'. - bool CanAccess(BrowsingContext* aTarget, bool aConsiderOpener = true); - bool IsSandboxedFrom(BrowsingContext* aTarget); // The runnable will be called once there is idle time, or the top level @@ -964,11 +956,6 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { // parent WC changes. void RecomputeCanExecuteScripts(); - // Find the special browsing context if aName is '_self', '_parent', - // '_top', but not '_blank'. The latter is handled in FindWithName - BrowsingContext* FindWithSpecialName(const nsAString& aName, - BrowsingContext& aRequestingContext); - // Is it early enough in the BrowsingContext's lifecycle that it is still // OK to set OriginAttributes? bool CanSetOriginAttributes(); diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index 16033a3105bd..3995e91af191 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -1438,61 +1438,6 @@ nsDOMNavigationTiming* nsDocShell::GetNavigationTiming() const { return mTiming; } -// -// Bug 13871: Prevent frameset spoofing -// -// This routine answers: 'Is origin's document from same domain as -// target's document?' -// -// file: uris are considered the same domain for the purpose of -// frame navigation regardless of script accessibility (bug 420425) -// -/* static */ -bool nsDocShell::ValidateOrigin(BrowsingContext* aOrigin, - BrowsingContext* aTarget) { - nsIDocShell* originDocShell = aOrigin->GetDocShell(); - MOZ_ASSERT(originDocShell, "originDocShell must not be null"); - Document* originDocument = originDocShell->GetDocument(); - NS_ENSURE_TRUE(originDocument, false); - - nsIDocShell* targetDocShell = aTarget->GetDocShell(); - MOZ_ASSERT(targetDocShell, "targetDocShell must not be null"); - Document* targetDocument = targetDocShell->GetDocument(); - NS_ENSURE_TRUE(targetDocument, false); - - bool equal; - nsresult rv = originDocument->NodePrincipal()->Equals( - targetDocument->NodePrincipal(), &equal); - if (NS_SUCCEEDED(rv) && equal) { - return true; - } - // Not strictly equal, special case if both are file: uris - nsCOMPtr originURI; - nsCOMPtr targetURI; - nsCOMPtr innerOriginURI; - nsCOMPtr innerTargetURI; - - // Casting to BasePrincipal, as we can't get InnerMost URI otherwise - auto* originDocumentBasePrincipal = - BasePrincipal::Cast(originDocument->NodePrincipal()); - - rv = originDocumentBasePrincipal->GetURI(getter_AddRefs(originURI)); - if (NS_SUCCEEDED(rv) && originURI) { - innerOriginURI = NS_GetInnermostURI(originURI); - } - - auto* targetDocumentBasePrincipal = - BasePrincipal::Cast(targetDocument->NodePrincipal()); - - rv = targetDocumentBasePrincipal->GetURI(getter_AddRefs(targetURI)); - if (NS_SUCCEEDED(rv) && targetURI) { - innerTargetURI = NS_GetInnermostURI(targetURI); - } - - return innerOriginURI && innerTargetURI && SchemeIsFile(innerOriginURI) && - SchemeIsFile(innerTargetURI); -} - nsPresContext* nsDocShell::GetEldestPresContext() { nsIContentViewer* viewer = mContentViewer; while (viewer) { @@ -8499,7 +8444,11 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) { aLoadState->Target().LowerCaseEqualsLiteral("_self") || aLoadState->Target().LowerCaseEqualsLiteral("_parent") || aLoadState->Target().LowerCaseEqualsLiteral("_top")) { - targetContext = mBrowsingContext->FindWithName( + Document* document = GetDocument(); + NS_ENSURE_TRUE(document, NS_ERROR_FAILURE); + WindowGlobalChild* wgc = document->GetWindowGlobalChild(); + NS_ENSURE_TRUE(wgc, NS_ERROR_FAILURE); + targetContext = wgc->FindBrowsingContextWithName( aLoadState->Target(), /* aUseEntryGlobalForAccessCheck */ false); } diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h index 28f02a3ce860..4b7af3a55d92 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -558,11 +558,6 @@ class nsDocShell final : public nsDocLoader, nsDocShell(mozilla::dom::BrowsingContext* aBrowsingContext, uint64_t aContentWindowID); - // Security check to prevent frameset spoofing. See comments at - // implementation site. - static bool ValidateOrigin(mozilla::dom::BrowsingContext* aOrigin, - mozilla::dom::BrowsingContext* aTarget); - static inline uint32_t PRTimeToSeconds(PRTime aTimeUsec) { return uint32_t(aTimeUsec / PR_USEC_PER_SEC); } diff --git a/docshell/test/browser/browser_browsingContext-02.js b/docshell/test/browser/browser_browsingContext-02.js index 5d09802412ba..b3108b2617ff 100644 --- a/docshell/test/browser/browser_browsingContext-02.js +++ b/docshell/test/browser/browser_browsingContext-02.js @@ -107,12 +107,14 @@ add_task(async function() { // wish to confirm that targeting is able to find // appropriate browsing contexts. - // BrowsingContext.findWithName requires access checks, which - // can only be performed in the process of the accessor BC's - // docShell. + // WindowGlobalChild.findBrowsingContextWithName requires access + // checks, which can only be performed in the process of the accessor + // WindowGlobalChild. function findWithName(bc, name) { - return content.SpecialPowers.spawn(bc, [bc, name], (bc, name) => { - return bc.findWithName(name); + return content.SpecialPowers.spawn(bc, [name], name => { + return content.windowGlobalChild.findBrowsingContextWithName( + name + ); }); } diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index 7a5df6943c76..01d489bc1374 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -3966,9 +3966,11 @@ Nullable nsGlobalWindowOuter::GetTopOuter() { already_AddRefed nsGlobalWindowOuter::GetChildWindow( const nsAString& aName) { NS_ENSURE_TRUE(mBrowsingContext, nullptr); + NS_ENSURE_TRUE(mInnerWindow, nullptr); + NS_ENSURE_TRUE(mInnerWindow->GetWindowGlobalChild(), nullptr); - return do_AddRef( - mBrowsingContext->FindChildWithName(aName, *mBrowsingContext)); + return do_AddRef(mBrowsingContext->FindChildWithName( + aName, *mInnerWindow->GetWindowGlobalChild())); } bool nsGlobalWindowOuter::DispatchCustomEvent( @@ -4036,7 +4038,10 @@ bool nsGlobalWindowOuter::WindowExists(const nsAString& aName, aName.LowerCaseEqualsLiteral("_parent"); } - return !!mBrowsingContext->FindWithName(aName, aLookForCallerOnJSStack); + if (WindowGlobalChild* wgc = mInnerWindow->GetWindowGlobalChild()) { + return wgc->FindBrowsingContextWithName(aName, aLookForCallerOnJSStack); + } + return false; } already_AddRefed nsGlobalWindowOuter::GetMainWidget() { diff --git a/dom/chrome-webidl/BrowsingContext.webidl b/dom/chrome-webidl/BrowsingContext.webidl index 369da91c6e31..186b007c09d1 100644 --- a/dom/chrome-webidl/BrowsingContext.webidl +++ b/dom/chrome-webidl/BrowsingContext.webidl @@ -71,9 +71,6 @@ interface BrowsingContext { sequence getAllBrowsingContextsInSubtree(); - BrowsingContext? findChildWithName(DOMString name, BrowsingContext accessor); - BrowsingContext? findWithName(DOMString name); - readonly attribute DOMString name; readonly attribute BrowsingContext? parent; diff --git a/dom/chrome-webidl/WindowGlobalActors.webidl b/dom/chrome-webidl/WindowGlobalActors.webidl index e860e98891f5..b116c569d102 100644 --- a/dom/chrome-webidl/WindowGlobalActors.webidl +++ b/dom/chrome-webidl/WindowGlobalActors.webidl @@ -177,6 +177,8 @@ interface WindowGlobalChild { static WindowGlobalChild? getByInnerWindowId(unsigned long long innerWIndowId); + BrowsingContext? findBrowsingContextWithName(DOMString name); + /** * Get or create the JSWindowActor with the given name. * diff --git a/dom/clients/manager/ClientOpenWindowUtils.cpp b/dom/clients/manager/ClientOpenWindowUtils.cpp index 4c6524a425bc..c9edd51892d5 100644 --- a/dom/clients/manager/ClientOpenWindowUtils.cpp +++ b/dom/clients/manager/ClientOpenWindowUtils.cpp @@ -17,6 +17,7 @@ #include "nsFocusManager.h" #include "nsIBrowserDOMWindow.h" #include "nsIDocShell.h" +#include "nsIDocShellTreeOwner.h" #include "nsIDOMChromeWindow.h" #include "nsIURI.h" #include "nsIBrowser.h" @@ -327,23 +328,27 @@ void GeckoViewOpenWindow(const ClientOpenWindowArgsParsed& aArgsValidated, promiseResult->Then( GetMainThreadSerialEventTarget(), __func__, [aArgsValidated, promise](nsString sessionId) { - nsresult rv; - nsCOMPtr wwatch = - do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); - if (NS_WARN_IF(NS_FAILED(rv))) { - promise->Reject(rv, __func__); - return rv; - } - // Retrieve the browsing context by using the GeckoSession ID. The // window is named the same as the ID of the GeckoSession it is // associated with. - RefPtr browsingContext = - static_cast(wwatch.get()) - ->GetBrowsingContextByName(sessionId, false, nullptr); - if (NS_WARN_IF(!browsingContext)) { - promise->Reject(NS_ERROR_FAILURE, __func__); - return NS_ERROR_FAILURE; + RefPtr browsingContext; + nsresult rv = [&sessionId, &browsingContext]() -> nsresult { + nsresult rv; + nsCOMPtr wwatch = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr chromeWindow; + rv = wwatch->GetWindowByName(sessionId, getter_AddRefs(chromeWindow)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(chromeWindow, NS_ERROR_FAILURE); + browsingContext = + nsPIDOMWindowOuter::From(chromeWindow)->GetBrowsingContext(); + NS_ENSURE_TRUE(browsingContext, NS_ERROR_FAILURE); + return NS_OK; + }(); + if (NS_WARN_IF(NS_FAILED(rv))) { + promise->Reject(rv, __func__); + return rv; } WaitForLoad(aArgsValidated, browsingContext, promise); diff --git a/dom/ipc/WindowGlobalChild.cpp b/dom/ipc/WindowGlobalChild.cpp index 8f3fe436d73c..f400bef2b42d 100644 --- a/dom/ipc/WindowGlobalChild.cpp +++ b/dom/ipc/WindowGlobalChild.cpp @@ -41,6 +41,7 @@ #include "mozilla/dom/JSActorService.h" #include "nsIHttpChannelInternal.h" #include "nsIURIMutator.h" +#include "nsURLHelper.h" using namespace mozilla::ipc; using namespace mozilla::dom::ipc; @@ -641,6 +642,169 @@ bool WindowGlobalChild::SameOriginWithTop() { return IsSameOriginWith(WindowContext()->TopWindowContext()); } +// For historical context, see: +// +// Bug 13871: Prevent frameset spoofing +// Bug 103638: Targets with same name in different windows open in wrong +// window with javascript +// Bug 408052: Adopt "ancestor" frame navigation policy +// Bug 1570207: Refactor logic to rely on BrowsingContextGroups to enforce +// origin attribute isolation +// Bug 1810619: Crash at null in nsDocShell::ValidateOrigin +bool WindowGlobalChild::CanNavigate(dom::BrowsingContext* aTarget, + bool aConsiderOpener) { + MOZ_DIAGNOSTIC_ASSERT(WindowContext()->Group() == aTarget->Group(), + "A WindowGlobalChild should never try to navigate a " + "BrowsingContext from another group"); + + auto isFileScheme = [](nsIPrincipal* aPrincipal) -> bool { + // NOTE: This code previously checked for a file scheme using + // `nsIPrincipal::GetURI()` combined with `NS_GetInnermostURI`. We no longer + // use GetURI, as it has been deprecated, and it makes more sense to take + // advantage of the pre-computed origin, which will already use the + // innermost URI (bug 1810619) + nsAutoCString origin, scheme; + return NS_SUCCEEDED(aPrincipal->GetOriginNoSuffix(origin)) && + NS_SUCCEEDED(net_ExtractURLScheme(origin, scheme)) && + scheme == "file"_ns; + }; + + // A frame can navigate itself and its own root. + if (aTarget == BrowsingContext() || aTarget == BrowsingContext()->Top()) { + return true; + } + + // If the target frame doesn't yet have a WindowContext, start checking + // principals from its direct ancestor instead. It would inherit its principal + // from this document upon creation. + dom::WindowContext* initialWc = aTarget->GetCurrentWindowContext(); + if (!initialWc) { + initialWc = aTarget->GetParentWindowContext(); + } + + // A frame can navigate any frame with a same-origin ancestor. + bool isFileDocument = isFileScheme(DocumentPrincipal()); + for (dom::WindowContext* wc = initialWc; wc; + wc = wc->GetParentWindowContext()) { + dom::WindowGlobalChild* wgc = wc->GetWindowGlobalChild(); + if (!wgc) { + continue; // out-of process, so not same-origin. + } + + if (DocumentPrincipal()->Equals(wgc->DocumentPrincipal())) { + return true; + } + + // Not strictly equal, special case if both are file: URIs. + // + // file: URIs are considered the same domain for the purpose of frame + // navigation, regardless of script accessibility (bug 420425). + if (isFileDocument && isFileScheme(wgc->DocumentPrincipal())) { + return true; + } + } + + // If the target is a top-level document, a frame can navigate it if it can + // navigate its opener. + if (aConsiderOpener && !aTarget->GetParent()) { + if (RefPtr opener = aTarget->GetOpener()) { + return CanNavigate(opener, false); + } + } + + return false; +} + +// FindWithName follows the rules for choosing a browsing context, +// with the exception of sandboxing for iframes. The implementation +// for arbitrarily choosing between two browsing contexts with the +// same name is as follows: +// +// 1) The start browsing context, i.e. 'this' +// 2) Descendants in insertion order +// 3) The parent +// 4) Siblings and their children, both in insertion order +// 5) After this we iteratively follow the parent chain, repeating 3 +// and 4 until +// 6) If there is no parent, consider all other top level browsing +// contexts and their children, both in insertion order +// +// See +// https://html.spec.whatwg.org/multipage/browsers.html#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name +dom::BrowsingContext* WindowGlobalChild::FindBrowsingContextWithName( + const nsAString& aName, bool aUseEntryGlobalForAccessCheck) { + RefPtr requestingContext = this; + if (aUseEntryGlobalForAccessCheck) { + if (nsGlobalWindowInner* caller = nsContentUtils::EntryInnerWindow()) { + if (caller->GetBrowsingContextGroup() == WindowContext()->Group()) { + requestingContext = caller->GetWindowGlobalChild(); + } else { + MOZ_RELEASE_ASSERT(caller->GetPrincipal()->IsSystemPrincipal(), + "caller must be either same-group or system"); + } + } + } + MOZ_ASSERT(requestingContext, "must have a requestingContext"); + + dom::BrowsingContext* found = nullptr; + if (aName.IsEmpty()) { + // You can't find a browsing context with an empty name. + found = nullptr; + } else if (aName.LowerCaseEqualsLiteral("_blank")) { + // Just return null. Caller must handle creating a new window with + // a blank name. + found = nullptr; + } else if (nsContentUtils::IsSpecialName(aName)) { + found = BrowsingContext()->FindWithSpecialName(aName, *requestingContext); + } else if (dom::BrowsingContext* child = + BrowsingContext()->FindWithNameInSubtree(aName, + requestingContext)) { + found = child; + } else { + dom::WindowContext* current = WindowContext(); + + do { + Span> siblings; + dom::WindowContext* parent = current->GetParentWindowContext(); + + if (!parent) { + // We've reached the root of the tree, consider browsing + // contexts in the same browsing context group. + siblings = WindowContext()->Group()->Toplevels(); + } else if (dom::BrowsingContext* bc = parent->GetBrowsingContext(); + bc && bc->NameEquals(aName) && + requestingContext->CanNavigate(bc) && bc->IsTargetable()) { + found = bc; + break; + } else { + siblings = parent->NonSyntheticChildren(); + } + + for (dom::BrowsingContext* sibling : siblings) { + if (sibling == current->GetBrowsingContext()) { + continue; + } + + if (dom::BrowsingContext* relative = + sibling->FindWithNameInSubtree(aName, requestingContext)) { + found = relative; + // Breaks the outer loop + parent = nullptr; + break; + } + } + + current = parent; + } while (current); + } + + // Helpers should perform access control checks, which means that we + // only need to assert that we can access found. + MOZ_DIAGNOSTIC_ASSERT(!found || requestingContext->CanNavigate(found)); + + return found; +} + void WindowGlobalChild::UnblockBFCacheFor(BFCacheStatus aStatus) { SendUpdateBFCacheStatus(0, aStatus); } diff --git a/dom/ipc/WindowGlobalChild.h b/dom/ipc/WindowGlobalChild.h index d5eca95ef391..057238678813 100644 --- a/dom/ipc/WindowGlobalChild.h +++ b/dom/ipc/WindowGlobalChild.h @@ -123,6 +123,23 @@ class WindowGlobalChild final : public WindowGlobalActor, bool SameOriginWithTop(); + // Returns `true` if this WindowGlobal is allowed to navigate the given + // BrowsingContext. BrowsingContexts which are currently out-of-process are + // supported, and assumed to be cross-origin. + // + // The given BrowsingContext must be in the same BrowsingContextGroup as this + // WindowGlobal. + bool CanNavigate(dom::BrowsingContext* aTarget, bool aConsiderOpener = true); + + // Using the rules for choosing a browsing context we try to find + // the browsing context with the given name in the set of + // transitively reachable browsing contexts. Performs access control + // checks with regard to this. + // See + // https://html.spec.whatwg.org/multipage/browsers.html#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name. + dom::BrowsingContext* FindBrowsingContextWithName( + const nsAString& aName, bool aUseEntryGlobalForAccessCheck = true); + nsISupports* GetParentObject(); JSObject* WrapObject(JSContext* aCx, JS::Handle aGivenProto) override; diff --git a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm index da7640cc01f6..ff6c0003bf4b 100644 --- a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm +++ b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm @@ -193,10 +193,7 @@ class GeckoViewNavigation extends GeckoViewModule { let triggeringPrincipal, referrerInfo, csp; if (referrerSessionId) { - const referrerWindow = Services.ww.getWindowByName( - referrerSessionId, - this.window - ); + const referrerWindow = Services.ww.getWindowByName(referrerSessionId); triggeringPrincipal = referrerWindow.browser.contentPrincipal; csp = referrerWindow.browser.csp; diff --git a/toolkit/components/windowwatcher/nsIWindowWatcher.idl b/toolkit/components/windowwatcher/nsIWindowWatcher.idl index c330e4dc8daa..85a40970d02a 100644 --- a/toolkit/components/windowwatcher/nsIWindowWatcher.idl +++ b/toolkit/components/windowwatcher/nsIWindowWatcher.idl @@ -137,18 +137,17 @@ interface nsIWindowWatcher : nsISupports { nsIWebBrowserChrome getChromeForWindow(in mozIDOMWindowProxy aWindow); /** - Retrieve an existing window (or frame). + Retrieve an existing chrome window (or frame). @param aTargetName the window name - @param aCurrentWindow a starting point in the window hierarchy to - begin the search. If null, each toplevel window - will be searched. + + Note: This method will not consider special names like "_blank", "_top", + "_self", or "_parent", as there is no reference window. Note: This method will search all open windows for any window or frame with the given window name. Make sure you understand the security implications of this before using this method! */ - mozIDOMWindowProxy getWindowByName(in AString aTargetName, - in mozIDOMWindowProxy aCurrentWindow); + mozIDOMWindowProxy getWindowByName(in AString aTargetName); /** Retrieves the active window from the focus manager. diff --git a/toolkit/components/windowwatcher/nsWindowWatcher.cpp b/toolkit/components/windowwatcher/nsWindowWatcher.cpp index 6d6a91821e0d..d21b1fa97755 100644 --- a/toolkit/components/windowwatcher/nsWindowWatcher.cpp +++ b/toolkit/components/windowwatcher/nsWindowWatcher.cpp @@ -71,6 +71,7 @@ #include "mozilla/dom/BrowserParent.h" #include "mozilla/dom/BrowserHost.h" #include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/WindowGlobalChild.h" #include "mozilla/dom/SessionStorageManager.h" #include "nsIAppWindow.h" #include "nsIXULBrowserWindow.h" @@ -696,8 +697,35 @@ nsresult nsWindowWatcher::OpenWindowInternal( return NS_ERROR_ABORT; } + // If no parent, consider it chrome when running in the parent process. + bool hasChromeParent = !XRE_IsContentProcess(); + if (aParent) { + // Check if the parent document has chrome privileges. + hasChromeParent = parentDoc && nsContentUtils::IsChromeDoc(parentDoc); + } + + bool isCallerChrome = nsContentUtils::LegacyIsCallerChromeOrNativeCode(); + // try to find an extant browsing context with the given name - targetBC = GetBrowsingContextByName(name, aForceNoOpener, parentBC); + if (!name.IsEmpty() && + (!aForceNoOpener || nsContentUtils::IsSpecialName(name))) { + if (parentInnerWin && parentInnerWin->GetWindowGlobalChild()) { + // If we have a parent window, perform the look-up relative to the parent + // inner window. + targetBC = + parentInnerWin->GetWindowGlobalChild()->FindBrowsingContextWithName( + name); + } else if (hasChromeParent && isCallerChrome && + !nsContentUtils::IsSpecialName(name)) { + // Otherwise, if this call is from chrome, perform the lookup relative + // to the system group. + nsCOMPtr chromeWindow; + MOZ_ALWAYS_SUCCEEDS(GetWindowByName(name, getter_AddRefs(chromeWindow))); + if (chromeWindow) { + targetBC = nsPIDOMWindowOuter::From(chromeWindow)->GetBrowsingContext(); + } + } + } // Do sandbox checks here, instead of waiting until nsIDocShell::LoadURI. // The state of the window can change before this call and if we are blocked @@ -714,15 +742,6 @@ nsresult nsWindowWatcher::OpenWindowInternal( // no extant window? make a new one. - // If no parent, consider it chrome when running in the parent process. - bool hasChromeParent = !XRE_IsContentProcess(); - if (aParent) { - // Check if the parent document has chrome privileges. - hasChromeParent = parentDoc && nsContentUtils::IsChromeDoc(parentDoc); - } - - bool isCallerChrome = nsContentUtils::LegacyIsCallerChromeOrNativeCode(); - CSSToDesktopScale cssToDesktopScale(1.0); if (nsCOMPtr win = do_QueryInterface(parentDocShell)) { cssToDesktopScale = win->GetUnscaledCSSToDesktopScale(); @@ -1694,7 +1713,6 @@ nsWindowWatcher::GetChromeForWindow(mozIDOMWindowProxy* aWindow, NS_IMETHODIMP nsWindowWatcher::GetWindowByName(const nsAString& aTargetName, - mozIDOMWindowProxy* aCurrentWindow, mozIDOMWindowProxy** aResult) { if (!aResult) { return NS_ERROR_INVALID_ARG; @@ -1702,17 +1720,22 @@ nsWindowWatcher::GetWindowByName(const nsAString& aTargetName, *aResult = nullptr; - BrowsingContext* currentContext = - aCurrentWindow - ? nsPIDOMWindowOuter::From(aCurrentWindow)->GetBrowsingContext() - : nullptr; + // We won't be able to find any windows with a special or empty name. + if (aTargetName.IsEmpty() || nsContentUtils::IsSpecialName(aTargetName)) { + return NS_OK; + } - RefPtr context = - GetBrowsingContextByName(aTargetName, false, currentContext); - - if (context) { - *aResult = do_AddRef(context->GetDOMWindow()).take(); - MOZ_ASSERT(*aResult); + // Search each toplevel in the chrome BrowsingContextGroup for a window with + // the given name. + for (const RefPtr& toplevel : + BrowsingContextGroup::GetChromeGroup()->Toplevels()) { + BrowsingContext* context = + toplevel->FindWithNameInSubtree(aTargetName, nullptr); + if (context) { + *aResult = do_AddRef(context->GetDOMWindow()).take(); + MOZ_ASSERT(*aResult); + return NS_OK; + } } return NS_OK; @@ -2029,36 +2052,6 @@ uint32_t nsWindowWatcher::CalculateChromeFlagsForSystem( return chromeFlags; } -already_AddRefed nsWindowWatcher::GetBrowsingContextByName( - const nsAString& aName, bool aForceNoOpener, - BrowsingContext* aCurrentContext) { - if (aName.IsEmpty()) { - return nullptr; - } - - if (aForceNoOpener && !nsContentUtils::IsSpecialName(aName)) { - // Ignore all other names in the noopener case. - return nullptr; - } - - RefPtr foundContext; - if (aCurrentContext) { - foundContext = aCurrentContext->FindWithName(aName); - } else if (!nsContentUtils::IsSpecialName(aName)) { - // If we are looking for an item and we don't have a docshell we are - // checking on, let's just look in the chrome browsing context group! - for (RefPtr toplevel : - BrowsingContextGroup::GetChromeGroup()->Toplevels()) { - foundContext = toplevel->FindWithNameInSubtree(aName, *toplevel); - if (foundContext) { - break; - } - } - } - - return foundContext.forget(); -} - // public static bool nsWindowWatcher::HaveSpecifiedSize(const WindowFeatures& features) { return CalcSizeSpec(features, false, CSSToDesktopScale()).SizeSpecified(); diff --git a/toolkit/components/windowwatcher/nsWindowWatcher.h b/toolkit/components/windowwatcher/nsWindowWatcher.h index 89ee25241af3..e34865f510be 100644 --- a/toolkit/components/windowwatcher/nsWindowWatcher.h +++ b/toolkit/components/windowwatcher/nsWindowWatcher.h @@ -56,13 +56,6 @@ class nsWindowWatcher : public nsIWindowWatcher, uint32_t aChromeFlags, bool aCalledFromJS, bool aIsForPrinting); - // Will first look for a caller on the JS stack, and then fall back on - // aCurrentContext if it can't find one. - // It also knows to not look for things if aForceNoOpener is set. - already_AddRefed GetBrowsingContextByName( - const nsAString& aName, bool aForceNoOpener, - mozilla::dom::BrowsingContext* aCurrentContext); - static bool HaveSpecifiedSize(const mozilla::dom::WindowFeatures& features); protected: diff --git a/toolkit/mozapps/extensions/AbuseReporter.jsm b/toolkit/mozapps/extensions/AbuseReporter.jsm index 309d654aaf55..843744be2855 100644 --- a/toolkit/mozapps/extensions/AbuseReporter.jsm +++ b/toolkit/mozapps/extensions/AbuseReporter.jsm @@ -381,7 +381,7 @@ const AbuseReporter = { * @returns {Window?} */ getOpenDialog() { - return Services.ww.getWindowByName(DIALOG_WINDOW_NAME, null); + return Services.ww.getWindowByName(DIALOG_WINDOW_NAME); }, /** diff --git a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js index 6ef1786081b2..bb277072b769 100644 --- a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js +++ b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js @@ -188,7 +188,7 @@ const AbuseReportTestUtils = { // Returns the currently open abuse report dialog window (if any). getReportDialog() { - return Services.ww.getWindowByName("addons-abuse-report-dialog", null); + return Services.ww.getWindowByName("addons-abuse-report-dialog"); }, // Returns the parameters related to the report dialog (if any).