/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AntiTrackingUtils.h" #include "AntiTrackingLog.h" #include "mozilla/BasePrincipal.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/CanonicalBrowsingContext.h" #include "mozilla/net/CookieJarSettings.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/WindowGlobalParent.h" #include "mozilla/dom/WindowContext.h" #include "mozilla/net/NeckoChannelParams.h" #include "mozilla/PermissionManager.h" #include "mozIThirdPartyUtil.h" #include "nsGlobalWindowInner.h" #include "nsIChannel.h" #include "nsIHttpChannel.h" #include "nsIPermission.h" #include "nsIURI.h" #include "nsNetUtil.h" #include "nsPIDOMWindow.h" #include "nsSandboxFlags.h" #include "nsScriptSecurityManager.h" #include "PartitioningExceptionList.h" #define ANTITRACKING_PERM_KEY "3rdPartyStorage" using namespace mozilla; using namespace mozilla::dom; /* static */ already_AddRefed AntiTrackingUtils::GetInnerWindow(BrowsingContext* aBrowsingContext) { MOZ_ASSERT(aBrowsingContext); nsCOMPtr outer = aBrowsingContext->GetDOMWindow(); if (!outer) { return nullptr; } nsCOMPtr inner = outer->GetCurrentInnerWindow(); return inner.forget(); } /* static */ already_AddRefed AntiTrackingUtils::GetTopWindow(nsPIDOMWindowInner* aWindow) { Document* document = aWindow->GetExtantDoc(); if (!document) { return nullptr; } nsIChannel* channel = document->GetChannel(); if (!channel) { return nullptr; } nsCOMPtr pwin = aWindow->GetBrowsingContext()->Top()->GetDOMWindow(); if (!pwin) { return nullptr; } return pwin.forget(); } /* static */ already_AddRefed AntiTrackingUtils::MaybeGetDocumentURIBeingLoaded( nsIChannel* aChannel) { nsCOMPtr uriBeingLoaded; nsLoadFlags loadFlags = 0; nsresult rv = aChannel->GetLoadFlags(&loadFlags); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) { // If the channel being loaded is a document channel, this call may be // coming from an OnStopRequest notification, which might mean that our // document may still be in the loading process, so we may need to pass in // the uriBeingLoaded argument explicitly. rv = aChannel->GetURI(getter_AddRefs(uriBeingLoaded)); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } } return uriBeingLoaded.forget(); } // static void AntiTrackingUtils::CreateStoragePermissionKey( const nsACString& aTrackingOrigin, nsACString& aPermissionKey) { MOZ_ASSERT(aPermissionKey.IsEmpty()); static const nsLiteralCString prefix = nsLiteralCString(ANTITRACKING_PERM_KEY "^"); aPermissionKey.SetCapacity(prefix.Length() + aTrackingOrigin.Length()); aPermissionKey.Append(prefix); aPermissionKey.Append(aTrackingOrigin); } // static bool AntiTrackingUtils::CreateStoragePermissionKey(nsIPrincipal* aPrincipal, nsACString& aKey) { if (!aPrincipal) { return false; } nsAutoCString origin; nsresult rv = aPrincipal->GetOriginNoSuffix(origin); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } CreateStoragePermissionKey(origin, aKey); return true; } // static bool AntiTrackingUtils::IsStorageAccessPermission(nsIPermission* aPermission, nsIPrincipal* aPrincipal) { MOZ_ASSERT(aPermission); MOZ_ASSERT(aPrincipal); // The permission key may belong either to a tracking origin on the same // origin as the granted origin, or on another origin as the granted origin // (for example when a tracker in a third-party context uses window.open to // open another origin where that second origin would be the granted origin.) // But even in the second case, the type of the permission would still be // formed by concatenating the granted origin to the end of the type name // (see CreatePermissionKey). Therefore, we pass in the same argument to // both tracking origin and granted origin here in order to compute the // shorter permission key and will then do a prefix match on the type of the // input permission to see if it is a storage access permission or not. nsAutoCString permissionKey; bool result = CreateStoragePermissionKey(aPrincipal, permissionKey); if (NS_WARN_IF(!result)) { return false; } nsAutoCString type; nsresult rv = aPermission->GetType(type); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } return StringBeginsWith(type, permissionKey); } // static bool AntiTrackingUtils::CheckStoragePermission(nsIPrincipal* aPrincipal, const nsAutoCString& aType, bool aIsInPrivateBrowsing, uint32_t* aRejectedReason, uint32_t aBlockedReason) { PermissionManager* permManager = PermissionManager::GetInstance(); if (NS_WARN_IF(!permManager)) { LOG(("Failed to obtain the permission manager")); return false; } uint32_t result = 0; if (aIsInPrivateBrowsing) { LOG_PRIN(("Querying the permissions for private modei looking for a " "permission of type %s for %s", aType.get(), _spec), aPrincipal); if (!permManager->PermissionAvailable(aPrincipal, aType)) { LOG( ("Permission isn't available for this principal in the current " "process")); return false; } nsTArray> permissions; nsresult rv = permManager->GetAllForPrincipal(aPrincipal, permissions); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Failed to get the list of permissions")); return false; } bool found = false; for (const auto& permission : permissions) { if (!permission) { LOG(("Couldn't get the permission for unknown reasons")); continue; } nsAutoCString permissionType; if (NS_SUCCEEDED(permission->GetType(permissionType)) && permissionType != aType) { LOG(("Non-matching permission type: %s", aType.get())); continue; } uint32_t capability = 0; if (NS_SUCCEEDED(permission->GetCapability(&capability)) && capability != nsIPermissionManager::ALLOW_ACTION) { LOG(("Non-matching permission capability: %d", capability)); continue; } uint32_t expirationType = 0; if (NS_SUCCEEDED(permission->GetExpireType(&expirationType)) && expirationType != nsIPermissionManager ::EXPIRE_SESSION) { LOG(("Non-matching permission expiration type: %d", expirationType)); continue; } int64_t expirationTime = 0; if (NS_SUCCEEDED(permission->GetExpireTime(&expirationTime)) && expirationTime != 0) { LOG(("Non-matching permission expiration time: %" PRId64, expirationTime)); continue; } LOG(("Found a matching permission")); found = true; break; } if (!found) { if (aRejectedReason) { *aRejectedReason = aBlockedReason; } return false; } } else { nsresult rv = permManager->TestPermissionWithoutDefaultsFromPrincipal( aPrincipal, aType, &result); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Failed to test the permission")); return false; } LOG_PRIN( ("Testing permission type %s for %s resulted in %d (%s)", aType.get(), _spec, int(result), result == nsIPermissionManager::ALLOW_ACTION ? "success" : "failure"), aPrincipal); if (result != nsIPermissionManager::ALLOW_ACTION) { if (aRejectedReason) { *aRejectedReason = aBlockedReason; } return false; } } return true; } /* static */ bool AntiTrackingUtils::HasStoragePermissionInParent( nsIChannel* aChannel) { MOZ_ASSERT(aChannel); MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); nsCOMPtr loadInfo = aChannel->LoadInfo(); nsCOMPtr cookieJarSettings; auto policyType = loadInfo->GetExternalContentPolicyType(); // The channel is for the document load of the top-level window. The top-level // window should always has 'hasStoragePermission' flag as false. So, we can // return here directly. if (policyType == ExtContentPolicy::TYPE_DOCUMENT) { return false; } nsresult rv = loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } int32_t cookieBehavior = cookieJarSettings->GetCookieBehavior(); // We only need to check the storage permission if the cookie behavior is // BEHAVIOR_REJECT_TRACKER, BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN or // BEHAVIOR_REJECT_FOREIGN with exceptions. Because ContentBlocking wouldn't // update or check the storage permission if the cookie behavior is not // belongs to these three. if (!net::CookieJarSettings::IsRejectThirdPartyContexts(cookieBehavior)) { return false; } RefPtr bc; rv = loadInfo->GetTargetBrowsingContext(getter_AddRefs(bc)); if (NS_WARN_IF(NS_FAILED(rv)) || !bc) { return false; } uint64_t targetWindowId = GetTopLevelAntiTrackingWindowId(bc); nsCOMPtr targetPrincipal; if (targetWindowId) { RefPtr wgp = WindowGlobalParent::GetByInnerWindowId(targetWindowId); if (NS_WARN_IF(!wgp)) { return false; } targetPrincipal = wgp->DocumentPrincipal(); } else { // We try to use the loading principal if there is no AntiTrackingWindowId. targetPrincipal = loadInfo->GetLoadingPrincipal(); } if (!targetPrincipal) { nsCOMPtr httpChannel = do_QueryInterface(aChannel); if (httpChannel) { // We don't have a loading principal, let's see if this is a document // channel which belongs to a top-level window. bool isDocument = false; rv = httpChannel->GetIsMainDocumentChannel(&isDocument); if (NS_SUCCEEDED(rv) && isDocument) { nsIScriptSecurityManager* ssm = nsScriptSecurityManager::GetScriptSecurityManager(); Unused << ssm->GetChannelResultPrincipal( aChannel, getter_AddRefs(targetPrincipal)); } } } // Let's use the triggering principal if we still have nothing on the hand. if (!targetPrincipal) { targetPrincipal = loadInfo->TriggeringPrincipal(); } // Cannot get the target principal, bail out. if (NS_WARN_IF(!targetPrincipal)) { return false; } nsAutoCString targetOrigin; if (NS_FAILED(targetPrincipal->GetAsciiOrigin(targetOrigin))) { return false; } nsCOMPtr trackingURI; rv = aChannel->GetURI(getter_AddRefs(trackingURI)); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } nsAutoCString trackingOrigin; rv = nsContentUtils::GetASCIIOrigin(trackingURI, trackingOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } nsAutoCString type; AntiTrackingUtils::CreateStoragePermissionKey(trackingOrigin, type); uint32_t unusedReason = 0; if (PartitioningExceptionList::Check(targetOrigin, trackingOrigin)) { return true; } return AntiTrackingUtils::CheckStoragePermission( targetPrincipal, type, NS_UsePrivateBrowsing(aChannel), &unusedReason, unusedReason); } uint64_t AntiTrackingUtils::GetTopLevelAntiTrackingWindowId( BrowsingContext* aBrowsingContext) { MOZ_ASSERT(aBrowsingContext); RefPtr winContext = aBrowsingContext->GetCurrentWindowContext(); if (!winContext || winContext->GetCookieBehavior().isNothing()) { return 0; } // Do not check BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN her because when // a third-party subresource is inside the main frame, we need to return the // top-level window id to partition its cookies correctly. uint32_t behavior = *winContext->GetCookieBehavior(); if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER && aBrowsingContext->IsTop()) { return 0; } return aBrowsingContext->Top()->GetCurrentInnerWindowId(); } uint64_t AntiTrackingUtils::GetTopLevelStorageAreaWindowId( BrowsingContext* aBrowsingContext) { MOZ_ASSERT(aBrowsingContext); if (Document::StorageAccessSandboxed(aBrowsingContext->GetSandboxFlags())) { return 0; } BrowsingContext* parentBC = aBrowsingContext->GetParent(); if (!parentBC) { // No parent browsing context available! return 0; } if (!parentBC->IsTop()) { return 0; } return parentBC->GetCurrentInnerWindowId(); } /* static */ already_AddRefed AntiTrackingUtils::GetPrincipal( BrowsingContext* aBrowsingContext) { MOZ_ASSERT(aBrowsingContext); nsCOMPtr principal; if (XRE_IsContentProcess()) { // Passing an out-of-process browsing context in child processes to // this API won't get any result, so just assert. MOZ_ASSERT(aBrowsingContext->IsInProcess()); nsPIDOMWindowOuter* outer = aBrowsingContext->GetDOMWindow(); if (NS_WARN_IF(!outer)) { return nullptr; } nsPIDOMWindowInner* inner = outer->GetCurrentInnerWindow(); if (NS_WARN_IF(!inner)) { return nullptr; } principal = nsGlobalWindowInner::Cast(inner)->GetPrincipal(); } else { WindowGlobalParent* wgp = aBrowsingContext->Canonical()->GetCurrentWindowGlobal(); if (NS_WARN_IF(!wgp)) { return nullptr; } principal = wgp->DocumentPrincipal(); } return principal.forget(); } /* static */ bool AntiTrackingUtils::GetPrincipalAndTrackingOrigin( BrowsingContext* aBrowsingContext, nsIPrincipal** aPrincipal, nsACString& aTrackingOrigin) { MOZ_ASSERT(aBrowsingContext); // Passing an out-of-process browsing context in child processes to // this API won't get any result, so just assert. MOZ_ASSERT_IF(XRE_IsContentProcess(), aBrowsingContext->IsInProcess()); // Let's take the principal and the origin of the tracker. nsCOMPtr principal = AntiTrackingUtils::GetPrincipal(aBrowsingContext); if (NS_WARN_IF(!principal)) { return false; } nsresult rv = principal->GetOriginNoSuffix(aTrackingOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } if (aPrincipal) { principal.forget(aPrincipal); } return true; }; /* static */ uint32_t AntiTrackingUtils::GetCookieBehavior( BrowsingContext* aBrowsingContext) { MOZ_ASSERT(aBrowsingContext); RefPtr win = aBrowsingContext->GetCurrentWindowContext(); if (!win || win->GetCookieBehavior().isNothing()) { return nsICookieService::BEHAVIOR_REJECT; } return *win->GetCookieBehavior(); } /* static */ already_AddRefed AntiTrackingUtils::GetTopWindowExcludingExtensionAccessibleContentFrames( CanonicalBrowsingContext* aBrowsingContext, nsIURI* aURIBeingLoaded) { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(aBrowsingContext); CanonicalBrowsingContext* bc = aBrowsingContext; RefPtr prev; while (RefPtr parent = bc->GetParentWindowContext()) { CanonicalBrowsingContext* parentBC = parent->BrowsingContext(); nsIPrincipal* parentPrincipal = parent->DocumentPrincipal(); nsIURI* uri = prev ? prev->GetDocumentURI() : aURIBeingLoaded; // If the new parent has permission to load the current page, we're // at a moz-extension:// frame which has a host permission that allows // it to load the document that we've loaded. In that case, stop at // this frame and consider it the top-level frame. if (uri && BasePrincipal::Cast(parentPrincipal)->AddonAllowsLoad(uri, true)) { break; } bc = parentBC; prev = parent; } if (!prev) { prev = bc->GetCurrentWindowGlobal(); } return prev.forget(); } /* static */ void AntiTrackingUtils::ComputeIsThirdPartyToTopWindow(nsIChannel* aChannel) { MOZ_ASSERT(aChannel); MOZ_ASSERT(XRE_IsParentProcess()); nsCOMPtr loadInfo = aChannel->LoadInfo(); // When a top-level load is opened by window.open, BrowsingContext from // LoadInfo is its opener, which may make the third-party caculation code // below returns an incorrect result. So we use TYPE_DOCUMENT to // ensure a top-level load is not considered 3rd-party. auto policyType = loadInfo->GetExternalContentPolicyType(); if (policyType == ExtContentPolicy::TYPE_DOCUMENT) { loadInfo->SetIsThirdPartyContextToTopWindow(false); return; } RefPtr bc; loadInfo->GetBrowsingContext(getter_AddRefs(bc)); nsCOMPtr uri; Unused << aChannel->GetURI(getter_AddRefs(uri)); // In some cases we don't have a browsingContext. For example, in xpcshell // tests or channels that are used to download images. if (!bc) { // We turn to check the loading principal if there is no browsing context. auto* loadingPrincipal = BasePrincipal::Cast(loadInfo->GetLoadingPrincipal()); if (uri && loadingPrincipal) { bool isThirdParty = true; nsresult rv = loadingPrincipal->IsThirdPartyURI(uri, &isThirdParty); if (NS_SUCCEEDED(rv)) { loadInfo->SetIsThirdPartyContextToTopWindow(isThirdParty); } } return; } RefPtr topWindow = GetTopWindowExcludingExtensionAccessibleContentFrames(bc->Canonical(), uri); if (NS_WARN_IF(!topWindow)) { return; } nsCOMPtr topWindowPrincipal = topWindow->DocumentPrincipal(); if (topWindowPrincipal && !topWindowPrincipal->GetIsNullPrincipal()) { auto* basePrin = BasePrincipal::Cast(topWindowPrincipal); bool isThirdParty = true; // For about:blank, we can't just compare uri to determine whether the page // is third-party, so we use channel result principal instead. By doing // this, an about:blank inherits the principal from its parent is considered // not a third-party. if (NS_IsAboutBlank(uri)) { nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); if (NS_WARN_IF(!ssm)) { return; } nsCOMPtr principal; nsresult rv = ssm->GetChannelResultPrincipal(aChannel, getter_AddRefs(principal)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } basePrin->IsThirdPartyPrincipal(principal, &isThirdParty); } else { basePrin->IsThirdPartyURI(uri, &isThirdParty); } loadInfo->SetIsThirdPartyContextToTopWindow(isThirdParty); } } /* static */ bool AntiTrackingUtils::IsThirdPartyChannel(nsIChannel* aChannel) { MOZ_ASSERT(aChannel); // We only care whether the channel is 3rd-party with respect to // the top-level. nsCOMPtr loadInfo = aChannel->LoadInfo(); return loadInfo->GetIsThirdPartyContextToTopWindow(); } /* static */ bool AntiTrackingUtils::IsThirdPartyWindow(nsPIDOMWindowInner* aWindow, nsIURI* aURI) { MOZ_ASSERT(aWindow); MOZ_ASSERT(aURI); // We assume that the window is foreign to the URI by default. bool thirdParty = true; // This is to comply with ThirdPartyUtil::IsThirdPartyWindow API. if (aURI && !NS_IsAboutBlank(aURI)) { nsCOMPtr scriptObjPrin = do_QueryInterface(aWindow); if (!scriptObjPrin) { return thirdParty; } nsCOMPtr prin = scriptObjPrin->GetPrincipal(); if (!prin) { return thirdParty; } // Determine whether aURI is foreign with respect to the current principal. nsresult rv = prin->IsThirdPartyURI(aURI, &thirdParty); if (NS_FAILED(rv)) { return thirdParty; } if (thirdParty) { return thirdParty; } } RefPtr doc = aWindow->GetDoc(); if (!doc || !doc->GetChannel()) { // If we can't get channel from the window, ex, about:blank, fallback to use // IsThirdPartyWindow check that examine the whole hierarchy. nsCOMPtr thirdPartyUtil = services::GetThirdPartyUtil(); Unused << thirdPartyUtil->IsThirdPartyWindow(aWindow->GetOuterWindow(), nullptr, &thirdParty); return thirdParty; } // We only care whether the channel is 3rd-party with respect to // the top-level. nsCOMPtr loadInfo = doc->GetChannel()->LoadInfo(); return loadInfo->GetIsThirdPartyContextToTopWindow(); } /* static */ nsCString AntiTrackingUtils::GrantedReasonToString( ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason) { switch (aReason) { case ContentBlockingNotifier::eOpener: return "opener"_ns; case ContentBlockingNotifier::eOpenerAfterUserInteraction: return "user interaction"_ns; default: return "stroage access API"_ns; } } /* static */ void AntiTrackingUtils::UpdateAntiTrackingInfoForChannel(nsIChannel* aChannel) { MOZ_ASSERT(aChannel); if (!XRE_IsParentProcess()) { return; } MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); nsCOMPtr loadInfo = aChannel->LoadInfo(); Unused << loadInfo->SetHasStoragePermission( AntiTrackingUtils::HasStoragePermissionInParent(aChannel)); AntiTrackingUtils::ComputeIsThirdPartyToTopWindow(aChannel); // We only update the IsOnContentBlockingAllowList flag and the partition key // for the top-level http channel. // // The IsOnContentBlockingAllowList is only for http. For other types of // channels, such as 'file:', there will be no interface to modify this. So, // we only update it in http channels. // // The partition key is computed based on the site, so it's no point to set it // for channels other than http channels. nsCOMPtr httpChannel = do_QueryInterface(aChannel); if (!httpChannel || loadInfo->GetExternalContentPolicyType() != ExtContentPolicy::TYPE_DOCUMENT) { return; } nsCOMPtr cookieJarSettings; Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); // Update the IsOnContentBlockingAllowList flag in the CookieJarSettings // if this is a top level loading. For sub-document loading, this flag // would inherit from the parent. net::CookieJarSettings::Cast(cookieJarSettings) ->UpdateIsOnContentBlockingAllowList(aChannel); // We only need to set FPD for top-level loads. FPD will automatically be // propagated to non-top level loads via CookieJarSetting. nsCOMPtr uri; Unused << aChannel->GetURI(getter_AddRefs(uri)); net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri); }