/* -*- 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 "AntiTrackingCommon.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/ipc/MessageChannel.h" #include "mozilla/AbstractThread.h" #include "mozilla/HashFunctions.h" #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/Logging.h" #include "mozilla/MruCache.h" #include "mozilla/Pair.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs_extensions.h" #include "mozilla/StaticPrefs_privacy.h" #include "mozIThirdPartyUtil.h" #include "nsContentUtils.h" #include "nsGlobalWindowInner.h" #include "nsIClassifiedChannel.h" #include "nsICookiePermission.h" #include "nsICookieService.h" #include "nsIDocShell.h" #include "nsIHttpChannelInternal.h" #include "nsIParentChannel.h" #include "nsIPermission.h" #include "nsPermissionManager.h" #include "nsIPrincipal.h" #include "nsIRedirectHistoryEntry.h" #include "nsIScriptError.h" #include "nsIURI.h" #include "nsIURIFixup.h" #include "nsIWebProgressListener.h" #include "nsNetUtil.h" #include "nsPIDOMWindow.h" #include "nsPrintfCString.h" #include "nsScriptSecurityManager.h" #include "nsSandboxFlags.h" #include "prtime.h" #define ANTITRACKING_PERM_KEY "3rdPartyStorage" #define ANTITRACKING_CONSOLE_CATEGORY NS_LITERAL_CSTRING("Content Blocking") using namespace mozilla; using mozilla::dom::BrowsingContext; using mozilla::dom::ContentChild; using mozilla::dom::Document; static LazyLogModule gAntiTrackingLog("AntiTracking"); static const nsCString::size_type sMaxSpecLength = 128; static const uint32_t kMaxConsoleOutputDelayMs = 100; #define LOG(format) MOZ_LOG(gAntiTrackingLog, mozilla::LogLevel::Debug, format) #define LOG_SPEC(format, uri) \ PR_BEGIN_MACRO \ if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) { \ nsAutoCString _specStr(NS_LITERAL_CSTRING("(null)")); \ _specStr.Truncate(std::min(_specStr.Length(), sMaxSpecLength)); \ if (uri) { \ _specStr = uri->GetSpecOrDefault(); \ } \ const char* _spec = _specStr.get(); \ LOG(format); \ } \ PR_END_MACRO #define LOG_SPEC2(format, uri1, uri2) \ PR_BEGIN_MACRO \ if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) { \ nsAutoCString _specStr1(NS_LITERAL_CSTRING("(null)")); \ _specStr1.Truncate(std::min(_specStr1.Length(), sMaxSpecLength)); \ if (uri1) { \ _specStr1 = uri1->GetSpecOrDefault(); \ } \ const char* _spec1 = _specStr1.get(); \ nsAutoCString _specStr2(NS_LITERAL_CSTRING("(null)")); \ _specStr2.Truncate(std::min(_specStr2.Length(), sMaxSpecLength)); \ if (uri2) { \ _specStr2 = uri2->GetSpecOrDefault(); \ } \ const char* _spec2 = _specStr2.get(); \ LOG(format); \ } \ PR_END_MACRO namespace { UniquePtr> gSettingsChangedCallbacks; bool GetParentPrincipalAndTrackingOrigin( nsGlobalWindowInner* a3rdPartyTrackingWindow, uint32_t aBehavior, nsIPrincipal** aTopLevelStoragePrincipal, nsACString& aTrackingOrigin, nsIURI** aTrackingURI, nsIPrincipal** aTrackingPrincipal) { // Now we need the principal and the origin of the parent window. nsCOMPtr topLevelStoragePrincipal = // Use the "top-level storage area principal" behaviour in reject tracker // mode only. (aBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER) ? a3rdPartyTrackingWindow->GetTopLevelStorageAreaPrincipal() : a3rdPartyTrackingWindow->GetTopLevelAntiTrackingPrincipal(); if (!topLevelStoragePrincipal) { LOG(("No top-level storage area principal at hand")); return false; } // Let's take the principal and the origin of the tracker. nsCOMPtr trackingPrincipal = a3rdPartyTrackingWindow->GetPrincipal(); if (NS_WARN_IF(!trackingPrincipal)) { return false; } nsCOMPtr trackingURI; nsresult rv = trackingPrincipal->GetURI(getter_AddRefs(trackingURI)); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } rv = trackingPrincipal->GetOriginNoSuffix(aTrackingOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } topLevelStoragePrincipal.forget(aTopLevelStoragePrincipal); if (aTrackingURI) { trackingURI.forget(aTrackingURI); } if (aTrackingPrincipal) { trackingPrincipal.forget(aTrackingPrincipal); } return true; }; void CreatePermissionKey(const nsCString& aTrackingOrigin, nsACString& aPermissionKey) { MOZ_ASSERT(aPermissionKey.IsEmpty()); static const nsLiteralCString prefix = NS_LITERAL_CSTRING(ANTITRACKING_PERM_KEY "^"); aPermissionKey.SetCapacity(prefix.Length() + aTrackingOrigin.Length()); aPermissionKey.Append(prefix); aPermissionKey.Append(aTrackingOrigin); } // This internal method returns ACCESS_DENY if the access is denied, // ACCESS_DEFAULT if unknown, some other access code if granted. uint32_t CheckCookiePermissionForPrincipal(nsICookieSettings* aCookieSettings, nsIPrincipal* aPrincipal) { MOZ_ASSERT(aCookieSettings); MOZ_ASSERT(aPrincipal); uint32_t cookiePermission = nsICookiePermission::ACCESS_DEFAULT; if (!aPrincipal->GetIsContentPrincipal()) { return cookiePermission; } nsresult rv = aCookieSettings->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->CookieSettings()->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 cookieSettings; nsresult rv = aLoadInfo->GetCookieSettings(getter_AddRefs(cookieSettings)); if (NS_WARN_IF(NS_FAILED(rv))) { return nsICookieService::BEHAVIOR_REJECT; } return cookieSettings->GetCookieBehavior(); } int32_t CookiesBehavior(nsIPrincipal* aPrincipal, nsICookieSettings* aCookieSettings) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aCookieSettings); // WebExtensions principals always get BEHAVIOR_ACCEPT as cookieBehavior // (See Bug 1406675 for rationale). if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) { return nsICookieService::BEHAVIOR_ACCEPT; } return aCookieSettings->GetCookieBehavior(); } struct ContentBlockingAllowListKey { ContentBlockingAllowListKey() : mHash(mozilla::HashGeneric(uintptr_t(0))) {} // Ensure that we compute a different hash for window and channel pointers of // the same numeric value, in the off chance that we get unlucky and encounter // a case where the allocator reallocates a window object where a channel used // to live and vice versa. explicit ContentBlockingAllowListKey(nsPIDOMWindowInner* aWindow) : mHash(mozilla::AddToHash(aWindow->WindowID(), mozilla::HashString("window"))) {} explicit ContentBlockingAllowListKey(nsIHttpChannel* aChannel) : mHash(mozilla::AddToHash(aChannel->ChannelId(), mozilla::HashString("channel"))) {} ContentBlockingAllowListKey(const ContentBlockingAllowListKey& aRHS) : mHash(aRHS.mHash) {} bool operator==(const ContentBlockingAllowListKey& aRHS) const { return mHash == aRHS.mHash; } HashNumber GetHash() const { return mHash; } private: HashNumber mHash; }; struct ContentBlockingAllowListEntry { ContentBlockingAllowListEntry() : mResult(false) {} ContentBlockingAllowListEntry(nsPIDOMWindowInner* aWindow, bool aResult) : mKey(aWindow), mResult(aResult) {} ContentBlockingAllowListEntry(nsIHttpChannel* aChannel, bool aResult) : mKey(aChannel), mResult(aResult) {} ContentBlockingAllowListKey mKey; bool mResult; }; struct ContentBlockingAllowListCache : MruCache { static HashNumber Hash(const ContentBlockingAllowListKey& aKey) { return aKey.GetHash(); } static bool Match(const ContentBlockingAllowListKey& aKey, const ContentBlockingAllowListEntry& aValue) { return aValue.mKey == aKey; } }; ContentBlockingAllowListCache& GetContentBlockingAllowListCache() { static bool initialized = false; static ContentBlockingAllowListCache cache; if (!initialized) { AntiTrackingCommon::OnAntiTrackingSettingsChanged([&] { // Drop everything in the cache, since the result of content blocking // allow list checks may change past this point. cache.Clear(); }); initialized = true; } return cache; } bool CheckContentBlockingAllowList(nsIPrincipal* aTopWinPrincipal, bool aIsPrivateBrowsing) { bool isAllowed = false; nsresult rv = AntiTrackingCommon::IsOnContentBlockingAllowList( aTopWinPrincipal, aIsPrivateBrowsing, isAllowed); if (NS_SUCCEEDED(rv) && isAllowed) { LOG( ("The top-level window is on the content blocking allow list, " "bail out early")); return true; } if (NS_FAILED(rv)) { LOG(("Checking the content blocking allow list for failed with %" PRIx32, static_cast(rv))); } return false; } bool CheckContentBlockingAllowList(nsPIDOMWindowInner* aWindow) { ContentBlockingAllowListKey cacheKey(aWindow); auto entry = GetContentBlockingAllowListCache().Lookup(cacheKey); if (entry) { // We've recently performed a content blocking allow list check for this // window, so let's quickly return the answer instead of continuing with the // rest of this potentially expensive computation. return entry.Data().mResult; } nsPIDOMWindowOuter* top = aWindow->GetBrowsingContext()->Top()->GetDOMWindow(); Document* doc = top ? top->GetExtantDoc() : nullptr; if (doc) { bool isPrivateBrowsing = nsContentUtils::IsInPrivateBrowsing(doc); const bool result = CheckContentBlockingAllowList( doc->GetContentBlockingAllowListPrincipal(), isPrivateBrowsing); entry.Set(ContentBlockingAllowListEntry(aWindow, result)); return result; } LOG( ("Could not check the content blocking allow list because the top " "window wasn't accessible")); entry.Set(ContentBlockingAllowListEntry(aWindow, false)); return false; } bool CheckContentBlockingAllowList(nsIHttpChannel* aChannel) { ContentBlockingAllowListKey cacheKey(aChannel); auto entry = GetContentBlockingAllowListCache().Lookup(cacheKey); if (entry) { // We've recently performed a content blocking allow list check for this // channel, so let's quickly return the answer instead of continuing with // the rest of this potentially expensive computation. return entry.Data().mResult; } nsCOMPtr principal; nsCOMPtr httpChan = do_QueryInterface(aChannel); if (httpChan) { nsresult rv = httpChan->GetContentBlockingAllowListPrincipal( getter_AddRefs(principal)); if (NS_FAILED(rv) || !principal) { LOG( ("Could not check the content blocking allow list because the top " "window wasn't accessible")); entry.Set(ContentBlockingAllowListEntry(aChannel, false)); return false; } } const bool result = CheckContentBlockingAllowList(principal, NS_UsePrivateBrowsing(aChannel)); entry.Set(ContentBlockingAllowListEntry(aChannel, result)); return result; } void RunConsoleReportingRunnable(already_AddRefed&& aRunnable) { if (StaticPrefs::privacy_restrict3rdpartystorage_console_lazy()) { nsresult rv = NS_DispatchToCurrentThreadQueue(std::move(aRunnable), kMaxConsoleOutputDelayMs, EventQueuePriority::Idle); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } else { nsCOMPtr runnable(std::move(aRunnable)); nsresult rv = runnable->Run(); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } } void ReportBlockingToConsole(nsPIDOMWindowOuter* aWindow, nsIURI* aURI, uint32_t aRejectedReason) { MOZ_ASSERT(aWindow && aURI); MOZ_ASSERT( aRejectedReason == 0 || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN); RefPtr doc = aWindow->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return; } nsAutoString sourceLine; uint32_t lineNumber = 0, columnNumber = 0; JSContext* cx = nsContentUtils::GetCurrentJSContext(); if (cx) { nsJSUtils::GetCallingLocation(cx, sourceLine, &lineNumber, &columnNumber); } nsCOMPtr uri(aURI); RefPtr runnable = NS_NewRunnableFunction( "ReportBlockingToConsoleDelayed", [doc, sourceLine, lineNumber, columnNumber, uri, aRejectedReason]() { const char* message = nullptr; nsAutoCString category; // When changing this list, please make sure to update the corresponding // code in antitracking_head.js (inside _createTask). switch (aRejectedReason) { case nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION: message = "CookieBlockedByPermission"; category = NS_LITERAL_CSTRING("cookieBlockedPermission"); break; case nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER: message = "CookieBlockedTracker"; category = NS_LITERAL_CSTRING("cookieBlockedTracker"); break; case nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL: message = "CookieBlockedAll"; category = NS_LITERAL_CSTRING("cookieBlockedAll"); break; case nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN: message = "CookieBlockedForeign"; category = NS_LITERAL_CSTRING("cookieBlockedForeign"); break; default: return; } MOZ_ASSERT(message); // Strip the URL of any possible username/password and make it ready // to be presented in the UI. nsCOMPtr urifixup = services::GetURIFixup(); NS_ENSURE_TRUE_VOID(urifixup); nsCOMPtr exposableURI; nsresult rv = urifixup->CreateExposableURI(uri, getter_AddRefs(exposableURI)); NS_ENSURE_SUCCESS_VOID(rv); AutoTArray params; CopyUTF8toUTF16(exposableURI->GetSpecOrDefault(), *params.AppendElement()); nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, category, doc, nsContentUtils::eNECKO_PROPERTIES, message, params, nullptr, sourceLine, lineNumber, columnNumber); }); RunConsoleReportingRunnable(runnable.forget()); } void ReportUnblockingToConsole( nsPIDOMWindowInner* aWindow, const nsAString& aTrackingOrigin, AntiTrackingCommon::StorageAccessGrantedReason aReason) { nsCOMPtr principal = nsGlobalWindowInner::Cast(aWindow)->GetPrincipal(); if (NS_WARN_IF(!principal)) { return; } RefPtr doc = aWindow->GetExtantDoc(); if (NS_WARN_IF(!doc)) { return; } nsAutoString trackingOrigin(aTrackingOrigin); nsAutoString sourceLine; uint32_t lineNumber = 0, columnNumber = 0; JSContext* cx = nsContentUtils::GetCurrentJSContext(); if (cx) { nsJSUtils::GetCallingLocation(cx, sourceLine, &lineNumber, &columnNumber); } RefPtr runnable = NS_NewRunnableFunction( "ReportUnblockingToConsoleDelayed", [doc, principal, trackingOrigin, sourceLine, lineNumber, columnNumber, aReason]() { nsAutoString origin; nsresult rv = nsContentUtils::GetUTFOrigin(principal, origin); if (NS_WARN_IF(NS_FAILED(rv))) { return; } // Not adding grantedOrigin yet because we may not want it later. AutoTArray params = {origin, trackingOrigin}; const char* messageWithSameOrigin = nullptr; switch (aReason) { case AntiTrackingCommon::eStorageAccessAPI: messageWithSameOrigin = "CookieAllowedForTrackerByStorageAccessAPI"; break; case AntiTrackingCommon::eOpenerAfterUserInteraction: [[fallthrough]]; case AntiTrackingCommon::eOpener: messageWithSameOrigin = "CookieAllowedForTrackerByHeuristic"; break; } nsContentUtils::ReportToConsole( nsIScriptError::warningFlag, ANTITRACKING_CONSOLE_CATEGORY, doc, nsContentUtils::eNECKO_PROPERTIES, messageWithSameOrigin, params, nullptr, sourceLine, lineNumber, columnNumber); }); RunConsoleReportingRunnable(runnable.forget()); } already_AddRefed 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(); } class TemporaryAccessGrantCacheKey : public PLDHashEntryHdr { public: typedef Pair, nsCString> KeyType; typedef const KeyType* KeyTypePointer; explicit TemporaryAccessGrantCacheKey(KeyTypePointer aKey) : mPrincipal(aKey->first()), mType(aKey->second()) {} TemporaryAccessGrantCacheKey(TemporaryAccessGrantCacheKey&& aOther) = default; ~TemporaryAccessGrantCacheKey() = default; KeyType GetKey() const { return MakePair(mPrincipal, mType); } bool KeyEquals(KeyTypePointer aKey) const { return !!mPrincipal == !!aKey->first() && mType == aKey->second() && (mPrincipal ? (mPrincipal->Equals(aKey->first())) : true); } static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; } static PLDHashNumber HashKey(KeyTypePointer aKey) { if (!aKey) { return 0; } BasePrincipal* bp = BasePrincipal::Cast(aKey->first()); return HashGeneric(bp->GetOriginNoSuffixHash(), bp->GetOriginSuffixHash(), HashString(aKey->second())); } enum { ALLOW_MEMMOVE = true }; private: nsCOMPtr mPrincipal; nsCString mType; }; class TemporaryAccessGrantObserver final : public nsIObserver { public: NS_DECL_ISUPPORTS NS_DECL_NSIOBSERVER static void Create(nsPermissionManager* aPM, nsIPrincipal* aPrincipal, const nsACString& aType) { MOZ_ASSERT(XRE_IsParentProcess()); if (!sObservers) { sObservers = MakeUnique(); } Unused << sObservers ->LookupForAdd(MakePair(nsCOMPtr(aPrincipal), nsCString(aType))) .OrInsert([&]() -> nsITimer* { // Only create a new observer if we don't have a matching // entry in our hashtable. nsCOMPtr timer; RefPtr observer = new TemporaryAccessGrantObserver(aPM, aPrincipal, aType); nsresult rv = NS_NewTimerWithObserver( getter_AddRefs(timer), observer, 24 * 60 * 60 * 1000, // 24 hours nsITimer::TYPE_ONE_SHOT); if (NS_SUCCEEDED(rv)) { observer->SetTimer(timer); return timer; } timer->Cancel(); return nullptr; }); } void SetTimer(nsITimer* aTimer) { mTimer = aTimer; nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) { observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); } } private: TemporaryAccessGrantObserver(nsPermissionManager* aPM, nsIPrincipal* aPrincipal, const nsACString& aType) : mPM(aPM), mPrincipal(aPrincipal), mType(aType) { MOZ_ASSERT(XRE_IsParentProcess(), "Enforcing temporary access grant lifetimes can only be done in " "the parent process"); } ~TemporaryAccessGrantObserver() = default; private: typedef nsDataHashtable> ObserversTable; static UniquePtr sObservers; nsCOMPtr mTimer; RefPtr mPM; nsCOMPtr mPrincipal; nsCString mType; }; UniquePtr TemporaryAccessGrantObserver::sObservers; NS_IMPL_ISUPPORTS(TemporaryAccessGrantObserver, nsIObserver) NS_IMETHODIMP TemporaryAccessGrantObserver::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC) == 0) { Unused << mPM->RemoveFromPrincipal(mPrincipal, mType); MOZ_ASSERT(sObservers); sObservers->Remove(MakePair(mPrincipal, mType)); } else if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) { observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); } if (mTimer) { mTimer->Cancel(); mTimer = nullptr; } sObservers.reset(); } return NS_OK; } class SettingsChangeObserver final : public nsIObserver { ~SettingsChangeObserver() = default; public: NS_DECL_ISUPPORTS NS_DECL_NSIOBSERVER static void PrivacyPrefChanged(const char* aPref = nullptr, void* = nullptr); private: static void RunAntiTrackingSettingsChangedCallbacks(); }; NS_IMPL_ISUPPORTS(SettingsChangeObserver, nsIObserver) NS_IMETHODIMP SettingsChangeObserver::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp(aTopic, "xpcom-shutdown")) { nsCOMPtr obs = services::GetObserverService(); if (obs) { obs->RemoveObserver(this, "perm-added"); obs->RemoveObserver(this, "perm-changed"); obs->RemoveObserver(this, "perm-cleared"); obs->RemoveObserver(this, "perm-deleted"); obs->RemoveObserver(this, "xpcom-shutdown"); Preferences::UnregisterPrefixCallback( SettingsChangeObserver::PrivacyPrefChanged, "browser.contentblocking."); Preferences::UnregisterPrefixCallback( SettingsChangeObserver::PrivacyPrefChanged, "network.cookie."); Preferences::UnregisterPrefixCallback( SettingsChangeObserver::PrivacyPrefChanged, "privacy."); gSettingsChangedCallbacks = nullptr; } } else { nsCOMPtr perm = do_QueryInterface(aSubject); if (perm) { nsAutoCString type; nsresult rv = perm->GetType(type); if (NS_WARN_IF(NS_FAILED(rv)) || type.Equals(USER_INTERACTION_PERM)) { // Ignore failures or notifications that have been sent because of // user interactions. return NS_OK; } } RunAntiTrackingSettingsChangedCallbacks(); } return NS_OK; } // static void SettingsChangeObserver::PrivacyPrefChanged(const char* aPref, void* aClosure) { RunAntiTrackingSettingsChangedCallbacks(); } // static void SettingsChangeObserver::RunAntiTrackingSettingsChangedCallbacks() { if (gSettingsChangedCallbacks) { for (auto& callback : *gSettingsChangedCallbacks) { callback(); } } } bool CheckAntiTrackingPermission(nsIPrincipal* aPrincipal, const nsAutoCString& aType, bool aIsInPrivateBrowsing, uint32_t* aRejectedReason, uint32_t aBlockedReason, nsIURI* aPrincipalURI) { nsPermissionManager* permManager = nsPermissionManager::GetInstance(); if (NS_WARN_IF(!permManager)) { LOG(("Failed to obtain the permission manager")); return false; } uint32_t result = 0; if (aIsInPrivateBrowsing) { LOG_SPEC(("Querying the permissions for private modei looking for a " "permission of type %s for %s", aType.get(), _spec), aPrincipalURI); 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_SPEC( ("Testing permission type %s for %s resulted in %d (%s)", aType.get(), _spec, int(result), result == nsIPermissionManager::ALLOW_ACTION ? "success" : "failure"), aPrincipalURI); if (result != nsIPermissionManager::ALLOW_ACTION) { if (aRejectedReason) { *aRejectedReason = aBlockedReason; } return false; } } return true; } void NotifyBlockingDecisionInternal( nsIChannel* aReportingChannel, nsIChannel* aTrackingChannel, AntiTrackingCommon::BlockingDecision aDecision, uint32_t aRejectedReason, nsIURI* aURI, nsPIDOMWindowOuter* aWindow) { MOZ_DIAGNOSTIC_ASSERT_IF( nsGlobalWindowOuter::Cast(aWindow)->IsChromeWindow(), aDecision == AntiTrackingCommon::BlockingDecision::eAllow); if (aDecision == AntiTrackingCommon::BlockingDecision::eBlock) { AntiTrackingCommon::NotifyContentBlockingEvent(aWindow, aReportingChannel, aTrackingChannel, true, aRejectedReason, aURI); ReportBlockingToConsole(aWindow, aURI, aRejectedReason); } // Now send the generic "cookies loaded" notifications, from the most generic // to the most specific. AntiTrackingCommon::NotifyContentBlockingEvent( aWindow, aReportingChannel, aTrackingChannel, false, nsIWebProgressListener::STATE_COOKIES_LOADED, aURI); nsCOMPtr classifiedChannel = do_QueryInterface(aTrackingChannel); if (!classifiedChannel) { return; } uint32_t classificationFlags = classifiedChannel->GetThirdPartyClassificationFlags(); if (classificationFlags & nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_TRACKING) { AntiTrackingCommon::NotifyContentBlockingEvent( aWindow, aReportingChannel, aTrackingChannel, false, nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER, aURI); } if (classificationFlags & nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_SOCIALTRACKING) { AntiTrackingCommon::NotifyContentBlockingEvent( aWindow, aReportingChannel, aTrackingChannel, false, nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER, aURI); } } } // namespace /* static */ RefPtr AntiTrackingCommon::AddFirstPartyStorageAccessGrantedFor( nsIPrincipal* aPrincipal, nsPIDOMWindowInner* aParentWindow, StorageAccessGrantedReason aReason, const AntiTrackingCommon::PerformFinalChecks& aPerformFinalChecks) { MOZ_ASSERT(aParentWindow); switch (aReason) { case eOpener: if (!StaticPrefs:: privacy_restrict3rdpartystorage_heuristic_window_open()) { LOG( ("Bailing out early because the " "privacy.restrict3rdpartystorage.heuristic.window_open preference " "has been disabled")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } break; case 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 StorageAccessGrantPromise::CreateAndReject(false, __func__); } break; default: break; } nsCOMPtr uri; aPrincipal->GetURI(getter_AddRefs(uri)); if (NS_WARN_IF(!uri)) { LOG(("Can't get the URI from the principal")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) { nsAutoCString origin; Unused << nsContentUtils::GetASCIIOrigin(uri, origin); LOG(("Adding a first-party storage exception for %s...", PromiseFlatCString(origin).get())); } Document* parentDoc = aParentWindow->GetExtantDoc(); if (!parentDoc) { LOG(("Parent window has no doc")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } int32_t behavior = parentDoc->CookieSettings()->GetCookieBehavior(); if (!parentDoc->CookieSettings()->GetRejectThirdPartyTrackers()) { LOG( ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out " "early", behavior)); return StorageAccessGrantPromise::CreateAndResolve(true, __func__); } MOZ_ASSERT( behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); if (CheckContentBlockingAllowList(aParentWindow)) { return StorageAccessGrantPromise::CreateAndResolve(true, __func__); } nsCOMPtr topLevelStoragePrincipal; nsCOMPtr trackingURI; nsAutoCString trackingOrigin; nsCOMPtr trackingPrincipal; RefPtr parentWindow = nsGlobalWindowInner::Cast(aParentWindow); nsGlobalWindowOuter* outerParentWindow = nsGlobalWindowOuter::Cast(parentWindow->GetOuterWindow()); if (NS_WARN_IF(!outerParentWindow)) { LOG(("No outer window found for our parent window, bailing out early")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } LOG(("The current resource is %s-party", outerParentWindow->IsTopLevelWindow() ? "first" : "third")); // We are a first party resource. if (outerParentWindow->IsTopLevelWindow()) { nsAutoCString origin; nsresult rv = nsContentUtils::GetASCIIOrigin(uri, origin); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't get the origin from the URI")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } trackingOrigin = origin; trackingPrincipal = aPrincipal; rv = trackingPrincipal->GetURI(getter_AddRefs(trackingURI)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Couldn't get the tracking principal URI")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } topLevelStoragePrincipal = parentWindow->GetPrincipal(); if (NS_WARN_IF(!topLevelStoragePrincipal)) { LOG(("Top-level storage area principal not found, bailing out early")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } } else { // We should be a 3rd party source. bool isThirdParty = false; if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER) { isThirdParty = nsContentUtils::IsThirdPartyTrackingResourceWindow(parentWindow); } else if (behavior == nsICookieService:: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { isThirdParty = nsContentUtils::IsThirdPartyWindowOrChannel( parentWindow, nullptr, nullptr); } if (!isThirdParty) { if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER) { LOG(("Our window isn't a third-party tracking window")); } else if (behavior == nsICookieService:: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { LOG(("Our window isn't a third-party window")); } return StorageAccessGrantPromise::CreateAndReject(false, __func__); } Document* doc = parentWindow->GetExtantDoc(); // Make sure storage access isn't disabled if (doc && (doc->StorageAccessSandboxed())) { LOG(("Our document is sandboxed")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } if (!GetParentPrincipalAndTrackingOrigin( parentWindow, // Don't request the ETP specific behaviour of allowing only // singly-nested iframes here, because we are recording an allow // permission. nsICookieService::BEHAVIOR_ACCEPT, getter_AddRefs(topLevelStoragePrincipal), trackingOrigin, getter_AddRefs(trackingURI), getter_AddRefs(trackingPrincipal))) { LOG( ("Error while computing the parent principal and tracking origin, " "bailing out early")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } } nsPIDOMWindowOuter* topOuterWindow = aParentWindow->GetBrowsingContext()->Top()->GetDOMWindow(); nsGlobalWindowOuter* topWindow = nsGlobalWindowOuter::Cast(topOuterWindow); if (NS_WARN_IF(!topWindow)) { LOG(("No top outer window.")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } nsPIDOMWindowInner* topInnerWindow = topWindow->GetCurrentInnerWindow(); if (NS_WARN_IF(!topInnerWindow)) { LOG(("No top inner window.")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } // 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 blacklist, 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. // // We define this as an enum, since without that MSVC fails to capturing this // name inside the lambda without the explicit capture and clang warns if // there is an explicit capture with -Wunused-lambda-capture. enum : uint32_t { blockReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER }; if (nsContentUtils::IsURIInPrefList(trackingURI, "privacy.restrict3rdpartystorage." "userInteractionRequiredForHosts") && !HasUserInteraction(trackingPrincipal)) { LOG_SPEC(("Tracking principal (%s) hasn't been interacted with before, " "refusing to add a first-party storage permission to access it", _spec), trackingURI); NotifyBlockingDecision(aParentWindow, BlockingDecision::eBlock, blockReason); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } nsCOMPtr pwin = GetTopWindow(parentWindow); if (!pwin) { LOG(("Couldn't get the top window")); return StorageAccessGrantPromise::CreateAndReject(false, __func__); } auto storePermission = [pwin, parentWindow, trackingOrigin, trackingPrincipal, trackingURI, topInnerWindow, topLevelStoragePrincipal, aReason](int aAllowMode) -> RefPtr { nsAutoCString permissionKey; CreatePermissionKey(trackingOrigin, permissionKey); // Let's store the permission in the current parent window. topInnerWindow->SaveStorageAccessGranted(permissionKey); // Let's inform the parent window. parentWindow->StorageAccessGranted(); nsIChannel* channel = pwin->GetCurrentInnerWindow()->GetExtantDoc()->GetChannel(); NotifyContentBlockingEvent(pwin, channel, parentWindow->GetExtantDoc()->GetChannel(), false, blockReason, trackingURI, Some(aReason)); ReportUnblockingToConsole(parentWindow, NS_ConvertUTF8toUTF16(trackingOrigin), aReason); if (XRE_IsParentProcess()) { LOG(("Saving the permission: trackingOrigin=%s", trackingOrigin.get())); return SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess( topLevelStoragePrincipal, trackingPrincipal, trackingOrigin, aAllowMode) ->Then(GetCurrentThreadSerialEventTarget(), __func__, [](FirstPartyStorageAccessGrantPromise::ResolveOrRejectValue&& aValue) { if (aValue.IsResolve()) { return StorageAccessGrantPromise::CreateAndResolve( eAllow, __func__); } return StorageAccessGrantPromise::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 ->SendFirstPartyStorageAccessGrantedForOrigin( IPC::Principal(topLevelStoragePrincipal), IPC::Principal(trackingPrincipal), trackingOrigin, aAllowMode) ->Then(GetCurrentThreadSerialEventTarget(), __func__, [](const ContentChild:: FirstPartyStorageAccessGrantedForOriginPromise:: ResolveOrRejectValue& aValue) { if (aValue.IsResolve()) { return StorageAccessGrantPromise::CreateAndResolve( aValue.ResolveValue(), __func__); } return StorageAccessGrantPromise::CreateAndReject(false, __func__); }); }; if (aPerformFinalChecks) { return aPerformFinalChecks()->Then( GetCurrentThreadSerialEventTarget(), __func__, [storePermission]( StorageAccessGrantPromise::ResolveOrRejectValue&& aValue) { if (aValue.IsResolve()) { return storePermission(aValue.ResolveValue()); } return StorageAccessGrantPromise::CreateAndReject(false, __func__); }); } return storePermission(false); } /* static */ RefPtr AntiTrackingCommon::SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess( 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 FirstPartyStorageAccessGrantPromise::CreateAndReject(false, __func__); }; nsCOMPtr parentPrincipalURI; Unused << aParentPrincipal->GetURI(getter_AddRefs(parentPrincipalURI)); LOG_SPEC(("Saving a first-party storage permission on %s for " "trackingOrigin=%s", _spec, aTrackingOrigin.get()), parentPrincipalURI); if (NS_WARN_IF(!aParentPrincipal)) { // The child process is sending something wrong. Let's ignore it. LOG(("aParentPrincipal is null, bailing out early")); return FirstPartyStorageAccessGrantPromise::CreateAndReject(false, __func__); } nsPermissionManager* permManager = nsPermissionManager::GetInstance(); if (NS_WARN_IF(!permManager)) { LOG(("Permission manager is null, bailing out early")); return FirstPartyStorageAccessGrantPromise::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; CreatePermissionKey(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 (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 FirstPartyStorageAccessGrantPromise::CreateAndResolve(rv, __func__); } // static bool AntiTrackingCommon::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; } CreatePermissionKey(origin, aKey); return true; } // static bool AntiTrackingCommon::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); } bool AntiTrackingCommon::IsFirstPartyStorageAccessGrantedFor( 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; } BrowsingContext* topBC = aWindow->GetBrowsingContext()->Top(); nsGlobalWindowOuter* topWindow = nullptr; if (topBC->IsInProcess()) { topWindow = nsGlobalWindowOuter::Cast(topBC->GetDOMWindow()); } else { // For out-of-process top frames, we need to be able to access three things // from the top BrowsingContext in order to be able to port this code to // Fission successfully: // * The CookieSettings of the top BrowsingContext. // * The HasStorageAccessGranted() API on BrowsingContext. // For now, if we face an out-of-process top frame, instead of failing here, // we revert back to looking at the in-process top frame. This is of course // the wrong thing to do, but we seem to have a number of tests in the tree // which are depending on this incorrect behaviour. This path is intended // to temporarily keep those tests working... nsGlobalWindowOuter* outerWindow = nsGlobalWindowOuter::Cast(aWindow->GetOuterWindow()); if (!outerWindow) { LOG(("Our window has no outer window")); return false; } nsCOMPtr topOuterWindow = outerWindow->GetInProcessTop(); topWindow = nsGlobalWindowOuter::Cast(topOuterWindow); } if (NS_WARN_IF(!topWindow)) { LOG(("No top outer window")); return false; } nsPIDOMWindowInner* topInnerWindow = topWindow->GetCurrentInnerWindow(); if (NS_WARN_IF(!topInnerWindow)) { LOG(("No top inner window.")); return false; } uint32_t cookiePermission = CheckCookiePermissionForPrincipal( document->CookieSettings(), 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 (CheckContentBlockingAllowList(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 (!nsContentUtils::IsThirdPartyWindowOrChannel(aWindow, nullptr, aURI)) { LOG(("Our window isn't a third-party window")); return true; } } if (behavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN || 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( 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 { MOZ_ASSERT(behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); if (nsContentUtils::IsThirdPartyTrackingResourceWindow(aWindow)) { // fall through } else if (nsContentUtils::IsThirdPartyWindowOrChannel(aWindow, nullptr, 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; } } #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")); return false; } nsCOMPtr parentPrincipal; nsCOMPtr parentPrincipalURI; nsCOMPtr trackingURI; nsAutoCString trackingOrigin; if (!GetParentPrincipalAndTrackingOrigin( nsGlobalWindowInner::Cast(aWindow), behavior, getter_AddRefs(parentPrincipal), trackingOrigin, getter_AddRefs(trackingURI), nullptr)) { LOG(("Failed to obtain the parent principal and the tracking origin")); *aRejectedReason = blockedReason; return false; } Unused << parentPrincipal->GetURI(getter_AddRefs(parentPrincipalURI)); nsAutoCString type; CreatePermissionKey(trackingOrigin, type); if (topInnerWindow->HasStorageAccessGranted(type)) { LOG(("Permission stored in the window. All good.")); return true; } return CheckAntiTrackingPermission( parentPrincipal, type, nsContentUtils::IsInPrivateBrowsing(document), aRejectedReason, blockedReason, parentPrincipalURI); } bool AntiTrackingCommon::IsFirstPartyStorageAccessGrantedFor( 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(); // We need to find the correct principal to check the cookie permission. For // third-party contexts, we want to check if the top-level window has a custom // cookie permission. nsCOMPtr toplevelPrincipal = loadInfo->GetTopLevelPrincipal(); // If this is already the top-level window, we should use the loading // principal. if (!toplevelPrincipal) { LOG( ("Our loadInfo lacks a top-level principal, use the loadInfo's loading " "principal instead")); toplevelPrincipal = loadInfo->LoadingPrincipal(); } nsCOMPtr httpChannel = do_QueryInterface(aChannel); // If we don't have a loading principal and this is a document channel, we are // a top-level window! if (!toplevelPrincipal) { LOG( ("We don't have a loading principal, let's see if this is a document " "channel" " that belongs to a top-level window")); bool isDocument = false; if (httpChannel) { rv = httpChannel->GetIsMainDocumentChannel(&isDocument); } if (httpChannel && NS_SUCCEEDED(rv) && isDocument) { rv = ssm->GetChannelResultPrincipal(aChannel, getter_AddRefs(toplevelPrincipal)); if (NS_SUCCEEDED(rv)) { LOG(("Yes, we guessed right!")); } else { LOG( ("Yes, we guessed right, but minting the channel result principal " "failed")); } } else { LOG(("No, we guessed wrong!")); } } // Let's use the triggering principal then. if (!toplevelPrincipal) { LOG( ("Our loadInfo lacks a top-level principal, use the loadInfo's " "triggering principal instead")); toplevelPrincipal = loadInfo->TriggeringPrincipal(); } if (NS_WARN_IF(!toplevelPrincipal)) { LOG(("No top-level principal! Bail out early")); return false; } nsCOMPtr cookieSettings; rv = loadInfo->GetCookieSettings(getter_AddRefs(cookieSettings)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG( ("Failed to get the cookie settings from the loadinfo, bail out " "early")); return true; } nsCOMPtr channelPrincipal; rv = ssm->GetChannelResultPrincipal(aChannel, getter_AddRefs(channelPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("No channel principal, bail out early")); return false; } uint32_t cookiePermission = CheckCookiePermissionForPrincipal(cookieSettings, 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; } if (httpChannel && CheckContentBlockingAllowList(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 || 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( 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 { MOZ_ASSERT(behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); if (classifiedChannel && classifiedChannel->IsThirdPartyTrackingResource()) { // fall through } else if (nsContentUtils::IsThirdPartyWindowOrChannel(nullptr, aChannel, 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 channel isn't a third-party channel, storage is allowed")); return true; } } // Only use the "top-level storage area principal" behaviour for reject // tracker mode only. nsIPrincipal* parentPrincipal = (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER) ? loadInfo->GetTopLevelStorageAreaPrincipal() : loadInfo->GetTopLevelPrincipal(); if (!parentPrincipal) { LOG(("No top-level storage area principal at hand")); // parentPrincipal can be null if the parent window is not the top-level // window. if (loadInfo->GetTopLevelPrincipal()) { LOG(("Parent window is the top-level window, bail out early")); *aRejectedReason = blockedReason; return false; } parentPrincipal = toplevelPrincipal; if (NS_WARN_IF(!parentPrincipal)) { LOG( ("No triggering principal, this shouldn't be happening! Bail out " "early")); // Why we are here?!? return true; } } nsCOMPtr parentPrincipalURI; Unused << parentPrincipal->GetURI(getter_AddRefs(parentPrincipalURI)); // 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; } nsAutoCString type; CreatePermissionKey(trackingOrigin, type); uint32_t privateBrowsingId = 0; rv = channelPrincipal->GetPrivateBrowsingId(&privateBrowsingId); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Failed to get the channel principal's private browsing ID")); return false; } return CheckAntiTrackingPermission(parentPrincipal, type, !!privateBrowsingId, aRejectedReason, blockedReason, parentPrincipalURI); } bool AntiTrackingCommon::IsFirstPartyStorageAccessGrantedFor( nsIPrincipal* aPrincipal, nsICookieSettings* aCookieSettings) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aCookieSettings); uint32_t access = nsICookiePermission::ACCESS_DEFAULT; if (aPrincipal->GetIsContentPrincipal()) { nsPermissionManager* permManager = nsPermissionManager::GetInstance(); if (permManager) { Unused << NS_WARN_IF(NS_FAILED(permManager->TestPermissionFromPrincipal( aPrincipal, NS_LITERAL_CSTRING("cookie"), &access))); } } if (access != nsICookiePermission::ACCESS_DEFAULT) { return access != nsICookiePermission::ACCESS_DENY; } int32_t behavior = CookiesBehavior(aPrincipal, aCookieSettings); return behavior != nsICookieService::BEHAVIOR_REJECT; } /* static */ bool AntiTrackingCommon::MaybeIsFirstPartyStorageAccessGrantedFor( 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->CookieSettings()->GetRejectThirdPartyTrackers()) { LOG(("Disabled by the pref (%d), bail out early", parentDocument->CookieSettings()->GetCookieBehavior())); return true; } if (CheckContentBlockingAllowList(aFirstPartyWindow)) { return true; } if (!nsContentUtils::IsThirdPartyWindowOrChannel(aFirstPartyWindow, nullptr, aURI)) { LOG(("Our window isn't a third-party window")); return true; } uint32_t cookiePermission = CheckCookiePermissionForPrincipal( parentDocument->CookieSettings(), 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(); nsCOMPtr parentPrincipalURI; Unused << parentPrincipal->GetURI(getter_AddRefs(parentPrincipalURI)); nsAutoCString type; CreatePermissionKey(origin, type); return CheckAntiTrackingPermission( parentPrincipal, type, nsContentUtils::IsInPrivateBrowsing(parentDocument), nullptr, 0, parentPrincipalURI); } nsresult AntiTrackingCommon::IsOnContentBlockingAllowList( nsIPrincipal* aTopWinPrincipal, bool aIsPrivateBrowsing, bool& aIsAllowListed) { aIsAllowListed = false; if (!aTopWinPrincipal) { // Nothing to do! return NS_OK; } nsCOMPtr uri; Unused << aTopWinPrincipal->GetURI(getter_AddRefs(uri)); LOG_SPEC(("Deciding whether the user has overridden content blocking for %s", _spec), uri); nsPermissionManager* permManager = nsPermissionManager::GetInstance(); NS_ENSURE_TRUE(permManager, NS_ERROR_FAILURE); // Check both the normal mode and private browsing mode user override // permissions. Pair types[] = { {NS_LITERAL_CSTRING("trackingprotection"), false}, {NS_LITERAL_CSTRING("trackingprotection-pb"), true}}; for (size_t i = 0; i < ArrayLength(types); ++i) { if (aIsPrivateBrowsing != types[i].second()) { continue; } uint32_t permissions = nsIPermissionManager::UNKNOWN_ACTION; nsresult rv = permManager->TestPermissionFromPrincipal( aTopWinPrincipal, types[i].first(), &permissions); NS_ENSURE_SUCCESS(rv, rv); if (permissions == nsIPermissionManager::ALLOW_ACTION) { aIsAllowListed = true; LOG(("Found user override type %s", types[i].first().get())); // Stop checking the next permisson type if we decided to override. break; } } if (!aIsAllowListed) { LOG(("No user override found")); } return NS_OK; } /* static */ void AntiTrackingCommon::ComputeContentBlockingAllowListPrincipal( nsIPrincipal* aDocumentPrincipal, nsIPrincipal** aPrincipal) { MOZ_ASSERT(aPrincipal); auto returnInputArgument = MakeScopeExit([&] { NS_IF_ADDREF(*aPrincipal = aDocumentPrincipal); }); BasePrincipal* bp = BasePrincipal::Cast(aDocumentPrincipal); if (!bp || !bp->IsContentPrincipal()) { // If we have something other than a content principal, just return what we // have. This includes the case where we were passed a nullptr. return; } nsCOMPtr uri; nsresult rv = aDocumentPrincipal->GetURI(getter_AddRefs(uri)); if (NS_WARN_IF(NS_FAILED(rv))) { return; } // Take the host/port portion so we can allowlist by site. Also ignore the // scheme, since users who put sites on the allowlist probably don't expect // allowlisting to depend on scheme. nsAutoCString escaped(NS_LITERAL_CSTRING("https://")); nsAutoCString temp; rv = uri->GetHostPort(temp); // view-source URIs will be handled by the next block. if (NS_FAILED(rv) && !uri->SchemeIs("view-source")) { // Normal for some loads, no need to print a warning return; } // GetHostPort returns an empty string (with a success error code) for file:// // URIs. if (temp.IsEmpty()) { // In this case we want to make sure that our allow list principal would be // computed as null. returnInputArgument.release(); *aPrincipal = nullptr; return; } escaped.Append(temp); rv = NS_NewURI(getter_AddRefs(uri), escaped); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr principal = BasePrincipal::CreateContentPrincipal( uri, aDocumentPrincipal->OriginAttributesRef()); if (NS_WARN_IF(!principal)) { return; } returnInputArgument.release(); principal.forget(aPrincipal); } /* static */ void AntiTrackingCommon::RecomputeContentBlockingAllowListPrincipal( nsIURI* aURIBeingLoaded, const OriginAttributes& aAttrs, nsIPrincipal** aPrincipal) { MOZ_ASSERT(aPrincipal); auto returnInputArgument = MakeScopeExit([&] { *aPrincipal = nullptr; }); // Take the host/port portion so we can allowlist by site. Also ignore the // scheme, since users who put sites on the allowlist probably don't expect // allowlisting to depend on scheme. nsAutoCString escaped(NS_LITERAL_CSTRING("https://")); nsAutoCString temp; nsresult rv = aURIBeingLoaded->GetHostPort(temp); // view-source URIs will be handled by the next block. if (NS_FAILED(rv) && !aURIBeingLoaded->SchemeIs("view-source")) { // Normal for some loads, no need to print a warning return; } // GetHostPort returns an empty string (with a success error code) for file:// // URIs. if (temp.IsEmpty()) { return; } escaped.Append(temp); nsCOMPtr uri; rv = NS_NewURI(getter_AddRefs(uri), escaped); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr principal = BasePrincipal::CreateContentPrincipal(uri, aAttrs); if (NS_WARN_IF(!principal)) { return; } returnInputArgument.release(); principal.forget(aPrincipal); } /* static */ void AntiTrackingCommon::NotifyBlockingDecision(nsIChannel* aChannel, BlockingDecision aDecision, uint32_t aRejectedReason) { MOZ_ASSERT( aRejectedReason == 0 || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN); MOZ_ASSERT(aDecision == BlockingDecision::eBlock || aDecision == BlockingDecision::eAllow); if (!aChannel) { return; } // Can be called in EITHER the parent or child process. if (XRE_IsParentProcess()) { nsCOMPtr parentChannel; NS_QueryNotificationCallbacks(aChannel, parentChannel); if (parentChannel) { // This channel is a parent-process proxy for a child process request. // Tell the child process channel to do this instead. if (aDecision == BlockingDecision::eBlock) { parentChannel->NotifyCookieBlocked(aRejectedReason); } else { parentChannel->NotifyCookieAllowed(); } // TODO: For ETP fission, we don't need to notify the // OnContentBlockingEvent in the current stage. Because we still // send a IPC to the content process in order to update the log in // the content. And the content would send back to parent to notify // the event. So, we don't need to notify here. But, we will have to // notify here once the log move to parent entirely (Bug 1599046). } return; } MOZ_ASSERT(XRE_IsContentProcess()); nsCOMPtr thirdPartyUtil = services::GetThirdPartyUtil(); if (!thirdPartyUtil) { return; } nsCOMPtr uriBeingLoaded = MaybeGetDocumentURIBeingLoaded(aChannel); nsCOMPtr win; nsresult rv = thirdPartyUtil->GetTopWindowForChannel(aChannel, uriBeingLoaded, getter_AddRefs(win)); NS_ENSURE_SUCCESS_VOID(rv); nsCOMPtr pwin = nsPIDOMWindowOuter::From(win); if (!pwin) { return; } nsCOMPtr uri; aChannel->GetURI(getter_AddRefs(uri)); NotifyBlockingDecisionInternal(aChannel, aChannel, aDecision, aRejectedReason, uri, pwin); } /* static */ void AntiTrackingCommon::NotifyBlockingDecision(nsPIDOMWindowInner* aWindow, BlockingDecision aDecision, uint32_t aRejectedReason) { MOZ_ASSERT(aWindow); MOZ_ASSERT( aRejectedReason == 0 || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL || aRejectedReason == nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN); MOZ_ASSERT(aDecision == BlockingDecision::eBlock || aDecision == BlockingDecision::eAllow); nsCOMPtr pwin = GetTopWindow(aWindow); if (!pwin) { return; } nsPIDOMWindowInner* inner = pwin->GetCurrentInnerWindow(); if (!inner) { return; } Document* pwinDoc = inner->GetExtantDoc(); if (!pwinDoc) { return; } nsIChannel* channel = pwinDoc->GetChannel(); if (!channel) { return; } Document* document = aWindow->GetExtantDoc(); if (!document) { return; } nsIURI* uri = document->GetDocumentURI(); nsIChannel* trackingChannel = document->GetChannel(); NotifyBlockingDecisionInternal(channel, trackingChannel, aDecision, aRejectedReason, uri, pwin); } /* static */ void AntiTrackingCommon::StoreUserInteractionFor(nsIPrincipal* aPrincipal) { if (!aPrincipal) { // The content process may have sent us garbage data. return; } if (XRE_IsParentProcess()) { nsCOMPtr uri; Unused << aPrincipal->GetURI(getter_AddRefs(uri)); LOG_SPEC(("Saving the userInteraction for %s", _spec), uri); nsPermissionManager* permManager = nsPermissionManager::GetInstance(); if (NS_WARN_IF(!permManager)) { LOG(("Permission manager is null, bailing out early")); return; } // Remember that this pref is stored in seconds! uint32_t expirationType = nsIPermissionManager::EXPIRE_TIME; uint32_t expirationTime = StaticPrefs::privacy_userInteraction_expiration() * 1000; int64_t when = (PR_Now() / PR_USEC_PER_MSEC) + expirationTime; uint32_t privateBrowsingId = 0; nsresult rv = aPrincipal->GetPrivateBrowsingId(&privateBrowsingId); if (!NS_WARN_IF(NS_FAILED(rv)) && privateBrowsingId > 0) { // If we are coming from a private window, make sure to store a // session-only permission which won't get persisted to disk. expirationType = nsIPermissionManager::EXPIRE_SESSION; when = 0; } rv = permManager->AddFromPrincipal(aPrincipal, USER_INTERACTION_PERM, nsIPermissionManager::ALLOW_ACTION, expirationType, when); Unused << NS_WARN_IF(NS_FAILED(rv)); return; } ContentChild* cc = ContentChild::GetSingleton(); MOZ_ASSERT(cc); nsCOMPtr uri; Unused << aPrincipal->GetURI(getter_AddRefs(uri)); LOG_SPEC(("Asking the parent process to save the user-interaction for us: %s", _spec), uri); cc->SendStoreUserInteractionAsPermission(IPC::Principal(aPrincipal)); } /* static */ bool AntiTrackingCommon::HasUserInteraction(nsIPrincipal* aPrincipal) { nsPermissionManager* permManager = nsPermissionManager::GetInstance(); if (NS_WARN_IF(!permManager)) { return false; } uint32_t result = 0; nsresult rv = permManager->TestPermissionWithoutDefaultsFromPrincipal( aPrincipal, USER_INTERACTION_PERM, &result); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } return result == nsIPermissionManager::ALLOW_ACTION; } // static void AntiTrackingCommon::OnAntiTrackingSettingsChanged( const AntiTrackingCommon::AntiTrackingSettingsChangedCallback& aCallback) { static bool initialized = false; if (!initialized) { // It is possible that while we have some data in our cache, something // changes in our environment that causes the anti-tracking checks below to // change their response. Therefore, we need to clear our cache when we // detect a related change. Preferences::RegisterPrefixCallback( SettingsChangeObserver::PrivacyPrefChanged, "browser.contentblocking."); Preferences::RegisterPrefixCallback( SettingsChangeObserver::PrivacyPrefChanged, "network.cookie."); Preferences::RegisterPrefixCallback( SettingsChangeObserver::PrivacyPrefChanged, "privacy."); nsCOMPtr obs = services::GetObserverService(); if (obs) { RefPtr observer = new SettingsChangeObserver(); obs->AddObserver(observer, "perm-added", false); obs->AddObserver(observer, "perm-changed", false); obs->AddObserver(observer, "perm-cleared", false); obs->AddObserver(observer, "perm-deleted", false); obs->AddObserver(observer, "xpcom-shutdown", false); } gSettingsChangedCallbacks = MakeUnique>(); initialized = true; } gSettingsChangedCallbacks->AppendElement(aCallback); } /* static */ already_AddRefed AntiTrackingCommon::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 AntiTrackingCommon::NotifyContentBlockingEvent( nsPIDOMWindowOuter* aWindow, nsIChannel* aReportingChannel, nsIChannel* aTrackingChannel, bool aBlocked, uint32_t aRejectedReason, nsIURI* aURI, const Maybe& aReason) { MOZ_ASSERT(aWindow); // Log the content blocking event. This should be removed in Bug 1599046. aWindow->NotifyContentBlockingEvent(aRejectedReason, aReportingChannel, aBlocked, aURI, aTrackingChannel, aReason); RefPtr browserChild = dom::BrowserChild::GetFrom(aWindow); NS_ENSURE_TRUE_VOID(browserChild); nsTArray trackingFullHashes; nsCOMPtr classifiedChannel = do_QueryInterface(aTrackingChannel); if (classifiedChannel) { Unused << classifiedChannel->GetMatchedTrackingFullHashes( trackingFullHashes); } // Send a message to notify OnContentBlockingEvent in the parent, which will // update the ContentBlockingLog in the parent. // We also notify the event in the content above since we haven't move the log // into the parent process entirely. So, we need to notify and update the // ContentBlockingLog in the content processes. We can remove this once we // finish the Bug 1599046. browserChild->NotifyContentBlockingEvent(aRejectedReason, aReportingChannel, aBlocked, aURI, trackingFullHashes, aReason); } /* static */ void AntiTrackingCommon::RedirectHeuristic(nsIChannel* aOldChannel, nsIURI* aOldURI, nsIChannel* aNewChannel, nsIURI* aNewURI) { MOZ_ASSERT(aOldChannel); MOZ_ASSERT(aOldURI); MOZ_ASSERT(aNewChannel); MOZ_ASSERT(aNewURI); nsresult rv; if (!StaticPrefs::privacy_restrict3rdpartystorage_heuristic_redirect()) { return; } nsCOMPtr newChannel = do_QueryInterface(aNewChannel); if (!newChannel) { return; } LOG_SPEC(("Checking redirect-heuristic for %s", _spec), aOldURI); nsCOMPtr oldLoadInfo = aOldChannel->LoadInfo(); MOZ_ASSERT(oldLoadInfo); nsCOMPtr newLoadInfo = aNewChannel->LoadInfo(); MOZ_ASSERT(newLoadInfo); nsContentPolicyType contentType = oldLoadInfo->GetExternalContentPolicyType(); if (contentType != nsIContentPolicy::TYPE_DOCUMENT || !aOldChannel->IsDocument()) { LOG_SPEC(("Ignoring redirect for %s because it's not a document", _spec), aOldURI); // We care about document redirects only. return; } nsCOMPtr classifiedOldChannel = do_QueryInterface(aOldChannel); nsCOMPtr classifiedNewChannel = do_QueryInterface(aNewChannel); if (!classifiedOldChannel || !classifiedNewChannel) { LOG_SPEC2(("Ignoring redirect for %s to %s because there is not " "nsIClassifiedChannel interface", _spec1, _spec2), aOldURI, aNewURI); return; } bool allowedByPreviousRedirect = oldLoadInfo->GetAllowListFutureDocumentsCreatedFromThisRedirectChain(); if (classifiedNewChannel->IsTrackingResource()) { // This is not a tracking -> non-tracking redirect. LOG_SPEC2(("Redirect for %s to %s because it's not tracking to " "non-tracking. Part of a chain of granted redirects: %d", _spec1, _spec2, allowedByPreviousRedirect), aOldURI, aNewURI); newLoadInfo->SetAllowListFutureDocumentsCreatedFromThisRedirectChain( allowedByPreviousRedirect); return; } if (!classifiedOldChannel->IsTrackingResource() && !allowedByPreviousRedirect) { // This is not a tracking -> non-tracking redirect. LOG_SPEC2( ("Redirect for %s to %s because it's not tracking to non-tracking.", _spec1, _spec2), aOldURI, aNewURI); return; } nsIScriptSecurityManager* ssm = nsScriptSecurityManager::GetScriptSecurityManager(); MOZ_ASSERT(ssm); nsCOMPtr trackingPrincipal; nsCOMPtr trackingURI; if (allowedByPreviousRedirect && !chain.IsEmpty()) { const nsTArray>& chain = oldLoadInfo->RedirectChain(); rv = chain[0]->GetPrincipal(getter_AddRefs(trackingPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't obtain the principal from the redirect chain")); return; } rv = trackingPrincipal->GetURI(getter_AddRefs(trackingURI)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't obtain the uri from the redirect chain")); return; } } else { trackingURI = aOldURI; rv = ssm->GetChannelResultPrincipal(aOldChannel, getter_AddRefs(trackingPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't obtain the principal from the tracking")); return; } } MOZ_ASSERT(trackingURI); nsCOMPtr redirectedPrincipal; rv = ssm->GetChannelResultPrincipal(aNewChannel, getter_AddRefs(redirectedPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't obtain the principal from the redirected")); return; } if (!AntiTrackingCommon::HasUserInteraction(trackingPrincipal)) { LOG_SPEC2(("Ignoring redirect for %s to %s because no user-interaction on " "tracker", _spec1, _spec2), aOldURI, aNewURI); return; } nsAutoCString trackingOrigin; rv = nsContentUtils::GetASCIIOrigin(trackingURI, trackingOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't get the origin from the URI")); return; } nsAutoCString redirectedOrigin; rv = nsContentUtils::GetASCIIOrigin(aNewURI, redirectedOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't get the origin from the URI")); return; } LOG(("Adding a first-party storage exception for %s...", PromiseFlatCString(redirectedOrigin).get())); nsCOMPtr cookieSettings; rv = oldLoadInfo->GetCookieSettings(getter_AddRefs(cookieSettings)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't get the cookieSettings")); return; } int32_t behavior = cookieSettings->GetCookieBehavior(); if (!cookieSettings->GetRejectThirdPartyTrackers()) { LOG( ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out " "early", behavior)); return; } MOZ_ASSERT( behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); if (CheckContentBlockingAllowList(newChannel)) { return; } LOG(("Saving the permission: trackingOrigin=%s, grantedOrigin=%s", trackingOrigin.get(), redirectedOrigin.get())); // Any new redirect from this loadInfo must be considered as granted. newLoadInfo->SetAllowListFutureDocumentsCreatedFromThisRedirectChain(true); uint64_t innerWindowID; Unused << newChannel->GetTopLevelContentWindowId(&innerWindowID); nsAutoString errorText; AutoTArray params = {NS_ConvertUTF8toUTF16(redirectedOrigin), NS_ConvertUTF8toUTF16(trackingOrigin)}; rv = nsContentUtils::FormatLocalizedString( nsContentUtils::eNECKO_PROPERTIES, "CookieAllowedForTrackerByHeuristic", params, errorText); if (NS_SUCCEEDED(rv)) { nsContentUtils::ReportToConsoleByWindowID( errorText, nsIScriptError::warningFlag, ANTITRACKING_CONSOLE_CATEGORY, innerWindowID); } // We don't care about this promise because the operation is actually sync. RefPtr promise = SaveFirstPartyStorageAccessGrantedForOriginOnParentProcess( redirectedPrincipal, trackingPrincipal, trackingOrigin, StorageAccessPromptChoices::eAllow, StaticPrefs::privacy_restrict3rdpartystorage_expiration_redirect()); Unused << promise; }