/* -*- 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 "AntiTrackingLog.h" #include "ContentBlocking.h" #include "AntiTrackingUtils.h" #include "TemporaryAccessGrantObserver.h" #include "mozilla/ContentBlockingAllowList.h" #include "mozilla/ContentBlockingUserInteraction.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/BrowsingContextGroup.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/WindowContext.h" #include "mozilla/dom/WindowGlobalParent.h" #include "mozilla/net/CookieJarSettings.h" #include "mozilla/PermissionManager.h" #include "mozilla/StaticPrefs_privacy.h" #include "mozilla/Telemetry.h" #include "mozIThirdPartyUtil.h" #include "nsContentUtils.h" #include "nsGlobalWindowInner.h" #include "nsIClassifiedChannel.h" #include "nsICookiePermission.h" #include "nsICookieService.h" #include "nsIPermission.h" #include "nsIPrincipal.h" #include "nsIURI.h" #include "nsIOService.h" #include "nsIWebProgressListener.h" #include "nsScriptSecurityManager.h" #include "RejectForeignAllowList.h" namespace mozilla { LazyLogModule gAntiTrackingLog("AntiTracking"); } using namespace mozilla; using mozilla::dom::BrowsingContext; using mozilla::dom::ContentChild; using mozilla::dom::Document; using mozilla::dom::WindowGlobalParent; using mozilla::net::CookieJarSettings; namespace { bool GetTopLevelWindowId(BrowsingContext* aParentContext, uint32_t aBehavior, uint64_t& aTopLevelInnerWindowId) { MOZ_ASSERT(aParentContext); aTopLevelInnerWindowId = (aBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER) ? AntiTrackingUtils::GetTopLevelStorageAreaWindowId(aParentContext) : AntiTrackingUtils::GetTopLevelAntiTrackingWindowId(aParentContext); return aTopLevelInnerWindowId != 0; } // This internal method returns ACCESS_DENY if the access is denied, // ACCESS_DEFAULT if unknown, some other access code if granted. uint32_t CheckCookiePermissionForPrincipal( nsICookieJarSettings* aCookieJarSettings, nsIPrincipal* aPrincipal) { MOZ_ASSERT(aCookieJarSettings); MOZ_ASSERT(aPrincipal); uint32_t cookiePermission = nsICookiePermission::ACCESS_DEFAULT; if (!aPrincipal->GetIsContentPrincipal()) { return cookiePermission; } nsresult rv = aCookieJarSettings->CookiePermission(aPrincipal, &cookiePermission); if (NS_WARN_IF(NS_FAILED(rv))) { return nsICookiePermission::ACCESS_DEFAULT; } // If we have a custom cookie permission, let's use it. return cookiePermission; } int32_t CookiesBehavior(Document* a3rdPartyDocument) { MOZ_ASSERT(a3rdPartyDocument); // WebExtensions principals always get BEHAVIOR_ACCEPT as cookieBehavior // (See Bug 1406675 and Bug 1525917 for rationale). if (BasePrincipal::Cast(a3rdPartyDocument->NodePrincipal())->AddonPolicy()) { return nsICookieService::BEHAVIOR_ACCEPT; } return a3rdPartyDocument->CookieJarSettings()->GetCookieBehavior(); } int32_t CookiesBehavior(nsILoadInfo* aLoadInfo, nsIURI* a3rdPartyURI) { MOZ_ASSERT(aLoadInfo); MOZ_ASSERT(a3rdPartyURI); // WebExtensions 3rd party URI always get BEHAVIOR_ACCEPT as cookieBehavior, // this is semantically equivalent to the principal having a AddonPolicy(). if (a3rdPartyURI->SchemeIs("moz-extension")) { return nsICookieService::BEHAVIOR_ACCEPT; } nsCOMPtr cookieJarSettings; nsresult rv = aLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); if (NS_WARN_IF(NS_FAILED(rv))) { return nsICookieService::BEHAVIOR_REJECT; } return cookieJarSettings->GetCookieBehavior(); } int32_t CookiesBehavior(nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aCookieJarSettings); // WebExtensions principals always get BEHAVIOR_ACCEPT as cookieBehavior // (See Bug 1406675 for rationale). if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) { return nsICookieService::BEHAVIOR_ACCEPT; } return aCookieJarSettings->GetCookieBehavior(); } } // namespace /* static */ RefPtr ContentBlocking::AllowAccessFor( nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext, ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, const ContentBlocking::PerformFinalChecks& aPerformFinalChecks) { MOZ_ASSERT(aParentContext); switch (aReason) { case ContentBlockingNotifier::eOpener: if (!StaticPrefs:: privacy_restrict3rdpartystorage_heuristic_window_open()) { LOG( ("Bailing out early because the " "privacy.restrict3rdpartystorage.heuristic.window_open preference " "has been disabled")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } break; case ContentBlockingNotifier::eOpenerAfterUserInteraction: if (!StaticPrefs:: privacy_restrict3rdpartystorage_heuristic_opened_window_after_interaction()) { LOG( ("Bailing out early because the " "privacy.restrict3rdpartystorage.heuristic.opened_window_after_" "interaction preference has been disabled")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } break; default: break; } if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) { nsAutoCString origin; aPrincipal->GetAsciiOrigin(origin); LOG(("Adding a first-party storage exception for %s, triggered by %s", PromiseFlatCString(origin).get(), AntiTrackingUtils::GrantedReasonToString(aReason).get())); } RefPtr parentWindowContext = aParentContext->GetCurrentWindowContext(); if (!parentWindowContext) { LOG( ("No window context found for our parent browsing context, bailing out " "early")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } if (parentWindowContext->GetCookieBehavior().isNothing()) { LOG( ("No cookie behaviour found for our parent window context, bailing " "out early")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } // Only add storage permission when there is a reason to do so. uint32_t behavior = *parentWindowContext->GetCookieBehavior(); if (!CookieJarSettings::IsRejectThirdPartyContexts(behavior)) { LOG( ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out " "early", behavior)); return StorageAccessPermissionGrantPromise::CreateAndResolve(true, __func__); } MOZ_ASSERT( CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) || behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); // No need to continue when we are already in the allow list. if (parentWindowContext->GetIsOnContentBlockingAllowList()) { return StorageAccessPermissionGrantPromise::CreateAndResolve(true, __func__); } // Make sure storage access isn't disabled if (!aParentContext->IsTopContent() && Document::StorageAccessSandboxed(aParentContext->GetSandboxFlags())) { LOG(("Our document is sandboxed")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } bool isParentThirdParty = parentWindowContext->GetIsThirdPartyWindow(); uint64_t topLevelWindowId; nsAutoCString trackingOrigin; nsCOMPtr trackingPrincipal; LOG(("The current resource is %s-party", isParentThirdParty ? "third" : "first")); // We are a first party resource. if (!isParentThirdParty) { nsAutoCString origin; nsresult rv = aPrincipal->GetAsciiOrigin(origin); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't get the origin from the URI")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } trackingOrigin = origin; trackingPrincipal = aPrincipal; topLevelWindowId = aParentContext->GetCurrentInnerWindowId(); if (NS_WARN_IF(!topLevelWindowId)) { LOG(("Top-level storage area window id not found, bailing out early")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } } else { // We should be a 3rd party source. if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER && !parentWindowContext->GetIsThirdPartyTrackingResourceWindow()) { LOG(("Our window isn't a third-party tracking window")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } if ((CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) || behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) && !isParentThirdParty) { LOG(("Our window isn't a third-party window")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } if (!GetTopLevelWindowId(aParentContext, // Don't request the ETP specific behaviour of // allowing only singly-nested iframes here, // because we are recording an allow permission. nsICookieService::BEHAVIOR_ACCEPT, topLevelWindowId)) { LOG(("Error while retrieving the parent window id, bailing out early")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } // If we can't get the principal and tracking origin at this point, the // tracking principal will be gotten while running ::CompleteAllowAccessFor // in the parent. if (aParentContext->IsInProcess()) { if (!AntiTrackingUtils::GetPrincipalAndTrackingOrigin( aParentContext, getter_AddRefs(trackingPrincipal), trackingOrigin)) { LOG( ("Error while computing the parent principal and tracking origin, " "bailing out early")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } } } // We MAY need information that is only accessible in the parent, // so we need to determine whether we can run it in the current process (in // most of cases it should be a child process). // // If the following two cases are both true, we can continue to run in // the current process, otherwise, we need to ask the parent to continue // the work. // 1. aParentContext is an in-process browsing contex because we need the // principal of the parent window. // 2. tracking origin is not third-party with respect to the parent window // (aParentContext). This is because we need to test whether the user // has interacted with the tracking origin before, and this info is // not supposed to be seen from cross-origin processes. // The only case that aParentContext is not in-process is when the heuristic // is triggered because of user interactions. MOZ_ASSERT_IF( !aParentContext->IsInProcess(), aReason == ContentBlockingNotifier::eOpenerAfterUserInteraction); bool runInSameProcess; if (XRE_IsParentProcess()) { // If we are already in the parent, then continue run in the parent. runInSameProcess = true; } else { // Only continue to run in child processes when aParentContext is // in-process and tracking origin is not third-party with respect to // the parent window. if (aParentContext->IsInProcess()) { bool isThirdParty; nsCOMPtr principal = AntiTrackingUtils::GetPrincipal(aParentContext); if (!principal) { LOG(("Can't get the principal from the browsing context")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } Unused << trackingPrincipal->IsThirdPartyPrincipal(principal, &isThirdParty); runInSameProcess = !isThirdParty; } else { runInSameProcess = false; } } if (runInSameProcess) { return ContentBlocking::CompleteAllowAccessFor( aParentContext, topLevelWindowId, trackingPrincipal, trackingOrigin, behavior, aReason, aPerformFinalChecks); } MOZ_ASSERT(XRE_IsContentProcess()); // Only support PerformFinalChecks when we run ::CompleteAllowAccessFor in // the same process. This callback is only used by eStorageAccessAPI, // which is always runned in the same process. MOZ_ASSERT(!aPerformFinalChecks); ContentChild* cc = ContentChild::GetSingleton(); MOZ_ASSERT(cc); RefPtr bc = aParentContext; return cc ->SendCompleteAllowAccessFor(aParentContext, topLevelWindowId, IPC::Principal(trackingPrincipal), trackingOrigin, behavior, aReason) ->Then(GetCurrentSerialEventTarget(), __func__, [bc, trackingOrigin, behavior, aReason](const ContentChild::CompleteAllowAccessForPromise:: ResolveOrRejectValue& aValue) { if (aValue.IsResolve() && aValue.ResolveValue().isSome()) { // we don't call OnAllowAccessFor in the parent when this is // triggered by the opener heuristic, so we have to do it here. // See storePermission below for the reason. if (aReason == ContentBlockingNotifier::eOpener && !bc->IsDiscarded()) { MOZ_ASSERT(bc->IsInProcess()); ContentBlocking::OnAllowAccessFor(bc, trackingOrigin, behavior, aReason); } return StorageAccessPermissionGrantPromise::CreateAndResolve( aValue.ResolveValue().value(), __func__); } return StorageAccessPermissionGrantPromise::CreateAndReject( false, __func__); }); } // CompleteAllowAccessFor is used to process the remaining work in // AllowAccessFor that may need to access information not accessible // in the current process. // This API supports running running in the child process and the // parent process. When running in the child, aParentContext must be in-process. // // Here lists the possible cases based on our heuristics: // 1. eStorageAccessAPI // aParentContext is the browsing context of the document that calls this // API, so it is always in-process. Since the tracking origin is the // document's origin, it's same-origin to the parent window. // CompleteAllowAccessFor runs in the same process as AllowAccessFor. // // 2. eOpener // aParentContext is the browsing context of the opener that calls this // API, so it is always in-process. However, when the opener is a first // party and it opens a third-party window, the tracking origin is // origin of the third-party window. In this case, we should // run this API in the parent, as for the other cases, we can run in the // same process. // // 3. eOpenerAfterUserInteraction // aParentContext is the browsing context of the opener window, but // AllowAccessFor is called by the opened window. So as long as // aParentContext is not in-process, we should run in the parent. /* static */ RefPtr ContentBlocking::CompleteAllowAccessFor( dom::BrowsingContext* aParentContext, uint64_t aTopLevelWindowId, nsIPrincipal* aTrackingPrincipal, const nsCString& aTrackingOrigin, uint32_t aCookieBehavior, ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, const PerformFinalChecks& aPerformFinalChecks) { MOZ_ASSERT(aParentContext); MOZ_ASSERT_IF(XRE_IsContentProcess(), aParentContext->IsInProcess()); nsCOMPtr trackingPrincipal; nsAutoCString trackingOrigin; if (!aTrackingPrincipal) { // User interaction is the only case that tracking principal is not // available. MOZ_ASSERT(XRE_IsParentProcess() && aReason == ContentBlockingNotifier::eOpenerAfterUserInteraction); if (!AntiTrackingUtils::GetPrincipalAndTrackingOrigin( aParentContext, getter_AddRefs(trackingPrincipal), trackingOrigin)) { LOG( ("Error while computing the parent principal and tracking origin, " "bailing out early")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } } else { trackingPrincipal = aTrackingPrincipal; trackingOrigin = aTrackingOrigin; } LOG(("Tracking origin is %s", PromiseFlatCString(trackingOrigin).get())); // We hardcode this block reason since the first-party storage access // permission is granted for the purpose of blocking trackers. // Note that if aReason is eOpenerAfterUserInteraction and the // trackingPrincipal is not in a blocklist, we don't check the // user-interaction state, because it could be that the current process has // just sent the request to store the user-interaction permission into the // parent, without having received the permission itself yet. bool isInPrefList = false; trackingPrincipal->IsURIInPrefList( "privacy.restrict3rdpartystorage." "userInteractionRequiredForHosts", &isInPrefList); if (isInPrefList && !ContentBlockingUserInteraction::Exists(trackingPrincipal)) { LOG_PRIN(("Tracking principal (%s) hasn't been interacted with before, " "refusing to add a first-party storage permission to access it", _spec), trackingPrincipal); ContentBlockingNotifier::OnDecision( aParentContext, ContentBlockingNotifier::BlockingDecision::eBlock, CookieJarSettings::IsRejectThirdPartyWithExceptions(aCookieBehavior) ? nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN : nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } // Ensure we can find the window before continuing, so we can safely // execute storePermission. if (aParentContext->IsInProcess() && (!aParentContext->GetDOMWindow() || !aParentContext->GetDOMWindow()->GetCurrentInnerWindow())) { LOG( ("No window found for our parent browsing context, bailing out " "early")); return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); } auto storePermission = [aParentContext, aTopLevelWindowId, trackingOrigin, trackingPrincipal, aCookieBehavior, aReason](int aAllowMode) -> RefPtr { // Inform the window we granted permission for. This has to be done in the // window's process. if (aParentContext->IsInProcess()) { ContentBlocking::OnAllowAccessFor(aParentContext, trackingOrigin, aCookieBehavior, aReason); } else { MOZ_ASSERT(XRE_IsParentProcess()); // We don't have the window, send an IPC to the content process that // owns the parent window. But there is a special case, for window.open, // we'll return to the content process we need to inform when this // function is done. So we don't need to create an extra IPC for the case. if (aReason != ContentBlockingNotifier::eOpener) { ContentParent* cp = aParentContext->Canonical()->GetContentParent(); Unused << cp->SendOnAllowAccessFor(aParentContext, trackingOrigin, aCookieBehavior, aReason); } } Maybe reportReason; // We can directly report here if we can know the origin of the top. if (XRE_IsParentProcess() || aParentContext->Top()->IsInProcess()) { ContentBlockingNotifier::ReportUnblockingToConsole( aParentContext, NS_ConvertUTF8toUTF16(trackingOrigin), aReason); // Set the report reason to nothing if we've already reported. reportReason = Nothing(); } else { // Set the report reason, so that we can know the reason when reporting // in the parent. reportReason.emplace(aReason); } if (XRE_IsParentProcess()) { LOG(("Saving the permission: trackingOrigin=%s", trackingOrigin.get())); return SaveAccessForOriginOnParentProcess( aTopLevelWindowId, aParentContext, trackingPrincipal, trackingOrigin, aAllowMode) ->Then( GetCurrentSerialEventTarget(), __func__, [](ParentAccessGrantPromise::ResolveOrRejectValue&& aValue) { if (aValue.IsResolve()) { return StorageAccessPermissionGrantPromise::CreateAndResolve( ContentBlocking::eAllow, __func__); } return StorageAccessPermissionGrantPromise::CreateAndReject( false, __func__); }); } ContentChild* cc = ContentChild::GetSingleton(); MOZ_ASSERT(cc); LOG( ("Asking the parent process to save the permission for us: " "trackingOrigin=%s", trackingOrigin.get())); // This is not really secure, because here we have the content process // sending the request of storing a permission. return cc ->SendStorageAccessPermissionGrantedForOrigin( aTopLevelWindowId, aParentContext, IPC::Principal(trackingPrincipal), trackingOrigin, aAllowMode, reportReason) ->Then(GetCurrentSerialEventTarget(), __func__, [](const ContentChild:: StorageAccessPermissionGrantedForOriginPromise:: ResolveOrRejectValue& aValue) { if (aValue.IsResolve()) { return StorageAccessPermissionGrantPromise::CreateAndResolve( aValue.ResolveValue(), __func__); } return StorageAccessPermissionGrantPromise::CreateAndReject( false, __func__); }); }; if (aPerformFinalChecks) { return aPerformFinalChecks()->Then( GetCurrentSerialEventTarget(), __func__, [storePermission]( StorageAccessPermissionGrantPromise::ResolveOrRejectValue&& aValue) { if (aValue.IsResolve()) { return storePermission(aValue.ResolveValue()); } return StorageAccessPermissionGrantPromise::CreateAndReject(false, __func__); }); } return storePermission(false); } /* static */ void ContentBlocking::OnAllowAccessFor( dom::BrowsingContext* aParentContext, const nsCString& aTrackingOrigin, uint32_t aCookieBehavior, ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason) { MOZ_ASSERT(aParentContext->IsInProcess()); // Let's inform the parent window and the other windows having the // same tracking origin about the stroage permission is granted. ContentBlocking::UpdateAllowAccessOnCurrentProcess(aParentContext, aTrackingOrigin); // Let's inform the parent window. nsCOMPtr parentInner = AntiTrackingUtils::GetInnerWindow(aParentContext); if (NS_WARN_IF(!parentInner)) { return; } Document* doc = parentInner->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return; } if (!doc->GetChannel()) { return; } Telemetry::AccumulateCategorical( Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageGranted); switch (aReason) { case ContentBlockingNotifier::StorageAccessPermissionGrantedReason:: eStorageAccessAPI: Telemetry::AccumulateCategorical( Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageAccessAPI); break; case ContentBlockingNotifier::StorageAccessPermissionGrantedReason:: eOpenerAfterUserInteraction: Telemetry::AccumulateCategorical( Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::OpenerAfterUI); break; case ContentBlockingNotifier::StorageAccessPermissionGrantedReason::eOpener: Telemetry::AccumulateCategorical( Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::Opener); break; default: break; } // Theoratically this can be done in the parent process. But right now, // we need the channel while notifying content blocking events, and // we don't have a trivial way to obtain the channel in the parent // via BrowsingContext. So we just ask the child to do the work. ContentBlockingNotifier::OnEvent( doc->GetChannel(), false, CookieJarSettings::IsRejectThirdPartyWithExceptions(aCookieBehavior) ? nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN : nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER, aTrackingOrigin, Some(aReason)); } /* static */ RefPtr ContentBlocking::SaveAccessForOriginOnParentProcess( uint64_t aTopLevelWindowId, BrowsingContext* aParentContext, nsIPrincipal* aTrackingPrincipal, const nsCString& aTrackingOrigin, int aAllowMode, uint64_t aExpirationTime) { MOZ_ASSERT(aTopLevelWindowId != 0); RefPtr wgp = WindowGlobalParent::GetByInnerWindowId(aTopLevelWindowId); if (!wgp) { LOG(("Can't get window global parent")); return ParentAccessGrantPromise::CreateAndReject(false, __func__); } // If the permission is granted on a first-party window, also have to update // the permission to all the other windows with the same tracking origin (in // the same tab), if any. ContentBlocking::UpdateAllowAccessOnParentProcess(aParentContext, aTrackingOrigin); return ContentBlocking::SaveAccessForOriginOnParentProcess( wgp->DocumentPrincipal(), aTrackingPrincipal, aTrackingOrigin, aAllowMode, aExpirationTime); } /* static */ RefPtr ContentBlocking::SaveAccessForOriginOnParentProcess( nsIPrincipal* aParentPrincipal, nsIPrincipal* aTrackingPrincipal, const nsCString& aTrackingOrigin, int aAllowMode, uint64_t aExpirationTime) { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(aAllowMode == eAllow || aAllowMode == eAllowAutoGrant); if (!aParentPrincipal || !aTrackingPrincipal) { LOG(("Invalid input arguments passed")); return ParentAccessGrantPromise::CreateAndReject(false, __func__); }; LOG_PRIN(("Saving a first-party storage permission on %s for " "trackingOrigin=%s", _spec, aTrackingOrigin.get()), aParentPrincipal); if (NS_WARN_IF(!aParentPrincipal)) { // The child process is sending something wrong. Let's ignore it. LOG(("aParentPrincipal is null, bailing out early")); return ParentAccessGrantPromise::CreateAndReject(false, __func__); } PermissionManager* permManager = PermissionManager::GetInstance(); if (NS_WARN_IF(!permManager)) { LOG(("Permission manager is null, bailing out early")); return ParentAccessGrantPromise::CreateAndReject(false, __func__); } // Remember that this pref is stored in seconds! uint32_t expirationType = nsIPermissionManager::EXPIRE_TIME; uint32_t expirationTime = aExpirationTime * 1000; int64_t when = (PR_Now() / PR_USEC_PER_MSEC) + expirationTime; uint32_t privateBrowsingId = 0; nsresult rv = aParentPrincipal->GetPrivateBrowsingId(&privateBrowsingId); if ((!NS_WARN_IF(NS_FAILED(rv)) && privateBrowsingId > 0) || (aAllowMode == eAllowAutoGrant)) { // If we are coming from a private window or are automatically granting a // permission, make sure to store a session-only permission which won't // get persisted to disk. expirationType = nsIPermissionManager::EXPIRE_SESSION; when = 0; } nsAutoCString type; AntiTrackingUtils::CreateStoragePermissionKey(aTrackingOrigin, type); LOG( ("Computed permission key: %s, expiry: %u, proceeding to save in the " "permission manager", type.get(), expirationTime)); rv = permManager->AddFromPrincipal(aParentPrincipal, type, nsIPermissionManager::ALLOW_ACTION, expirationType, when); Unused << NS_WARN_IF(NS_FAILED(rv)); if (StaticPrefs::privacy_antitracking_testing()) { nsCOMPtr obs = services::GetObserverService(); obs->NotifyObservers(nullptr, "antitracking-test-storage-access-perm-added", nullptr); } if (NS_SUCCEEDED(rv) && (aAllowMode == eAllowAutoGrant)) { // Make sure temporary access grants do not survive more than 24 hours. TemporaryAccessGrantObserver::Create(permManager, aParentPrincipal, type); } LOG(("Result: %s", NS_SUCCEEDED(rv) ? "success" : "failure")); return ParentAccessGrantPromise::CreateAndResolve(rv, __func__); } // There are two methods to handle permisson update: // 1. UpdateAllowAccessOnCurrentProcess // 2. UpdateAllowAccessOnParentProcess // // In general, UpdateAllowAccessOnCurrentProcess is used to propagate storage // permission to same-origin frames in the same tab. // UpdateAllowAccessOnParentProcess is used to propagate storage permission to // same-origin frames in the same agent cluster. // // However, there is an exception in fission mode. When the heuristic is // triggered by a first-party window, for instance, a first-party script calls // window.open(tracker), we can't update 3rd-party frames's storage permission // in the child process that triggers the permission update because the // first-party and the 3rd-party are not in the same process. In this case, we // should update the storage permission in UpdateAllowAccessOnParentProcess. // This function is used to update permission to all in-process windows, so it // can be called either from the parent or the child. /* static */ void ContentBlocking::UpdateAllowAccessOnCurrentProcess( BrowsingContext* aParentContext, const nsACString& aTrackingOrigin) { MOZ_ASSERT(aParentContext && aParentContext->IsInProcess()); bool useRemoteSubframes; aParentContext->GetUseRemoteSubframes(&useRemoteSubframes); if (useRemoteSubframes && aParentContext->IsTopContent()) { // If we are a first-party and we are in fission mode, bail out early // because we can't do anything here. return; } BrowsingContext* top = aParentContext->Top(); // Propagate the storage permission to same-origin frames in the same tab. top->PreOrderWalk([&](BrowsingContext* aContext) { // Only check browsing contexts that are in-process. if (aContext->IsInProcess()) { nsAutoCString origin; Unused << AntiTrackingUtils::GetPrincipalAndTrackingOrigin( aContext, nullptr, origin); if (aTrackingOrigin == origin) { nsCOMPtr inner = AntiTrackingUtils::GetInnerWindow(aContext); if (inner) { inner->SaveStorageAccessPermissionGranted(); } nsCOMPtr outer = nsPIDOMWindowOuter::GetFromCurrentInner(inner); if (outer) { nsGlobalWindowOuter::Cast(outer)->SetStorageAccessPermissionGranted( true); } } } }); } /* static */ void ContentBlocking::UpdateAllowAccessOnParentProcess( BrowsingContext* aParentContext, const nsACString& aTrackingOrigin) { MOZ_ASSERT(XRE_IsParentProcess()); nsAutoCString topKey; nsCOMPtr topPrincipal = AntiTrackingUtils::GetPrincipal(aParentContext->Top()); PermissionManager::GetKeyForPrincipal(topPrincipal, false, topKey); // Propagate the storage permission to same-origin frames in the same // agent-cluster. for (const auto& topContext : aParentContext->Group()->Toplevels()) { if (topContext == aParentContext->Top()) { // In non-fission mode, storage permission is stored in the top-level, // don't need to propagtes it to tracker frames. bool useRemoteSubframes; aParentContext->GetUseRemoteSubframes(&useRemoteSubframes); if (!useRemoteSubframes) { continue; } // If parent context is third-party, we already propagate permission // in the child process, skip propagating here. RefPtr ctx = aParentContext->GetCurrentWindowContext(); if (ctx && ctx->GetIsThirdPartyWindow()) { continue; } } else { nsCOMPtr principal = AntiTrackingUtils::GetPrincipal(topContext); if (!principal) { continue; } nsAutoCString key; PermissionManager::GetKeyForPrincipal(principal, false, key); // Make sure we only apply to frames that have the same top-level. if (topKey != key) { continue; } } topContext->PreOrderWalk([&](BrowsingContext* aContext) { WindowGlobalParent* wgp = aContext->Canonical()->GetCurrentWindowGlobal(); if (!wgp) { return; } nsAutoCString origin; AntiTrackingUtils::GetPrincipalAndTrackingOrigin(aContext, nullptr, origin); if (aTrackingOrigin == origin) { Unused << wgp->SendSaveStorageAccessPermissionGranted(); } }); } } bool ContentBlocking::ShouldAllowAccessFor(nsPIDOMWindowInner* aWindow, nsIURI* aURI, uint32_t* aRejectedReason) { MOZ_ASSERT(aWindow); MOZ_ASSERT(aURI); // Let's avoid a null check on aRejectedReason everywhere else. uint32_t rejectedReason = 0; if (!aRejectedReason) { aRejectedReason = &rejectedReason; } LOG_SPEC(("Computing whether window %p has access to URI %s", aWindow, _spec), aURI); nsGlobalWindowInner* innerWindow = nsGlobalWindowInner::Cast(aWindow); Document* document = innerWindow->GetExtantDoc(); if (!document) { LOG(("Our window has no document")); return false; } uint32_t cookiePermission = CheckCookiePermissionForPrincipal( document->CookieJarSettings(), document->NodePrincipal()); if (cookiePermission != nsICookiePermission::ACCESS_DEFAULT) { LOG( ("CheckCookiePermissionForPrincipal() returned a non-default access " "code (%d) for window's principal, returning %s", int(cookiePermission), cookiePermission != nsICookiePermission::ACCESS_DENY ? "success" : "failure")); if (cookiePermission != nsICookiePermission::ACCESS_DENY) { return true; } *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION; return false; } int32_t behavior = CookiesBehavior(document); if (behavior == nsICookieService::BEHAVIOR_ACCEPT) { LOG(("The cookie behavior pref mandates accepting all cookies!")); return true; } if (ContentBlockingAllowList::Check(aWindow)) { return true; } if (behavior == nsICookieService::BEHAVIOR_REJECT) { LOG(("The cookie behavior pref mandates rejecting all cookies!")); *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL; return false; } // As a performance optimization, we only perform this check for // BEHAVIOR_REJECT_FOREIGN and BEHAVIOR_LIMIT_FOREIGN. For // BEHAVIOR_REJECT_TRACKER and BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // third-partiness is implicily checked later below. if (behavior != nsICookieService::BEHAVIOR_REJECT_TRACKER && behavior != nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { // Let's check if this is a 3rd party context. if (!AntiTrackingUtils::IsThirdPartyWindow(aWindow, aURI)) { LOG(("Our window isn't a third-party window")); return true; } } if ((behavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN && !CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)) || behavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) { // XXX For non-cookie forms of storage, we handle BEHAVIOR_LIMIT_FOREIGN by // simply rejecting the request to use the storage. In the future, if we // change the meaning of BEHAVIOR_LIMIT_FOREIGN to be one which makes sense // for non-cookie storage types, this may change. LOG(("Nothing more to do due to the behavior code %d", int(behavior))); *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN; return false; } MOZ_ASSERT( CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) || behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); uint32_t blockedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER; if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER) { if (!nsContentUtils::IsThirdPartyTrackingResourceWindow(aWindow)) { LOG(("Our window isn't a third-party tracking window")); return true; } nsCOMPtr classifiedChannel = do_QueryInterface(document->GetChannel()); if (classifiedChannel) { uint32_t classificationFlags = classifiedChannel->GetThirdPartyClassificationFlags(); if (classificationFlags & nsIClassifiedChannel::ClassificationFlags:: CLASSIFIED_SOCIALTRACKING) { blockedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER; } } } else if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { if (nsContentUtils::IsThirdPartyTrackingResourceWindow(aWindow)) { // fall through } else if (AntiTrackingUtils::IsThirdPartyWindow(aWindow, aURI)) { LOG(("We're in the third-party context, storage should be partitioned")); // fall through, but remember that we're partitioning. blockedReason = nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN; } else { LOG(("Our window isn't a third-party window, storage is allowed")); return true; } } else { MOZ_ASSERT(CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)); if (RejectForeignAllowList::Check(document)) { LOG(("This window is exceptionlisted for reject foreign")); return true; } blockedReason = nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN; } #ifdef DEBUG nsCOMPtr thirdPartyUtil = services::GetThirdPartyUtil(); if (thirdPartyUtil) { bool thirdParty = false; nsresult rv = thirdPartyUtil->IsThirdPartyWindow(aWindow->GetOuterWindow(), aURI, &thirdParty); // The result of this assertion depends on whether IsThirdPartyWindow // succeeds, because otherwise IsThirdPartyWindowOrChannel artificially // fails. MOZ_ASSERT_IF(NS_SUCCEEDED(rv), nsContentUtils::IsThirdPartyWindowOrChannel( aWindow, nullptr, aURI) == thirdParty); } #endif Document* doc = aWindow->GetExtantDoc(); // Make sure storage access isn't disabled if (doc && (doc->StorageAccessSandboxed())) { LOG(("Our document is sandboxed")); *aRejectedReason = blockedReason; return false; } // Document::HasStoragePermission first checks if storage access granted is // cached in the inner window, if no, it then checks the storage permission // flag in the channel's loadinfo bool allowed = document->HasStorageAccessPermissionGranted(); if (!allowed) { *aRejectedReason = blockedReason; } else { if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug) && aWindow->HasStorageAccessPermissionGranted()) { LOG(("Permission stored in the window. All good.")); } } return allowed; } bool ContentBlocking::ShouldAllowAccessFor(nsIChannel* aChannel, nsIURI* aURI, uint32_t* aRejectedReason) { MOZ_ASSERT(aURI); MOZ_ASSERT(aChannel); // Let's avoid a null check on aRejectedReason everywhere else. uint32_t rejectedReason = 0; if (!aRejectedReason) { aRejectedReason = &rejectedReason; } nsIScriptSecurityManager* ssm = nsScriptSecurityManager::GetScriptSecurityManager(); MOZ_ASSERT(ssm); nsCOMPtr channelURI; nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI)); if (NS_FAILED(rv)) { LOG(("Failed to get the channel final URI, bail out early")); return true; } LOG_SPEC( ("Computing whether channel %p has access to URI %s", aChannel, _spec), channelURI); nsCOMPtr loadInfo = aChannel->LoadInfo(); nsCOMPtr cookieJarSettings; rv = loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG( ("Failed to get the cookie jar settings from the loadinfo, bail out " "early")); return true; } nsCOMPtr channelPrincipal; rv = ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(channelPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("No channel principal, bail out early")); return false; } uint32_t cookiePermission = CheckCookiePermissionForPrincipal(cookieJarSettings, channelPrincipal); if (cookiePermission != nsICookiePermission::ACCESS_DEFAULT) { LOG( ("CheckCookiePermissionForPrincipal() returned a non-default access " "code (%d) for channel's principal, returning %s", int(cookiePermission), cookiePermission != nsICookiePermission::ACCESS_DENY ? "success" : "failure")); if (cookiePermission != nsICookiePermission::ACCESS_DENY) { return true; } *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION; return false; } if (!channelURI) { LOG(("No channel uri, bail out early")); return false; } int32_t behavior = CookiesBehavior(loadInfo, channelURI); if (behavior == nsICookieService::BEHAVIOR_ACCEPT) { LOG(("The cookie behavior pref mandates accepting all cookies!")); return true; } nsCOMPtr httpChannel = do_QueryInterface(aChannel); if (httpChannel && ContentBlockingAllowList::Check(httpChannel)) { return true; } if (behavior == nsICookieService::BEHAVIOR_REJECT) { LOG(("The cookie behavior pref mandates rejecting all cookies!")); *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL; return false; } nsCOMPtr thirdPartyUtil = services::GetThirdPartyUtil(); if (!thirdPartyUtil) { LOG(("No thirdPartyUtil, bail out early")); return true; } bool thirdParty = false; rv = thirdPartyUtil->IsThirdPartyChannel(aChannel, aURI, &thirdParty); // Grant if it's not a 3rd party. // Be careful to check the return value of IsThirdPartyChannel, since // IsThirdPartyChannel() will fail if the channel's loading principal is the // system principal... if (NS_SUCCEEDED(rv) && !thirdParty) { LOG(("Our channel isn't a third-party channel")); return true; } if ((behavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN && !CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)) || behavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) { // XXX For non-cookie forms of storage, we handle BEHAVIOR_LIMIT_FOREIGN by // simply rejecting the request to use the storage. In the future, if we // change the meaning of BEHAVIOR_LIMIT_FOREIGN to be one which makes sense // for non-cookie storage types, this may change. LOG(("Nothing more to do due to the behavior code %d", int(behavior))); *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN; return false; } MOZ_ASSERT( CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) || behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); uint32_t blockedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER; // Not a tracker. nsCOMPtr classifiedChannel = do_QueryInterface(aChannel); if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER) { if (classifiedChannel) { if (!classifiedChannel->IsThirdPartyTrackingResource()) { LOG(("Our channel isn't a third-party tracking channel")); return true; } uint32_t classificationFlags = classifiedChannel->GetThirdPartyClassificationFlags(); if (classificationFlags & nsIClassifiedChannel::ClassificationFlags:: CLASSIFIED_SOCIALTRACKING) { blockedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER; } } } else if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { if (classifiedChannel && classifiedChannel->IsThirdPartyTrackingResource()) { // fall through } else if (AntiTrackingUtils::IsThirdPartyChannel(aChannel)) { LOG(("We're in the third-party context, storage should be partitioned")); // fall through but remember that we're partitioning. blockedReason = nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN; } else { LOG(("Our channel isn't a third-party channel, storage is allowed")); return true; } } else { MOZ_ASSERT(CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)); if (httpChannel && RejectForeignAllowList::Check(httpChannel)) { LOG(("This channel is exceptionlisted")); return true; } blockedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN; } RefPtr targetBC; rv = loadInfo->GetTargetBrowsingContext(getter_AddRefs(targetBC)); if (!targetBC || NS_WARN_IF(NS_FAILED(rv))) { LOG(("Failed to get the channel's target browsing context")); return false; } if (Document::StorageAccessSandboxed(targetBC->GetSandboxFlags())) { LOG(("Our document is sandboxed")); *aRejectedReason = blockedReason; return false; } // Let's see if we have to grant the access for this particular channel. nsCOMPtr trackingURI; rv = aChannel->GetURI(getter_AddRefs(trackingURI)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Failed to get the channel URI")); return true; } nsAutoCString trackingOrigin; rv = nsContentUtils::GetASCIIOrigin(trackingURI, trackingOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { LOG_SPEC(("Failed to compute the origin from %s", _spec), trackingURI); return false; } // HasStorageAccessPermissionGranted only applies to channels that load // documents, for sub-resources loads, just returns the result from loadInfo. bool isDocument = false; aChannel->GetIsDocument(&isDocument); if (isDocument) { nsCOMPtr inner = AntiTrackingUtils::GetInnerWindow(targetBC); if (inner && inner->HasStorageAccessPermissionGranted()) { LOG(("Permission stored in the window. All good.")); return true; } } bool allowed = loadInfo->GetHasStoragePermission(); if (!allowed) { *aRejectedReason = blockedReason; } return allowed; } bool ContentBlocking::ShouldAllowAccessFor( nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aCookieJarSettings); uint32_t access = nsICookiePermission::ACCESS_DEFAULT; if (aPrincipal->GetIsContentPrincipal()) { PermissionManager* permManager = PermissionManager::GetInstance(); if (permManager) { Unused << NS_WARN_IF(NS_FAILED(permManager->TestPermissionFromPrincipal( aPrincipal, "cookie"_ns, &access))); } } if (access != nsICookiePermission::ACCESS_DEFAULT) { return access != nsICookiePermission::ACCESS_DENY; } int32_t behavior = CookiesBehavior(aPrincipal, aCookieJarSettings); return behavior != nsICookieService::BEHAVIOR_REJECT; } /* static */ bool ContentBlocking::ApproximateAllowAccessForWithoutChannel( nsPIDOMWindowInner* aFirstPartyWindow, nsIURI* aURI) { MOZ_ASSERT(aFirstPartyWindow); MOZ_ASSERT(aURI); LOG_SPEC( ("Computing a best guess as to whether window %p has access to URI %s", aFirstPartyWindow, _spec), aURI); Document* parentDocument = nsGlobalWindowInner::Cast(aFirstPartyWindow)->GetExtantDoc(); if (NS_WARN_IF(!parentDocument)) { LOG(("Failed to get the first party window's document")); return false; } if (!parentDocument->CookieJarSettings()->GetRejectThirdPartyContexts()) { LOG(("Disabled by the pref (%d), bail out early", parentDocument->CookieJarSettings()->GetCookieBehavior())); return true; } if (ContentBlockingAllowList::Check(aFirstPartyWindow)) { return true; } if (!AntiTrackingUtils::IsThirdPartyWindow(aFirstPartyWindow, aURI)) { LOG(("Our window isn't a third-party window")); return true; } uint32_t cookiePermission = CheckCookiePermissionForPrincipal( parentDocument->CookieJarSettings(), parentDocument->NodePrincipal()); if (cookiePermission != nsICookiePermission::ACCESS_DEFAULT) { LOG( ("CheckCookiePermissionForPrincipal() returned a non-default access " "code (%d), returning %s", int(cookiePermission), cookiePermission != nsICookiePermission::ACCESS_DENY ? "success" : "failure")); return cookiePermission != nsICookiePermission::ACCESS_DENY; } nsAutoCString origin; nsresult rv = nsContentUtils::GetASCIIOrigin(aURI, origin); if (NS_WARN_IF(NS_FAILED(rv))) { LOG_SPEC(("Failed to compute the origin from %s", _spec), aURI); return false; } nsIPrincipal* parentPrincipal = parentDocument->NodePrincipal(); nsAutoCString type; AntiTrackingUtils::CreateStoragePermissionKey(origin, type); return AntiTrackingUtils::CheckStoragePermission( parentPrincipal, type, nsContentUtils::IsInPrivateBrowsing(parentDocument), nullptr, 0); }