diff options
Diffstat (limited to 'toolkit/components/antitracking')
302 files changed, 46070 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/AntiTrackingIPCUtils.h b/toolkit/components/antitracking/AntiTrackingIPCUtils.h new file mode 100644 index 0000000000..62d2280411 --- /dev/null +++ b/toolkit/components/antitracking/AntiTrackingIPCUtils.h @@ -0,0 +1,67 @@ +/* -*- 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/. */ + +#ifndef mozilla_antitrackingipcutils_h +#define mozilla_antitrackingipcutils_h + +#include "ipc/EnumSerializer.h" + +#include "mozilla/ContentBlockingNotifier.h" +#include "mozilla/StorageAccessAPIHelper.h" + +#include "nsILoadInfo.h" + +namespace IPC { + +// For allowing passing the enum +// ContentBlockingNotifier::StorageAccessPermissionGrantedReason over IPC. +template <> +struct ParamTraits< + mozilla::ContentBlockingNotifier::StorageAccessPermissionGrantedReason> + : public ContiguousEnumSerializerInclusive< + mozilla::ContentBlockingNotifier:: + StorageAccessPermissionGrantedReason, + mozilla::ContentBlockingNotifier:: + StorageAccessPermissionGrantedReason::eStorageAccessAPI, + mozilla::ContentBlockingNotifier:: + StorageAccessPermissionGrantedReason:: + ePrivilegeStorageAccessForOriginAPI> {}; + +// ContentBlockingNotifier::BlockingDecision over IPC. +template <> +struct ParamTraits<mozilla::ContentBlockingNotifier::BlockingDecision> + : public ContiguousEnumSerializerInclusive< + mozilla::ContentBlockingNotifier::BlockingDecision, + mozilla::ContentBlockingNotifier::BlockingDecision::eBlock, + mozilla::ContentBlockingNotifier::BlockingDecision::eAllow> {}; + +// StorageAccessAPIHelper::StorageAccessPromptChoices over IPC. +template <> +struct ParamTraits<mozilla::StorageAccessAPIHelper::StorageAccessPromptChoices> + : public ContiguousEnumSerializerInclusive< + mozilla::StorageAccessAPIHelper::StorageAccessPromptChoices, + mozilla::StorageAccessAPIHelper::StorageAccessPromptChoices::eAllow, + mozilla::StorageAccessAPIHelper::StorageAccessPromptChoices:: + eAllowAutoGrant> {}; + +// nsILoadInfo::StoragePermissionState over IPC. +template <> +struct ParamTraits<nsILoadInfo::StoragePermissionState> + : public ContiguousEnumSerializerInclusive< + nsILoadInfo::StoragePermissionState, + nsILoadInfo::StoragePermissionState::NoStoragePermission, + nsILoadInfo::StoragePermissionState::StoragePermissionAllowListed> {}; + +// ContentBlockingNotifier::CanvasFingerprinter over IPC. +template <> +struct ParamTraits<mozilla::ContentBlockingNotifier::CanvasFingerprinter> + : public ContiguousEnumSerializerInclusive< + mozilla::ContentBlockingNotifier::CanvasFingerprinter, + mozilla::ContentBlockingNotifier::CanvasFingerprinter::eFingerprintJS, + mozilla::ContentBlockingNotifier::CanvasFingerprinter::eMaybe> {}; +} // namespace IPC + +#endif // mozilla_antitrackingipcutils_h diff --git a/toolkit/components/antitracking/AntiTrackingLog.h b/toolkit/components/antitracking/AntiTrackingLog.h new file mode 100644 index 0000000000..09d4828c44 --- /dev/null +++ b/toolkit/components/antitracking/AntiTrackingLog.h @@ -0,0 +1,66 @@ +/* -*- 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/. */ + +#ifndef mozilla_antitrackinglog_h +#define mozilla_antitrackinglog_h + +#include "mozilla/Logging.h" +#include "nsString.h" + +namespace mozilla { + +extern LazyLogModule gAntiTrackingLog; +static const nsCString::size_type sMaxSpecLength = 128; + +#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("(null)"_ns); \ + if (uri) { \ + _specStr = (uri)->GetSpecOrDefault(); \ + } \ + _specStr.Truncate(std::min(_specStr.Length(), sMaxSpecLength)); \ + 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("(null)"_ns); \ + if (uri1) { \ + _specStr1 = (uri1)->GetSpecOrDefault(); \ + } \ + _specStr1.Truncate(std::min(_specStr1.Length(), sMaxSpecLength)); \ + const char* _spec1 = _specStr1.get(); \ + nsAutoCString _specStr2("(null)"_ns); \ + if (uri2) { \ + _specStr2 = (uri2)->GetSpecOrDefault(); \ + } \ + _specStr2.Truncate(std::min(_specStr2.Length(), sMaxSpecLength)); \ + const char* _spec2 = _specStr2.get(); \ + LOG(format); \ + } \ + PR_END_MACRO + +#define LOG_PRIN(format, principal) \ + PR_BEGIN_MACRO \ + if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) { \ + nsAutoCString _specStr("(null)"_ns); \ + if (principal) { \ + (principal)->GetAsciiSpec(_specStr); \ + } \ + _specStr.Truncate(std::min(_specStr.Length(), sMaxSpecLength)); \ + const char* _spec = _specStr.get(); \ + LOG(format); \ + } \ + PR_END_MACRO +} // namespace mozilla + +#endif // mozilla_antitrackinglog_h diff --git a/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.cpp b/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.cpp new file mode 100644 index 0000000000..274a8edbfb --- /dev/null +++ b/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.cpp @@ -0,0 +1,404 @@ +/* -*- 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 "AntiTrackingRedirectHeuristic.h" +#include "ContentBlockingAllowList.h" +#include "ContentBlockingUserInteraction.h" +#include "StorageAccessAPIHelper.h" + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/net/UrlClassifierCommon.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIClassifiedChannel.h" +#include "nsICookieService.h" +#include "nsIHttpChannel.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIScriptError.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsScriptSecurityManager.h" + +namespace mozilla { + +namespace { + +// The helper function to check if we need to check the redirect heuristic for +// ETP later when we know the classification flags of the new channel. This +// check is from the perspective of the old channel. We don't check for the new +// channel because the classification flags are not ready yet when we call this +// function. +bool ShouldCheckRedirectHeuristicETP(nsIChannel* aOldChannel, nsIURI* aOldURI, + nsIPrincipal* aOldPrincipal) { + nsCOMPtr<nsIClassifiedChannel> oldClassifiedChannel = + do_QueryInterface(aOldChannel); + + if (!oldClassifiedChannel) { + LOG_SPEC(("Ignoring the redirect from %s because there is no " + "nsIClassifiedChannel interface", + _spec), + aOldURI); + return false; + } + + nsCOMPtr<nsILoadInfo> oldLoadInfo = aOldChannel->LoadInfo(); + MOZ_ASSERT(oldLoadInfo); + + bool allowedByPreviousRedirect = + oldLoadInfo->GetAllowListFutureDocumentsCreatedFromThisRedirectChain(); + + uint32_t oldClassificationFlags = + oldClassifiedChannel->GetFirstPartyClassificationFlags(); + + // We will skip this check if we have granted storage access before so that we + // can grant the storage access to the rest of the chain. + if (!net::UrlClassifierCommon::IsTrackingClassificationFlag( + oldClassificationFlags, NS_UsePrivateBrowsing(aOldChannel)) && + !allowedByPreviousRedirect) { + // This is not a tracking -> non-tracking redirect. + LOG_SPEC(("Ignoring the redirect from %s because it's not tracking to " + "non-tracking.", + _spec), + aOldURI); + return false; + } + + if (!ContentBlockingUserInteraction::Exists(aOldPrincipal)) { + LOG_SPEC(("Ignoring redirect for %s because no user-interaction on " + "tracker", + _spec), + aOldURI); + return false; + } + + return true; +} + +// The helper function to decide and set the storage access after we know the +// classification flags of the new channel. +bool ShouldRedirectHeuristicApplyETP(nsIChannel* aNewChannel, nsIURI* aNewURI) { + nsCOMPtr<nsIClassifiedChannel> newClassifiedChannel = + do_QueryInterface(aNewChannel); + + if (!aNewChannel) { + LOG_SPEC(("Ignoring the redirect to %s because there is no " + "nsIClassifiedChannel interface", + _spec), + aNewURI); + return false; + } + + // We're looking at the first-party classification flags because we're + // interested in first-party redirects. + uint32_t newClassificationFlags = + newClassifiedChannel->GetFirstPartyClassificationFlags(); + + if (net::UrlClassifierCommon::IsTrackingClassificationFlag( + newClassificationFlags, NS_UsePrivateBrowsing(aNewChannel))) { + // This is not a tracking -> non-tracking redirect. + LOG_SPEC(("Ignoring the redirect to %s because it's not tracking to " + "non-tracking.", + _spec), + aNewURI); + return false; + } + + return true; +} + +bool ShouldRedirectHeuristicApply(nsIChannel* aNewChannel, nsIURI* aNewURI) { + nsCOMPtr<nsILoadInfo> newLoadInfo = aNewChannel->LoadInfo(); + MOZ_ASSERT(newLoadInfo); + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + + nsresult rv = + newLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't obtain the cookieJarSetting from the channel")); + return false; + } + + uint32_t cookieBehavior = cookieJarSettings->GetCookieBehavior(); + if (cookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || + cookieBehavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { + return ShouldRedirectHeuristicApplyETP(aNewChannel, aNewURI); + } + + LOG(( + "Heuristic doesn't apply because the cookieBehavior doesn't require it")); + return false; +} + +bool ShouldCheckRedirectHeuristic(nsIChannel* aOldChannel, nsIURI* aOldURI, + nsIPrincipal* aOldPrincipal) { + nsCOMPtr<nsILoadInfo> oldLoadInfo = aOldChannel->LoadInfo(); + MOZ_ASSERT(oldLoadInfo); + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + + nsresult rv = + oldLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't obtain the cookieJarSettings from the old channel")); + return false; + } + + uint32_t cookieBehavior = cookieJarSettings->GetCookieBehavior(); + if (cookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || + cookieBehavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { + return ShouldCheckRedirectHeuristicETP(aOldChannel, aOldURI, aOldPrincipal); + } + + LOG( + ("Heuristic doesn't be needed because the cookieBehavior doesn't require " + "it")); + return false; +} + +} // namespace + +void PrepareForAntiTrackingRedirectHeuristic(nsIChannel* aOldChannel, + nsIURI* aOldURI, + nsIChannel* aNewChannel, + nsIURI* aNewURI) { + MOZ_ASSERT(aOldChannel); + MOZ_ASSERT(aOldURI); + MOZ_ASSERT(aNewChannel); + MOZ_ASSERT(aNewURI); + + // This heuristic works only on the parent process. + if (!XRE_IsParentProcess()) { + return; + } + + if (!StaticPrefs::privacy_antitracking_enableWebcompat() || + !StaticPrefs::privacy_restrict3rdpartystorage_heuristic_redirect()) { + return; + } + + nsCOMPtr<nsIHttpChannel> oldChannel = do_QueryInterface(aOldChannel); + if (!oldChannel) { + return; + } + + nsCOMPtr<nsIHttpChannel> newChannel = do_QueryInterface(aNewChannel); + if (!newChannel) { + return; + } + + LOG_SPEC2(("Preparing redirect-heuristic for the redirect %s -> %s", _spec1, + _spec2), + aOldURI, aNewURI); + + nsCOMPtr<nsILoadInfo> oldLoadInfo = aOldChannel->LoadInfo(); + MOZ_ASSERT(oldLoadInfo); + + nsCOMPtr<nsILoadInfo> newLoadInfo = aNewChannel->LoadInfo(); + MOZ_ASSERT(newLoadInfo); + + // We need to clear the flag first because the new loadInfo was cloned from + // the old loadInfo. + newLoadInfo->SetNeedForCheckingAntiTrackingHeuristic(false); + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = + oldLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the cookieJarSettings")); + return; + } + + int32_t behavior = cookieJarSettings->GetCookieBehavior(); + + if (!cookieJarSettings->GetRejectThirdPartyContexts()) { + 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); + + ExtContentPolicyType contentType = + oldLoadInfo->GetExternalContentPolicyType(); + if (contentType != ExtContentPolicy::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; + } + + if (ContentBlockingAllowList::Check(newChannel)) { + return; + } + + nsIScriptSecurityManager* ssm = + nsScriptSecurityManager::GetScriptSecurityManager(); + MOZ_ASSERT(ssm); + + nsCOMPtr<nsIPrincipal> oldPrincipal; + + const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& chain = + oldLoadInfo->RedirectChain(); + + if (oldLoadInfo->GetAllowListFutureDocumentsCreatedFromThisRedirectChain() && + !chain.IsEmpty()) { + rv = chain[0]->GetPrincipal(getter_AddRefs(oldPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't obtain the principal from the redirect chain")); + return; + } + } else { + rv = ssm->GetChannelResultPrincipal(aOldChannel, + getter_AddRefs(oldPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't obtain the principal from the previous channel")); + return; + } + } + + newLoadInfo->SetNeedForCheckingAntiTrackingHeuristic( + ShouldCheckRedirectHeuristic(aOldChannel, aOldURI, oldPrincipal)); +} + +void FinishAntiTrackingRedirectHeuristic(nsIChannel* aNewChannel, + nsIURI* aNewURI) { + MOZ_ASSERT(aNewChannel); + MOZ_ASSERT(aNewURI); + + // This heuristic works only on the parent process. + if (!XRE_IsParentProcess()) { + return; + } + + if (!StaticPrefs::privacy_antitracking_enableWebcompat() || + !StaticPrefs::privacy_restrict3rdpartystorage_heuristic_redirect()) { + return; + } + + nsCOMPtr<nsIHttpChannel> newChannel = do_QueryInterface(aNewChannel); + if (!newChannel) { + return; + } + + LOG_SPEC(("Finishing redirect-heuristic for the redirect to %s", _spec), + aNewURI); + + nsCOMPtr<nsILoadInfo> newLoadInfo = newChannel->LoadInfo(); + MOZ_ASSERT(newLoadInfo); + + // Bailing out early if there is no need to do the heuristic. + if (!newLoadInfo->GetNeedForCheckingAntiTrackingHeuristic()) { + return; + } + + if (!ShouldRedirectHeuristicApply(aNewChannel, aNewURI)) { + return; + } + + nsIScriptSecurityManager* ssm = + nsScriptSecurityManager::GetScriptSecurityManager(); + MOZ_ASSERT(ssm); + + const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& chain = + newLoadInfo->RedirectChain(); + + if (chain.IsEmpty()) { + LOG(("Can't obtain the redirect chain")); + return; + } + + nsCOMPtr<nsIPrincipal> oldPrincipal; + uint32_t idx = + newLoadInfo->GetAllowListFutureDocumentsCreatedFromThisRedirectChain() + ? 0 + : chain.Length() - 1; + + nsresult rv = chain[idx]->GetPrincipal(getter_AddRefs(oldPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't obtain the principal from the redirect chain")); + return; + } + + nsCOMPtr<nsIPrincipal> newPrincipal; + rv = + ssm->GetChannelResultPrincipal(aNewChannel, getter_AddRefs(newPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't obtain the principal from the new channel")); + return; + } + + if (oldPrincipal->Equals(newPrincipal)) { + LOG(("No permission needed for same principals.")); + return; + } + + nsAutoCString oldOrigin; + rv = oldPrincipal->GetOriginNoSuffix(oldOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the origin from the old Principal")); + return; + } + + nsAutoCString newOrigin; + rv = newPrincipal->GetOriginNoSuffix(newOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the origin from the new Principal")); + return; + } + + LOG(("Adding a first-party storage exception for %s...", newOrigin.get())); + + LOG(("Saving the permission: oldOrigin=%s, grantedOrigin=%s", oldOrigin.get(), + newOrigin.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<nsString, 2> params = {NS_ConvertUTF8toUTF16(newOrigin), + NS_ConvertUTF8toUTF16(oldOrigin)}; + rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eNECKO_PROPERTIES, "CookieAllowedForOriginByHeuristic", + params, errorText); + if (NS_SUCCEEDED(rv)) { + nsContentUtils::ReportToConsoleByWindowID( + errorText, nsIScriptError::warningFlag, ANTITRACKING_CONSOLE_CATEGORY, + innerWindowID); + } + + Telemetry::AccumulateCategorical( + Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageGranted); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::Redirect); + + // We don't care about this promise because the operation is actually sync. + RefPtr<StorageAccessAPIHelper::ParentAccessGrantPromise> promise = + StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess( + newPrincipal, oldPrincipal, + StorageAccessAPIHelper::StorageAccessPromptChoices::eAllow, false, + StaticPrefs::privacy_restrict3rdpartystorage_expiration_redirect()); + Unused << promise; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.h b/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.h new file mode 100644 index 0000000000..6a6ccbab0c --- /dev/null +++ b/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.h @@ -0,0 +1,34 @@ +/* -*- 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/. */ + +#ifndef mozilla_antitrackingredirectheuristic_h +#define mozilla_antitrackingredirectheuristic_h + +class nsIChannel; +class nsIURI; + +namespace mozilla { + +// This function will be called when we know we are about to perform a http +// redirect. It will check if we need to perform the AntiTracking redirect +// heuristic from the old channel perspective. We cannot know the classification +// flags of the new channel at the point. So, we will save the result in the +// loadInfo in order to finish the heuristic once the classification flags is +// ready. +void PrepareForAntiTrackingRedirectHeuristic(nsIChannel* aOldChannel, + nsIURI* aOldURI, + nsIChannel* aNewChannel, + nsIURI* aNewURI); + +// This function will be called once the classification flags of the new channel +// is known. It will check and perform the AntiTracking redirect heuristic +// according to the flags and the result from previous preparation. +void FinishAntiTrackingRedirectHeuristic(nsIChannel* aNewChannel, + nsIURI* aNewURI); + +} // namespace mozilla + +#endif // mozilla_antitrackingredirectheuristic_h diff --git a/toolkit/components/antitracking/AntiTrackingUtils.cpp b/toolkit/components/antitracking/AntiTrackingUtils.cpp new file mode 100644 index 0000000000..2c530dc3da --- /dev/null +++ b/toolkit/components/antitracking/AntiTrackingUtils.cpp @@ -0,0 +1,1045 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AntiTrackingUtils.h" + +#include "AntiTrackingLog.h" +#include "HttpBaseChannel.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Components.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/PermissionManager.h" +#include "mozIThirdPartyUtil.h" +#include "nsEffectiveTLDService.h" +#include "nsGlobalWindowInner.h" +#include "nsIChannel.h" +#include "nsICookieService.h" +#include "nsIHttpChannel.h" +#include "nsIPermission.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsQueryObject.h" +#include "nsRFPService.h" +#include "nsSandboxFlags.h" +#include "nsScriptSecurityManager.h" +#include "PartitioningExceptionList.h" + +#define ANTITRACKING_PERM_KEY "3rdPartyStorage" +#define ANTITRACKING_FRAME_PERM_KEY "3rdPartyFrameStorage" + +using namespace mozilla; +using namespace mozilla::dom; + +/* static */ already_AddRefed<nsPIDOMWindowInner> +AntiTrackingUtils::GetInnerWindow(BrowsingContext* aBrowsingContext) { + MOZ_ASSERT(aBrowsingContext); + + nsCOMPtr<nsPIDOMWindowOuter> outer = aBrowsingContext->GetDOMWindow(); + if (!outer) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowInner> inner = outer->GetCurrentInnerWindow(); + return inner.forget(); +} + +/* static */ already_AddRefed<nsPIDOMWindowOuter> +AntiTrackingUtils::GetTopWindow(nsPIDOMWindowInner* aWindow) { + Document* document = aWindow->GetExtantDoc(); + if (!document) { + return nullptr; + } + + nsIChannel* channel = document->GetChannel(); + if (!channel) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowOuter> pwin = + aWindow->GetBrowsingContext()->Top()->GetDOMWindow(); + + if (!pwin) { + return nullptr; + } + + return pwin.forget(); +} + +/* static */ +already_AddRefed<nsIURI> AntiTrackingUtils::MaybeGetDocumentURIBeingLoaded( + nsIChannel* aChannel) { + nsCOMPtr<nsIURI> 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 = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uriBeingLoaded)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + } + return uriBeingLoaded.forget(); +} + +// static +void AntiTrackingUtils::CreateStoragePermissionKey( + const nsACString& aTrackingOrigin, nsACString& aPermissionKey) { + MOZ_ASSERT(aPermissionKey.IsEmpty()); + + static const nsLiteralCString prefix = + nsLiteralCString(ANTITRACKING_PERM_KEY "^"); + + aPermissionKey.SetCapacity(prefix.Length() + aTrackingOrigin.Length()); + aPermissionKey.Append(prefix); + aPermissionKey.Append(aTrackingOrigin); +} + +// static +bool AntiTrackingUtils::CreateStoragePermissionKey(nsIPrincipal* aPrincipal, + nsACString& aKey) { + if (!aPrincipal) { + return false; + } + + nsAutoCString origin; + nsresult rv = aPrincipal->GetOriginNoSuffix(origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + CreateStoragePermissionKey(origin, aKey); + return true; +} + +// static +void AntiTrackingUtils::CreateStorageFramePermissionKey( + const nsACString& aTrackingSite, nsACString& aPermissionKey) { + MOZ_ASSERT(aPermissionKey.IsEmpty()); + + static const nsLiteralCString prefix = + nsLiteralCString(ANTITRACKING_FRAME_PERM_KEY "^"); + + aPermissionKey.SetCapacity(prefix.Length() + aTrackingSite.Length()); + aPermissionKey.Append(prefix); + aPermissionKey.Append(aTrackingSite); +} + +// static +bool AntiTrackingUtils::CreateStorageFramePermissionKey( + nsIPrincipal* aPrincipal, nsACString& aKey) { + MOZ_ASSERT(aPrincipal); + + nsAutoCString site; + nsresult rv = aPrincipal->GetSiteOriginNoSuffix(site); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + CreateStorageFramePermissionKey(site, aKey); + return true; +} + +// static +bool AntiTrackingUtils::CreateStorageRequestPermissionKey( + nsIURI* aURI, nsACString& aPermissionKey) { + MOZ_ASSERT(aPermissionKey.IsEmpty()); + RefPtr<nsEffectiveTLDService> eTLDService = + nsEffectiveTLDService::GetInstance(); + if (!eTLDService) { + return false; + } + nsCString site; + nsresult rv = eTLDService->GetSite(aURI, site); + if (NS_FAILED(rv)) { + return false; + } + static const nsLiteralCString prefix = + nsLiteralCString("AllowStorageAccessRequest^"); + aPermissionKey.SetCapacity(prefix.Length() + site.Length()); + aPermissionKey.Append(prefix); + aPermissionKey.Append(site); + return true; +} + +// static +bool AntiTrackingUtils::IsStorageAccessPermission(nsIPermission* aPermission, + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aPermission); + MOZ_ASSERT(aPrincipal); + + // The permission key may belong either to a tracking origin on the same + // origin as the granted origin, or on another origin as the granted origin + // (for example when a tracker in a third-party context uses window.open to + // open another origin where that second origin would be the granted origin.) + // But even in the second case, the type of the permission would still be + // formed by concatenating the granted origin to the end of the type name + // (see CreatePermissionKey). Therefore, we pass in the same argument to + // both tracking origin and granted origin here in order to compute the + // shorter permission key and will then do a prefix match on the type of the + // input permission to see if it is a storage access permission or not. + nsAutoCString permissionKey; + bool result = CreateStoragePermissionKey(aPrincipal, permissionKey); + if (NS_WARN_IF(!result)) { + return false; + } + + nsAutoCString type; + nsresult rv = aPermission->GetType(type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return StringBeginsWith(type, permissionKey); +} + +// static +Maybe<size_t> AntiTrackingUtils::CountSitesAllowStorageAccess( + nsIPrincipal* aPrincipal) { + PermissionManager* permManager = PermissionManager::GetInstance(); + if (NS_WARN_IF(!permManager)) { + return Nothing(); + } + + nsAutoCString prefix; + AntiTrackingUtils::CreateStoragePermissionKey(aPrincipal, prefix); + nsAutoCString framePrefix; + AntiTrackingUtils::CreateStorageFramePermissionKey(aPrincipal, framePrefix); + + using Permissions = nsTArray<RefPtr<nsIPermission>>; + Permissions perms; + nsresult rv = permManager->GetAllWithTypePrefix(prefix, perms); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + Permissions framePermissions; + rv = permManager->GetAllWithTypePrefix(framePrefix, framePermissions); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + if (!perms.AppendElements(framePermissions, fallible)) { + return Nothing(); + } + + // Create a set of unique origins + using Sites = nsTArray<nsCString>; + Sites sites; + + // Iterate over all permissions that have a prefix equal to the expected + // permission we are looking for. This includes two things we need to remove: + // duplicates and origin strings that have a prefix of aPrincipal's origin + // string, e.g. https://example.company when aPrincipal is + // https://example.com. + for (const auto& perm : perms) { + nsAutoCString type; + rv = perm->GetType(type); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + // Let's make sure that we're not looking at a permission for + // https://exampletracker.company when we mean to look for the + // permission for https://exampletracker.com! + if (type != prefix && type != framePrefix) { + continue; + } + + nsCOMPtr<nsIPrincipal> principal; + rv = perm->GetPrincipal(getter_AddRefs(principal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + nsAutoCString site; + rv = principal->GetSiteOrigin(site); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + ToLowerCase(site); + + // Append if it would not be a duplicate + if (sites.IndexOf(site) == Sites::NoIndex) { + sites.AppendElement(site); + } + } + + return Some(sites.Length()); +} + +// static +bool AntiTrackingUtils::CheckStoragePermission(nsIPrincipal* aPrincipal, + const nsAutoCString& aType, + bool aIsInPrivateBrowsing, + uint32_t* aRejectedReason, + uint32_t aBlockedReason) { + PermissionManager* permManager = PermissionManager::GetInstance(); + if (NS_WARN_IF(!permManager)) { + LOG(("Failed to obtain the permission manager")); + return false; + } + + uint32_t result = 0; + if (aIsInPrivateBrowsing) { + LOG_PRIN(("Querying the permissions for private modei looking for a " + "permission of type %s for %s", + aType.get(), _spec), + aPrincipal); + if (!permManager->PermissionAvailable(aPrincipal, aType)) { + LOG( + ("Permission isn't available for this principal in the current " + "process")); + return false; + } + nsTArray<RefPtr<nsIPermission>> permissions; + nsresult rv = permManager->GetAllForPrincipal(aPrincipal, permissions); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Failed to get the list of permissions")); + return false; + } + + bool found = false; + for (const auto& permission : permissions) { + if (!permission) { + LOG(("Couldn't get the permission for unknown reasons")); + continue; + } + + nsAutoCString permissionType; + if (NS_SUCCEEDED(permission->GetType(permissionType)) && + permissionType != aType) { + LOG(("Non-matching permission type: %s", aType.get())); + continue; + } + + uint32_t capability = 0; + if (NS_SUCCEEDED(permission->GetCapability(&capability)) && + capability != nsIPermissionManager::ALLOW_ACTION) { + LOG(("Non-matching permission capability: %d", capability)); + continue; + } + + uint32_t expirationType = 0; + if (NS_SUCCEEDED(permission->GetExpireType(&expirationType)) && + expirationType != nsIPermissionManager ::EXPIRE_SESSION) { + LOG(("Non-matching permission expiration type: %d", expirationType)); + continue; + } + + int64_t expirationTime = 0; + if (NS_SUCCEEDED(permission->GetExpireTime(&expirationTime)) && + expirationTime != 0) { + LOG(("Non-matching permission expiration time: %" PRId64, + expirationTime)); + continue; + } + + LOG(("Found a matching permission")); + found = true; + break; + } + + if (!found) { + if (aRejectedReason) { + *aRejectedReason = aBlockedReason; + } + return false; + } + } else { + nsresult rv = permManager->TestPermissionWithoutDefaultsFromPrincipal( + aPrincipal, aType, &result); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Failed to test the permission")); + return false; + } + + LOG_PRIN( + ("Testing permission type %s for %s resulted in %d (%s)", aType.get(), + _spec, int(result), + result == nsIPermissionManager::ALLOW_ACTION ? "success" : "failure"), + aPrincipal); + + if (result != nsIPermissionManager::ALLOW_ACTION) { + if (aRejectedReason) { + *aRejectedReason = aBlockedReason; + } + return false; + } + } + + return true; +} + +/* static */ +nsresult AntiTrackingUtils::TestStoragePermissionInParent( + nsIPrincipal* aTopPrincipal, nsIPrincipal* aPrincipal, uint32_t* aResult) { + NS_ENSURE_ARG(aResult); + *aResult = nsIPermissionManager::UNKNOWN_ACTION; + NS_ENSURE_ARG(aTopPrincipal); + NS_ENSURE_ARG(aPrincipal); + + nsCOMPtr<nsIPermissionManager> permMgr = + components::PermissionManager::Service(); + NS_ENSURE_TRUE(permMgr, NS_ERROR_FAILURE); + + // Build the permission keys + nsAutoCString requestPermissionKey; + bool success = AntiTrackingUtils::CreateStoragePermissionKey( + aPrincipal, requestPermissionKey); + NS_ENSURE_TRUE(success, NS_ERROR_FAILURE); + + nsAutoCString requestFramePermissionKey; + success = AntiTrackingUtils::CreateStorageFramePermissionKey( + aPrincipal, requestFramePermissionKey); + NS_ENSURE_TRUE(success, NS_ERROR_FAILURE); + + // Test the permission + uint32_t access = nsIPermissionManager::UNKNOWN_ACTION; + nsresult rv = permMgr->TestPermissionFromPrincipal( + aTopPrincipal, requestPermissionKey, &access); + NS_ENSURE_SUCCESS(rv, rv); + + if (access != nsIPermissionManager::UNKNOWN_ACTION) { + *aResult = access; + return NS_OK; + } + + uint32_t frameAccess = nsIPermissionManager::UNKNOWN_ACTION; + rv = permMgr->TestPermissionFromPrincipal( + aTopPrincipal, requestFramePermissionKey, &frameAccess); + NS_ENSURE_SUCCESS(rv, rv); + + *aResult = frameAccess; + return NS_OK; +} + +/* static */ +nsILoadInfo::StoragePermissionState +AntiTrackingUtils::GetStoragePermissionStateInParent(nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + + auto policyType = loadInfo->GetExternalContentPolicyType(); + + // The channel is for the document load of the top-level window. The top-level + // window should always has 'hasStoragePermission' flag as false. So, we can + // return here directly. + if (policyType == ExtContentPolicy::TYPE_DOCUMENT) { + return nsILoadInfo::NoStoragePermission; + } + + nsresult rv = + loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsILoadInfo::NoStoragePermission; + } + + int32_t cookieBehavior = cookieJarSettings->GetCookieBehavior(); + + // We only need to check the storage permission if the cookie behavior is + // BEHAVIOR_REJECT_TRACKER, BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN or + // BEHAVIOR_REJECT_FOREIGN with exceptions. Because ContentBlocking wouldn't + // update or check the storage permission if the cookie behavior is not + // belongs to these three. + if (!net::CookieJarSettings::IsRejectThirdPartyContexts(cookieBehavior)) { + return nsILoadInfo::NoStoragePermission; + } + + RefPtr<BrowsingContext> bc; + rv = loadInfo->GetTargetBrowsingContext(getter_AddRefs(bc)); + if (NS_WARN_IF(NS_FAILED(rv)) || !bc) { + return nsILoadInfo::NoStoragePermission; + } + + uint64_t targetWindowId = GetTopLevelAntiTrackingWindowId(bc); + nsCOMPtr<nsIPrincipal> targetPrincipal; + + if (targetWindowId) { + RefPtr<WindowGlobalParent> wgp = + WindowGlobalParent::GetByInnerWindowId(targetWindowId); + + if (NS_WARN_IF(!wgp)) { + return nsILoadInfo::NoStoragePermission; + } + + targetPrincipal = wgp->DocumentPrincipal(); + } else { + // We try to use the loading principal if there is no AntiTrackingWindowId. + targetPrincipal = loadInfo->GetLoadingPrincipal(); + } + + if (!targetPrincipal) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + + if (httpChannel) { + // We don't have a loading principal, let's see if this is a document + // channel which belongs to a top-level window. + bool isDocument = false; + rv = httpChannel->GetIsMainDocumentChannel(&isDocument); + if (NS_SUCCEEDED(rv) && isDocument) { + nsIScriptSecurityManager* ssm = + nsScriptSecurityManager::GetScriptSecurityManager(); + Unused << ssm->GetChannelResultPrincipal( + aChannel, getter_AddRefs(targetPrincipal)); + } + } + } + + // Let's use the triggering principal if we still have nothing on the hand. + if (!targetPrincipal) { + targetPrincipal = loadInfo->TriggeringPrincipal(); + } + + // Cannot get the target principal, bail out. + if (NS_WARN_IF(!targetPrincipal)) { + return nsILoadInfo::NoStoragePermission; + } + + nsCOMPtr<nsIURI> trackingURI; + rv = aChannel->GetURI(getter_AddRefs(trackingURI)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsILoadInfo::NoStoragePermission; + } + + nsCOMPtr<nsIPrincipal> trackingPrincipal = + BasePrincipal::CreateContentPrincipal(trackingURI, + loadInfo->GetOriginAttributes()); + + if (IsThirdPartyChannel(aChannel)) { + nsAutoCString targetOrigin; + nsAutoCString trackingOrigin; + if (NS_FAILED(targetPrincipal->GetOriginNoSuffix(targetOrigin)) || + NS_FAILED(trackingPrincipal->GetOriginNoSuffix(trackingOrigin))) { + return nsILoadInfo::NoStoragePermission; + } + + if (PartitioningExceptionList::Check(targetOrigin, trackingOrigin)) { + return nsILoadInfo::StoragePermissionAllowListed; + } + } + + nsAutoCString type; + AntiTrackingUtils::CreateStoragePermissionKey(trackingPrincipal, type); + + uint32_t unusedReason = 0; + + if (AntiTrackingUtils::CheckStoragePermission(targetPrincipal, type, + NS_UsePrivateBrowsing(aChannel), + &unusedReason, unusedReason)) { + return nsILoadInfo::HasStoragePermission; + } + + WindowContext* wc = bc->GetCurrentWindowContext(); + if (!wc) { + return nsILoadInfo::NoStoragePermission; + } + WindowGlobalParent* wgp = wc->Canonical(); + if (!wgp) { + return nsILoadInfo::NoStoragePermission; + } + nsIPrincipal* framePrincipal = wgp->DocumentPrincipal(); + if (!framePrincipal) { + return nsILoadInfo::NoStoragePermission; + } + + if (policyType == ExtContentPolicy::TYPE_SUBDOCUMENT) { + // For loads of framed documents, we only use storage access + // if the load is the result of a same-origin, self-initiated + // navigation of the frame. + uint64_t targetWindowIdNoTop = bc->GetCurrentInnerWindowId(); + uint64_t triggeringWindowId; + rv = loadInfo->GetTriggeringWindowId(&triggeringWindowId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsILoadInfo::NoStoragePermission; + } + bool triggeringWindowHasStorageAccess; + rv = + loadInfo->GetTriggeringStorageAccess(&triggeringWindowHasStorageAccess); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsILoadInfo::NoStoragePermission; + } + RefPtr<net::HttpBaseChannel> httpChannel = do_QueryObject(aChannel); + + if (targetWindowIdNoTop == triggeringWindowId && + triggeringWindowHasStorageAccess && + trackingPrincipal->Equals(framePrincipal) && httpChannel && + !httpChannel->HasRedirectTaintedOrigin()) { + return nsILoadInfo::HasStoragePermission; + } + } else if (!bc->IsTop()) { + // For subframe resources, check if the document has storage access + // and that the resource being loaded is same-site to the page. + bool isThirdParty = true; + nsresult rv = framePrincipal->IsThirdPartyURI(trackingURI, &isThirdParty); + if (NS_SUCCEEDED(rv) && wc->GetUsingStorageAccess() && !isThirdParty) { + return nsILoadInfo::HasStoragePermission; + } + } + + return nsILoadInfo::NoStoragePermission; +} + +uint64_t AntiTrackingUtils::GetTopLevelAntiTrackingWindowId( + BrowsingContext* aBrowsingContext) { + MOZ_ASSERT(aBrowsingContext); + + RefPtr<WindowContext> winContext = + aBrowsingContext->GetCurrentWindowContext(); + if (!winContext || winContext->GetCookieBehavior().isNothing()) { + return 0; + } + + // Do not check BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN her because when + // a third-party subresource is inside the main frame, we need to return the + // top-level window id to partition its cookies correctly. + uint32_t behavior = *winContext->GetCookieBehavior(); + if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER && + aBrowsingContext->IsTop()) { + return 0; + } + + return aBrowsingContext->Top()->GetCurrentInnerWindowId(); +} + +uint64_t AntiTrackingUtils::GetTopLevelStorageAreaWindowId( + BrowsingContext* aBrowsingContext) { + MOZ_ASSERT(aBrowsingContext); + + if (Document::StorageAccessSandboxed(aBrowsingContext->GetSandboxFlags())) { + return 0; + } + + BrowsingContext* parentBC = aBrowsingContext->GetParent(); + if (!parentBC) { + // No parent browsing context available! + return 0; + } + + if (!parentBC->IsTop()) { + return 0; + } + + return parentBC->GetCurrentInnerWindowId(); +} + +/* static */ +already_AddRefed<nsIPrincipal> AntiTrackingUtils::GetPrincipal( + BrowsingContext* aBrowsingContext) { + MOZ_ASSERT(aBrowsingContext); + + nsCOMPtr<nsIPrincipal> principal; + + if (XRE_IsContentProcess()) { + // Passing an out-of-process browsing context in child processes to + // this API won't get any result, so just assert. + MOZ_ASSERT(aBrowsingContext->IsInProcess()); + + nsPIDOMWindowOuter* outer = aBrowsingContext->GetDOMWindow(); + if (NS_WARN_IF(!outer)) { + return nullptr; + } + + nsPIDOMWindowInner* inner = outer->GetCurrentInnerWindow(); + if (NS_WARN_IF(!inner)) { + return nullptr; + } + + principal = nsGlobalWindowInner::Cast(inner)->GetPrincipal(); + } else { + WindowGlobalParent* wgp = + aBrowsingContext->Canonical()->GetCurrentWindowGlobal(); + if (NS_WARN_IF(!wgp)) { + return nullptr; + } + + principal = wgp->DocumentPrincipal(); + } + return principal.forget(); +} + +/* static */ +bool AntiTrackingUtils::GetPrincipalAndTrackingOrigin( + BrowsingContext* aBrowsingContext, nsIPrincipal** aPrincipal, + nsACString& aTrackingOrigin) { + MOZ_ASSERT(aBrowsingContext); + + // Passing an out-of-process browsing context in child processes to + // this API won't get any result, so just assert. + MOZ_ASSERT_IF(XRE_IsContentProcess(), aBrowsingContext->IsInProcess()); + + // Let's take the principal and the origin of the tracker. + nsCOMPtr<nsIPrincipal> principal = + AntiTrackingUtils::GetPrincipal(aBrowsingContext); + if (NS_WARN_IF(!principal)) { + return false; + } + + nsresult rv = principal->GetOriginNoSuffix(aTrackingOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + if (aPrincipal) { + principal.forget(aPrincipal); + } + + return true; +}; + +/* static */ +uint32_t AntiTrackingUtils::GetCookieBehavior( + BrowsingContext* aBrowsingContext) { + MOZ_ASSERT(aBrowsingContext); + + RefPtr<dom::WindowContext> win = aBrowsingContext->GetCurrentWindowContext(); + if (!win || win->GetCookieBehavior().isNothing()) { + return nsICookieService::BEHAVIOR_REJECT; + } + + return *win->GetCookieBehavior(); +} + +/* static */ +already_AddRefed<WindowGlobalParent> +AntiTrackingUtils::GetTopWindowExcludingExtensionAccessibleContentFrames( + CanonicalBrowsingContext* aBrowsingContext, nsIURI* aURIBeingLoaded) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aBrowsingContext); + + CanonicalBrowsingContext* bc = aBrowsingContext; + RefPtr<WindowGlobalParent> prev; + while (RefPtr<WindowGlobalParent> parent = bc->GetParentWindowContext()) { + CanonicalBrowsingContext* parentBC = parent->BrowsingContext(); + + nsIPrincipal* parentPrincipal = parent->DocumentPrincipal(); + nsIURI* uri = prev ? prev->GetDocumentURI() : aURIBeingLoaded; + + // If the new parent has permission to load the current page, we're + // at a moz-extension:// frame which has a host permission that allows + // it to load the document that we've loaded. In that case, stop at + // this frame and consider it the top-level frame. + if (uri && + BasePrincipal::Cast(parentPrincipal)->AddonAllowsLoad(uri, true)) { + break; + } + + bc = parentBC; + prev = parent; + } + if (!prev) { + prev = bc->GetCurrentWindowGlobal(); + } + return prev.forget(); +} + +/* static */ +void AntiTrackingUtils::ComputeIsThirdPartyToTopWindow(nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + // When a top-level load is opened by window.open, BrowsingContext from + // LoadInfo is its opener, which may make the third-party caculation code + // below returns an incorrect result. So we use TYPE_DOCUMENT to + // ensure a top-level load is not considered 3rd-party. + auto policyType = loadInfo->GetExternalContentPolicyType(); + if (policyType == ExtContentPolicy::TYPE_DOCUMENT) { + loadInfo->SetIsThirdPartyContextToTopWindow(false); + return; + } + + RefPtr<BrowsingContext> bc; + loadInfo->GetBrowsingContext(getter_AddRefs(bc)); + + nsCOMPtr<nsIURI> uri; + Unused << aChannel->GetURI(getter_AddRefs(uri)); + + // In some cases we don't have a browsingContext. For example, in xpcshell + // tests, channels that are used to download images and channels for loading + // worker script. + if (!bc) { + // If the flag was set before, we don't need to compute again. This could + // happen for the channels used to load worker scripts. + // + // Note that we cannot stop computing the flag in general even it has set + // before because sometimes we need to get the up-to-date flag, e.g. + // redirects. + if (static_cast<net::LoadInfo*>(loadInfo.get()) + ->HasIsThirdPartyContextToTopWindowSet()) { + return; + } + + // We turn to check the loading principal if there is no browsing context. + auto* loadingPrincipal = + BasePrincipal::Cast(loadInfo->GetLoadingPrincipal()); + + if (uri && loadingPrincipal) { + bool isThirdParty = true; + nsresult rv = loadingPrincipal->IsThirdPartyURI(uri, &isThirdParty); + + if (NS_SUCCEEDED(rv)) { + loadInfo->SetIsThirdPartyContextToTopWindow(isThirdParty); + } + } + return; + } + + RefPtr<WindowGlobalParent> topWindow = + GetTopWindowExcludingExtensionAccessibleContentFrames(bc->Canonical(), + uri); + + if (NS_WARN_IF(!topWindow)) { + return; + } + + nsCOMPtr<nsIPrincipal> topWindowPrincipal = topWindow->DocumentPrincipal(); + if (topWindowPrincipal && !topWindowPrincipal->GetIsNullPrincipal()) { + auto* basePrin = BasePrincipal::Cast(topWindowPrincipal); + bool isThirdParty = true; + + // For about:blank and about:srcdoc, we can't just compare uri to determine + // whether the page is third-party, so we use channel result principal + // instead. By doing this, an the resource inherits the principal from + // its parent is considered not a third-party. + if (NS_IsAboutBlank(uri) || NS_IsAboutSrcdoc(uri)) { + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (NS_WARN_IF(!ssm)) { + return; + } + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = + ssm->GetChannelResultPrincipal(aChannel, getter_AddRefs(principal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + basePrin->IsThirdPartyPrincipal(principal, &isThirdParty); + } else { + basePrin->IsThirdPartyURI(uri, &isThirdParty); + } + + loadInfo->SetIsThirdPartyContextToTopWindow(isThirdParty); + } +} + +/* static */ +bool AntiTrackingUtils::IsThirdPartyChannel(nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + + // We only care whether the channel is 3rd-party with respect to + // the top-level. + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + return loadInfo->GetIsThirdPartyContextToTopWindow(); +} + +/* static */ +bool AntiTrackingUtils::IsThirdPartyWindow(nsPIDOMWindowInner* aWindow, + nsIURI* aURI) { + MOZ_ASSERT(aWindow); + + // We assume that the window is foreign to the URI by default. + bool thirdParty = true; + + // We will skip checking URIs for about:blank and about:srcdoc because they + // have no domain. So, comparing them will always fail. + if (aURI && !NS_IsAboutBlank(aURI) && !NS_IsAboutSrcdoc(aURI)) { + nsCOMPtr<nsIScriptObjectPrincipal> scriptObjPrin = + do_QueryInterface(aWindow); + if (!scriptObjPrin) { + return thirdParty; + } + + nsCOMPtr<nsIPrincipal> prin = scriptObjPrin->GetPrincipal(); + if (!prin) { + return thirdParty; + } + + // Determine whether aURI is foreign with respect to the current principal. + nsresult rv = prin->IsThirdPartyURI(aURI, &thirdParty); + if (NS_FAILED(rv)) { + return thirdParty; + } + + if (thirdParty) { + return thirdParty; + } + } + + RefPtr<Document> doc = aWindow->GetDoc(); + if (!doc) { + // If we can't get the document from the window, ex, about:blank, fallback + // to use IsThirdPartyWindow check that examine the whole hierarchy. + nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil = + components::ThirdPartyUtil::Service(); + Unused << thirdPartyUtil->IsThirdPartyWindow(aWindow->GetOuterWindow(), + nullptr, &thirdParty); + return thirdParty; + } + + return IsThirdPartyDocument(doc); +} + +/* static */ +bool AntiTrackingUtils::IsThirdPartyDocument(Document* aDocument) { + MOZ_ASSERT(aDocument); + if (!aDocument->GetChannel()) { + // If we can't get the channel from the document, i.e. initial about:blank + // page, we use the browsingContext of the document to check if it's in the + // third-party context. If the browsing context is still not available, we + // will treat the window as third-party. + RefPtr<BrowsingContext> bc = aDocument->GetBrowsingContext(); + return bc ? IsThirdPartyContext(bc) : true; + } + + // We only care whether the channel is 3rd-party with respect to + // the top-level. + nsCOMPtr<nsILoadInfo> loadInfo = aDocument->GetChannel()->LoadInfo(); + return loadInfo->GetIsThirdPartyContextToTopWindow(); +} + +/* static */ +bool AntiTrackingUtils::IsThirdPartyContext(BrowsingContext* aBrowsingContext) { + MOZ_ASSERT(aBrowsingContext); + MOZ_ASSERT(aBrowsingContext->IsInProcess()); + + if (aBrowsingContext->IsTopContent()) { + return false; + } + + // If the top browsing context is not in the same process, it's cross-origin. + if (!aBrowsingContext->Top()->IsInProcess()) { + return true; + } + + nsIDocShell* docShell = aBrowsingContext->GetDocShell(); + if (!docShell) { + return true; + } + Document* doc = docShell->GetExtantDocument(); + if (!doc) { + return true; + } + nsIPrincipal* principal = doc->NodePrincipal(); + + nsIDocShell* topDocShell = aBrowsingContext->Top()->GetDocShell(); + if (!topDocShell) { + return true; + } + Document* topDoc = topDocShell->GetDocument(); + if (!topDoc) { + return true; + } + nsIPrincipal* topPrincipal = topDoc->NodePrincipal(); + + auto* topBasePrin = BasePrincipal::Cast(topPrincipal); + bool isThirdParty = true; + + topBasePrin->IsThirdPartyPrincipal(principal, &isThirdParty); + + return isThirdParty; +} + +/* static */ +nsCString AntiTrackingUtils::GrantedReasonToString( + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason) { + switch (aReason) { + case ContentBlockingNotifier::eOpener: + return "opener"_ns; + case ContentBlockingNotifier::eOpenerAfterUserInteraction: + return "user interaction"_ns; + default: + return "stroage access API"_ns; + } +} + +/* static */ +void AntiTrackingUtils::UpdateAntiTrackingInfoForChannel(nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + + if (!XRE_IsParentProcess()) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + AntiTrackingUtils::ComputeIsThirdPartyToTopWindow(aChannel); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + Unused << loadInfo->SetStoragePermission( + AntiTrackingUtils::GetStoragePermissionStateInParent(aChannel)); + + // Note that we need to put this after computing the IsThirdPartyToTopWindow + // flag because it will be used when getting the granular fingerprinting + // protections. + Maybe<RFPTarget> overriddenFingerprintingSettings = + nsRFPService::GetOverriddenFingerprintingSettingsForChannel(aChannel); + + if (overriddenFingerprintingSettings) { + loadInfo->SetOverriddenFingerprintingSettings( + overriddenFingerprintingSettings.ref()); + } +#ifdef DEBUG + static_cast<mozilla::net::LoadInfo*>(loadInfo.get()) + ->MarkOverriddenFingerprintingSettingsAsSet(); +#endif + + // We only update the IsOnContentBlockingAllowList flag and the partition key + // for the top-level http channel. + // + // The IsOnContentBlockingAllowList is only for http. For other types of + // channels, such as 'file:', there will be no interface to modify this. So, + // we only update it in http channels. + // + // The partition key is computed based on the site, so it's no point to set it + // for channels other than http channels. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + if (!httpChannel || loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + return; + } + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + + // Update the IsOnContentBlockingAllowList flag in the CookieJarSettings + // if this is a top level loading. For sub-document loading, this flag + // would inherit from the parent. + net::CookieJarSettings::Cast(cookieJarSettings) + ->UpdateIsOnContentBlockingAllowList(aChannel); + + // We only need to set FPD for top-level loads. FPD will automatically be + // propagated to non-top level loads via CookieJarSetting. + nsCOMPtr<nsIURI> uri; + Unused << aChannel->GetURI(getter_AddRefs(uri)); + net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri); + + // Generate the fingerprinting randomization key for top-level loads. The key + // will automatically be propagated to sub loads. + auto RFPRandomKey = nsRFPService::GenerateKey(aChannel); + if (RFPRandomKey) { + net::CookieJarSettings::Cast(cookieJarSettings) + ->SetFingerprintingRandomizationKey(RFPRandomKey.ref()); + } +} diff --git a/toolkit/components/antitracking/AntiTrackingUtils.h b/toolkit/components/antitracking/AntiTrackingUtils.h new file mode 100644 index 0000000000..8ed83d0ff7 --- /dev/null +++ b/toolkit/components/antitracking/AntiTrackingUtils.h @@ -0,0 +1,174 @@ +/* -*- 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/. */ + +#ifndef mozilla_antitrackingutils_h +#define mozilla_antitrackingutils_h + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Maybe.h" +#include "nsStringFwd.h" +#include "ContentBlockingNotifier.h" + +#include "nsILoadInfo.h" + +class nsPIDOMWindowInner; +class nsPIDOMWindowOuter; +class nsIChannel; +class nsIPermission; +class nsIPrincipal; +class nsIURI; + +namespace mozilla { +namespace dom { +class BrowsingContext; +class CanonicalBrowsingContext; +class Document; +class WindowGlobalParent; +} // namespace dom + +class AntiTrackingUtils final { + public: + static already_AddRefed<nsPIDOMWindowInner> GetInnerWindow( + dom::BrowsingContext* aBrowsingContext); + + static already_AddRefed<nsPIDOMWindowOuter> GetTopWindow( + nsPIDOMWindowInner* aWindow); + + // Get the current document URI from a document channel as it is being loaded. + static already_AddRefed<nsIURI> MaybeGetDocumentURIBeingLoaded( + nsIChannel* aChannel); + + static void CreateStoragePermissionKey(const nsACString& aTrackingOrigin, + nsACString& aPermissionKey); + + // Given a principal, returns the storage permission key that will be used for + // the principal. Returns true on success. + static bool CreateStoragePermissionKey(nsIPrincipal* aPrincipal, + nsACString& aKey); + + static void CreateStorageFramePermissionKey(const nsACString& aTrackingSite, + nsACString& aPermissionKey); + + // Given a principal, returns the per-frame storage permission key that will + // be used for the principal. Returns true on success. + static bool CreateStorageFramePermissionKey(nsIPrincipal* aPrincipal, + nsACString& aKey); + + // Given and embedded URI, returns the permission for allowing storage access + // requests from that URI's site. This permission is site-scoped in two ways: + // the principal it is stored under and the suffix built from aURI are both + // the Site rather than Origin. + static bool CreateStorageRequestPermissionKey(nsIURI* aURI, + nsACString& aPermissionKey); + + // Returns true if the permission passed in is a storage access permission + // for the passed in principal argument. + static bool IsStorageAccessPermission(nsIPermission* aPermission, + nsIPrincipal* aPrincipal); + + // Returns true if the storage permission is granted for the given principal + // and the storage permission key. + static bool CheckStoragePermission(nsIPrincipal* aPrincipal, + const nsAutoCString& aType, + bool aIsInPrivateBrowsing, + uint32_t* aRejectedReason, + uint32_t aBlockedReason); + + // Returns the number of sites that give this principal's origin storage + // access. + static Maybe<size_t> CountSitesAllowStorageAccess(nsIPrincipal* aPrincipal); + + // Test whether or not there is a storage access permission in aTopPrincipal + // with secondary key for embedee aPrincipal. + static nsresult TestStoragePermissionInParent(nsIPrincipal* aTopPrincipal, + nsIPrincipal* aPrincipal, + uint32_t* aResult); + + // Returns the storage permission state for the given channel. And this is + // meant to be called in the parent process. This only reflects the fact that + // whether the channel has the storage permission. It doesn't take the window + // hierarchy into account. i.e. this will return + // nsILoadInfo::HasStoragePermission even for a nested iframe that has storage + // permission. + static nsILoadInfo::StoragePermissionState GetStoragePermissionStateInParent( + nsIChannel* aChannel); + + // Returns the toplevel inner window id, returns 0 if this is a toplevel + // window. + static uint64_t GetTopLevelAntiTrackingWindowId( + dom::BrowsingContext* aBrowsingContext); + + // Returns the parent inner window id, returns 0 if this or the parent are not + // a toplevel window. This is mainly used to determine the anti-tracking + // storage area. + static uint64_t GetTopLevelStorageAreaWindowId( + dom::BrowsingContext* aBrowsingContext); + + // Returns the principal of the given browsing context. + // This API should only be used either in child processes with an in-process + // browsing context or in the parent process. + static already_AddRefed<nsIPrincipal> GetPrincipal( + dom::BrowsingContext* aBrowsingContext); + + // Returns the principal of the given browsing context and tracking origin. + // This API should only be used either in child processes with an in-process + // browsing context or in the parent process. + static bool GetPrincipalAndTrackingOrigin( + dom::BrowsingContext* aBrowsingContext, nsIPrincipal** aPrincipal, + nsACString& aTrackingOrigin); + + // Retruns the cookie behavior of the given browsingContext, + // return BEHAVIOR_REJECT when fail. + static uint32_t GetCookieBehavior(dom::BrowsingContext* aBrowsingContext); + + // Returns the top-level global window parent. But we would stop at the + // content window which is loaded by addons and consider this window as a top. + // + // Note that this is the parent-process implementation of + // nsGlobalWindowOuter::GetTopExcludingExtensionAccessibleContentFrames + static already_AddRefed<dom::WindowGlobalParent> + GetTopWindowExcludingExtensionAccessibleContentFrames( + dom::CanonicalBrowsingContext* aBrowsingContext, nsIURI* aURIBeingLoaded); + + // Given a channel, compute and set the IsThirdPartyContextToTopWindow for + // this channel. This function is supposed to be called in the parent process. + static void ComputeIsThirdPartyToTopWindow(nsIChannel* aChannel); + + // Given a channel, this function determines if this channel is a third party. + // Note that this function also considers the top-level window. The channel + // will be considered as a third party only when it's a third party to both + // its parent and the top-level window. + static bool IsThirdPartyChannel(nsIChannel* aChannel); + + // Given a window and a URI, this function first determines if the window is + // third-party with respect to the URI. The function returns if it's true. + // Otherwise, it will continue to check if the window is third-party. + static bool IsThirdPartyWindow(nsPIDOMWindowInner* aWindow, nsIURI* aURI); + + // Given a Document, this function determines if this document + // is considered as a third party with respect to the top level context. + // This prefers to use the document's Channel's LoadInfo, but falls back to + // the BrowsingContext. + static bool IsThirdPartyDocument(dom::Document* aDocument); + + // Given a browsing context, this function determines if this browsing context + // is considered as a third party in respect to the top-level context. + static bool IsThirdPartyContext(dom::BrowsingContext* aBrowsingContext); + + static nsCString GrantedReasonToString( + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason); + + /** + * This function updates all the fields used by anti-tracking when a channel + * is opened. We have to do this in the parent to access cross-origin info + * that is not exposed to child processes. + */ + static void UpdateAntiTrackingInfoForChannel(nsIChannel* aChannel); +}; + +} // namespace mozilla + +#endif // mozilla_antitrackingutils_h diff --git a/toolkit/components/antitracking/ContentBlockingAllowList.cpp b/toolkit/components/antitracking/ContentBlockingAllowList.cpp new file mode 100644 index 0000000000..25806cd1e7 --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingAllowList.cpp @@ -0,0 +1,257 @@ +/* -*- 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 "ContentBlockingAllowList.h" + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/PermissionManager.h" +#include "mozilla/ScopeExit.h" +#include "nsContentUtils.h" +#include "nsGlobalWindowInner.h" +#include "nsICookieJarSettings.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsNetUtil.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(ContentBlockingAllowList, nsIContentBlockingAllowList) + +NS_IMETHODIMP +// Wrapper for the static ContentBlockingAllowList::ComputePrincipal method +ContentBlockingAllowList::ComputeContentBlockingAllowListPrincipal( + nsIPrincipal* aDocumentPrincipal, nsIPrincipal** aPrincipal) { + NS_ENSURE_ARG_POINTER(aDocumentPrincipal); + NS_ENSURE_ARG_POINTER(aPrincipal); + + nsCOMPtr<nsIPrincipal> principal; + ContentBlockingAllowList::ComputePrincipal(aDocumentPrincipal, + getter_AddRefs(principal)); + + NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE); + + principal.forget(aPrincipal); + + return NS_OK; +} + +/* static */ bool ContentBlockingAllowList::Check( + nsIPrincipal* aTopWinPrincipal, bool aIsPrivateBrowsing) { + bool isAllowed = false; + nsresult rv = Check(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<uint32_t>(rv))); + } + return false; +} + +/* static */ bool ContentBlockingAllowList::Check( + nsICookieJarSettings* aCookieJarSettings) { + if (!aCookieJarSettings) { + LOG( + ("Could not check the content blocking allow list because the cookie " + "jar settings wasn't available")); + return false; + } + + return aCookieJarSettings->GetIsOnContentBlockingAllowList(); +} + +/* static */ bool ContentBlockingAllowList::Check(nsPIDOMWindowInner* aWindow) { + // TODO: this is a quick fix to ensure that we allow storage permission for + // a chrome window. We should check if there is a better way to do this in + // Bug 1626223. + if (nsGlobalWindowInner::Cast(aWindow)->GetPrincipal() == + nsContentUtils::GetSystemPrincipal()) { + return true; + } + + // We can check the IsOnContentBlockingAllowList flag in the document's + // CookieJarSettings. Because this flag represents the fact that whether the + // top-level document is on the content blocking allow list. And this flag was + // propagated from the top-level as the CookieJarSettings inherits from the + // parent. + RefPtr<dom::Document> doc = nsGlobalWindowInner::Cast(aWindow)->GetDocument(); + + if (!doc) { + LOG( + ("Could not check the content blocking allow list because the document " + "wasn't available")); + return false; + } + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = doc->CookieJarSettings(); + + return ContentBlockingAllowList::Check(cookieJarSettings); +} + +/* static */ bool ContentBlockingAllowList::Check(nsIHttpChannel* aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + + Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + + return ContentBlockingAllowList::Check(cookieJarSettings); +} + +nsresult ContentBlockingAllowList::Check( + nsIPrincipal* aContentBlockingAllowListPrincipal, bool aIsPrivateBrowsing, + bool& aIsAllowListed) { + MOZ_ASSERT(XRE_IsParentProcess()); + aIsAllowListed = false; + + if (!aContentBlockingAllowListPrincipal) { + // Nothing to do! + return NS_OK; + } + + LOG_PRIN(("Deciding whether the user has overridden content blocking for %s", + _spec), + aContentBlockingAllowListPrincipal); + + PermissionManager* permManager = PermissionManager::GetInstance(); + NS_ENSURE_TRUE(permManager, NS_ERROR_FAILURE); + + // Check both the normal mode and private browsing mode user override + // permissions. + std::pair<const nsLiteralCString, bool> types[] = { + {"trackingprotection"_ns, false}, {"trackingprotection-pb"_ns, true}}; + + for (const auto& type : types) { + if (aIsPrivateBrowsing != type.second) { + continue; + } + + uint32_t permissions = nsIPermissionManager::UNKNOWN_ACTION; + nsresult rv = permManager->TestPermissionFromPrincipal( + aContentBlockingAllowListPrincipal, type.first, &permissions); + NS_ENSURE_SUCCESS(rv, rv); + + if (permissions == nsIPermissionManager::ALLOW_ACTION) { + aIsAllowListed = true; + LOG(("Found user override type %s", type.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 ContentBlockingAllowList::ComputePrincipal( + 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; + } + + if (aDocumentPrincipal->SchemeIs("chrome") || + aDocumentPrincipal->SchemeIs("about")) { + returnInputArgument.release(); + *aPrincipal = nullptr; + 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("https://"_ns); + nsAutoCString temp; + nsresult rv = aDocumentPrincipal->GetHostPort(temp); + // view-source URIs will be handled by the next block. + if (NS_FAILED(rv) && !aDocumentPrincipal->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); + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), escaped); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsIPrincipal> principal = BasePrincipal::CreateContentPrincipal( + uri, aDocumentPrincipal->OriginAttributesRef()); + if (NS_WARN_IF(!principal)) { + return; + } + + returnInputArgument.release(); + principal.forget(aPrincipal); +} + +/* static */ void ContentBlockingAllowList::RecomputePrincipal( + 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("https://"_ns); + 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<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), escaped); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(uri, aAttrs); + if (NS_WARN_IF(!principal)) { + return; + } + + returnInputArgument.release(); + principal.forget(aPrincipal); +} diff --git a/toolkit/components/antitracking/ContentBlockingAllowList.h b/toolkit/components/antitracking/ContentBlockingAllowList.h new file mode 100644 index 0000000000..2601173db7 --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingAllowList.h @@ -0,0 +1,57 @@ +/* -*- 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/. */ + +#ifndef mozilla_contentblockingallowlist_h +#define mozilla_contentblockingallowlist_h + +#include "mozilla/dom/BrowsingContext.h" +#include "nsIContentBlockingAllowList.h" + +class nsICookieJarSettings; +class nsIHttpChannel; +class nsIPrincipal; +class nsIURI; +class nsPIDOMWindowInner; + +namespace mozilla { + +class OriginAttributes; +struct ContentBlockingAllowListCache; + +class ContentBlockingAllowList final : public nsIContentBlockingAllowList { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTBLOCKINGALLOWLIST + // Check whether a principal is on the content blocking allow list. + // aPrincipal should be a "content blocking allow list principal". + // This principal can be obtained from the load info object for top-level + // windows. + static nsresult Check(nsIPrincipal* aContentBlockingAllowListPrincipal, + bool aIsPrivateBrowsing, bool& aIsAllowListed); + + static bool Check(nsIHttpChannel* aChannel); + // Utility APIs for ContentBlocking. + static bool Check(nsPIDOMWindowInner* aWindow); + static bool Check(nsIPrincipal* aTopWinPrincipal, bool aIsPrivateBrowsing); + static bool Check(nsICookieJarSettings* aCookieJarSettings); + + // Computes the principal used to check the content blocking allow list for a + // top-level document based on the document principal. This function is used + // right after setting up the document principal. + static void ComputePrincipal(nsIPrincipal* aDocumentPrincipal, + nsIPrincipal** aPrincipal); + + static void RecomputePrincipal(nsIURI* aURIBeingLoaded, + const OriginAttributes& aAttrs, + nsIPrincipal** aPrincipal); + + private: + ~ContentBlockingAllowList() = default; +}; + +} // namespace mozilla + +#endif // mozilla_contentblockingallowlist_h diff --git a/toolkit/components/antitracking/ContentBlockingAllowList.sys.mjs b/toolkit/components/antitracking/ContentBlockingAllowList.sys.mjs new file mode 100644 index 0000000000..af1028083c --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingAllowList.sys.mjs @@ -0,0 +1,111 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +/** + * A helper module to manage the Content Blocking Allow List. + * + * This module provides a couple of utility APIs for adding or + * removing a given browser object to the Content Blocking allow + * list. + */ +export const ContentBlockingAllowList = { + _observingLastPBContext: false, + + _maybeSetupLastPBContextObserver() { + if (!this._observingLastPBContext) { + this._observer = { + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + observe(subject, topic, data) { + if (topic == "last-pb-context-exited") { + Services.perms.removeByType("trackingprotection-pb"); + } + }, + }; + Services.obs.addObserver(this._observer, "last-pb-context-exited", true); + this._observingLastPBContext = true; + } + }, + + _basePrincipalForAntiTrackingCommon(browser) { + let principal = + browser.browsingContext.currentWindowGlobal + ?.contentBlockingAllowListPrincipal; + // We can only use content principals for this purpose. + if (!principal || !principal.isContentPrincipal) { + return null; + } + return principal; + }, + + _permissionTypeFor(browser) { + return lazy.PrivateBrowsingUtils.isBrowserPrivate(browser) + ? "trackingprotection-pb" + : "trackingprotection"; + }, + + _expiryFor(browser) { + return lazy.PrivateBrowsingUtils.isBrowserPrivate(browser) + ? Ci.nsIPermissionManager.EXPIRE_SESSION + : Ci.nsIPermissionManager.EXPIRE_NEVER; + }, + + /** + * Returns false if this module cannot handle the current document loaded in + * the browser object. This can happen for example for about: or file: + * documents. + */ + canHandle(browser) { + return this._basePrincipalForAntiTrackingCommon(browser) != null; + }, + + /** + * Add the given browser object to the Content Blocking allow list. + */ + add(browser) { + // Start observing PB last-context-exit notification to do the needed cleanup. + this._maybeSetupLastPBContextObserver(); + + let prin = this._basePrincipalForAntiTrackingCommon(browser); + let type = this._permissionTypeFor(browser); + let expire = this._expiryFor(browser); + Services.perms.addFromPrincipal( + prin, + type, + Services.perms.ALLOW_ACTION, + expire + ); + }, + + /** + * Remove the given browser object from the Content Blocking allow list. + */ + remove(browser) { + let prin = this._basePrincipalForAntiTrackingCommon(browser); + let type = this._permissionTypeFor(browser); + Services.perms.removeFromPrincipal(prin, type); + }, + + /** + * Returns true if the current browser has loaded a document that is on the + * Content Blocking allow list. + */ + includes(browser) { + let prin = this._basePrincipalForAntiTrackingCommon(browser); + let type = this._permissionTypeFor(browser); + return ( + Services.perms.testExactPermissionFromPrincipal(prin, type) == + Services.perms.ALLOW_ACTION + ); + }, +}; diff --git a/toolkit/components/antitracking/ContentBlockingLog.cpp b/toolkit/components/antitracking/ContentBlockingLog.cpp new file mode 100644 index 0000000000..6797d2bd2b --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingLog.cpp @@ -0,0 +1,512 @@ +/* -*- 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 "ContentBlockingLog.h" + +#include "nsIEffectiveTLDService.h" +#include "nsITrackingDBService.h" +#include "nsIWebProgressListener.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsRFPService.h" +#include "nsServiceManagerUtils.h" +#include "nsTArray.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/Preferences.h" +#include "mozilla/RandomNum.h" +#include "mozilla/ReverseIterator.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StaticPrefs_telemetry.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Telemetry.h" +#include "mozilla/XorShift128PlusRNG.h" + +namespace mozilla { + +namespace { + +StaticAutoPtr<nsCString> gEmailWebAppDomainsPref; +static constexpr char kEmailWebAppDomainPrefName[] = + "privacy.trackingprotection.emailtracking.webapp.domains"; + +void EmailWebAppDomainPrefChangeCallback(const char* aPrefName, void*) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kEmailWebAppDomainPrefName)); + MOZ_ASSERT(gEmailWebAppDomainsPref); + + Preferences::GetCString(kEmailWebAppDomainPrefName, *gEmailWebAppDomainsPref); +} + +} // namespace + +Maybe<uint32_t> ContentBlockingLog::RecordLogParent( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason, + const nsTArray<nsCString>& aTrackingFullHashes, + const Maybe<ContentBlockingNotifier::CanvasFingerprinter>& + aCanvasFingerprinter, + const Maybe<bool> aCanvasFingerprinterKnownText) { + MOZ_ASSERT(XRE_IsParentProcess()); + + uint32_t events = GetContentBlockingEventsInLog(); + + bool blockedValue = aBlocked; + bool unblocked = false; + OriginEntry* entry; + + switch (aType) { + case nsIWebProgressListener::STATE_COOKIES_LOADED: + MOZ_ASSERT(!aBlocked, + "We don't expected to see blocked STATE_COOKIES_LOADED"); + [[fallthrough]]; + + case nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER: + MOZ_ASSERT( + !aBlocked, + "We don't expected to see blocked STATE_COOKIES_LOADED_TRACKER"); + [[fallthrough]]; + + case nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER: + MOZ_ASSERT(!aBlocked, + "We don't expected to see blocked " + "STATE_COOKIES_LOADED_SOCIALTRACKER"); + // Note that the logic in these branches are the logical negation of the + // logic in other branches, since the Document API we have is phrased + // in "loaded" terms as opposed to "blocked" terms. + blockedValue = !aBlocked; + [[fallthrough]]; + + case nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT: + case nsIWebProgressListener::STATE_BLOCKED_FINGERPRINTING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_FINGERPRINTING_CONTENT: + case nsIWebProgressListener::STATE_BLOCKED_CRYPTOMINING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_CRYPTOMINING_CONTENT: + case nsIWebProgressListener::STATE_BLOCKED_SOCIALTRACKING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_SOCIALTRACKING_CONTENT: + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION: + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL: + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN: + case nsIWebProgressListener::STATE_BLOCKED_EMAILTRACKING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT: + case nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_2_CONTENT: + Unused << RecordLogInternal(aOrigin, aType, blockedValue); + break; + + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER: + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER: + Unused << RecordLogInternal(aOrigin, aType, blockedValue, aReason, + aTrackingFullHashes); + break; + + case nsIWebProgressListener::STATE_REPLACED_FINGERPRINTING_CONTENT: + case nsIWebProgressListener::STATE_ALLOWED_FINGERPRINTING_CONTENT: + case nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT: + case nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT: + Unused << RecordLogInternal(aOrigin, aType, blockedValue, aReason, + aTrackingFullHashes); + break; + case nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING: + MOZ_ASSERT(!aBlocked, + "We don't expected to see blocked " + "STATE_ALLOWED_FONT_FINGERPRINTING"); + entry = RecordLogInternal(aOrigin, aType, blockedValue); + + // Replace the flag using the suspicious fingerprinting event so that we + // can report the event if we detect suspicious fingerprinting. + aType = nsIWebProgressListener::STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING; + + // Report blocking if we detect suspicious fingerprinting activity. + if (entry && entry->mData->mHasSuspiciousFingerprintingActivity) { + blockedValue = true; + } + break; + + case nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING: + MOZ_ASSERT(!aBlocked, + "We don't expected to see blocked " + "STATE_ALLOWED_CANVAS_FINGERPRINTING"); + entry = RecordLogInternal(aOrigin, aType, blockedValue, Nothing(), {}, + aCanvasFingerprinter, + aCanvasFingerprinterKnownText); + + // Replace the flag using the suspicious fingerprinting event so that we + // can report the event if we detect suspicious fingerprinting. + aType = nsIWebProgressListener::STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING; + + // Report blocking if we detect suspicious fingerprinting activity. + if (entry && entry->mData->mHasSuspiciousFingerprintingActivity) { + blockedValue = true; + } + break; + + default: + // Ignore nsIWebProgressListener::STATE_BLOCKED_UNSAFE_CONTENT; + break; + } + + if (!aBlocked) { + unblocked = (events & aType) != 0; + } + + const uint32_t oldEvents = events; + if (blockedValue) { + events |= aType; + } else if (unblocked) { + events &= ~aType; + } + + if (events == oldEvents +#ifdef ANDROID + // GeckoView always needs to notify about blocked trackers, + // since the GeckoView API always needs to report the URI and + // type of any blocked tracker. We use a platform-dependent code + // path here because reporting this notification on desktop + // platforms isn't necessary and doing so can have a big + // performance cost. + && aType != nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT +#endif + ) { + // Avoid dispatching repeated notifications when nothing has + // changed + return Nothing(); + } + + return Some(events); +} + +void ContentBlockingLog::ReportLog(nsIPrincipal* aFirstPartyPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aFirstPartyPrincipal); + + if (!StaticPrefs::browser_contentblocking_database_enabled()) { + return; + } + + if (mLog.IsEmpty()) { + return; + } + + nsCOMPtr<nsITrackingDBService> trackingDBService = + do_GetService("@mozilla.org/tracking-db-service;1"); + if (NS_WARN_IF(!trackingDBService)) { + return; + } + + trackingDBService->RecordContentBlockingLog(Stringify()); +} + +void ContentBlockingLog::ReportCanvasFingerprintingLog( + nsIPrincipal* aFirstPartyPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aFirstPartyPrincipal); + + // We don't need to report if the first party is not a content. + if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) { + return; + } + + bool hasCanvasFingerprinter = false; + bool canvasFingerprinterKnownText = false; + Maybe<ContentBlockingNotifier::CanvasFingerprinter> canvasFingerprinter; + for (const auto& originEntry : mLog) { + if (!originEntry.mData) { + continue; + } + + for (const auto& logEntry : Reversed(originEntry.mData->mLogs)) { + if (logEntry.mType != + nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING) { + continue; + } + + // Select the log entry with the highest fingerprinting likelihood, + // that primarily means preferring those with a FingerprinterKnownText. + if (!hasCanvasFingerprinter || + (!canvasFingerprinterKnownText && + *logEntry.mCanvasFingerprinterKnownText) || + (!canvasFingerprinterKnownText && canvasFingerprinter.isNothing() && + logEntry.mCanvasFingerprinter.isSome())) { + hasCanvasFingerprinter = true; + canvasFingerprinterKnownText = *logEntry.mCanvasFingerprinterKnownText; + canvasFingerprinter = logEntry.mCanvasFingerprinter; + } + } + } + + if (!hasCanvasFingerprinter) { + Telemetry::Accumulate(Telemetry::CANVAS_FINGERPRINTING_PER_TAB, + "unknown"_ns, 0); + } else { + int32_t fingerprinter = + canvasFingerprinter.isSome() ? (*canvasFingerprinter + 1) : 0; + Telemetry::Accumulate( + Telemetry::CANVAS_FINGERPRINTING_PER_TAB, + canvasFingerprinterKnownText ? "known_text"_ns : "unknown"_ns, + fingerprinter); + } +} + +void ContentBlockingLog::ReportFontFingerprintingLog( + nsIPrincipal* aFirstPartyPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aFirstPartyPrincipal); + + // We don't need to report if the first party is not a content. + if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) { + return; + } + + bool hasFontFingerprinter = false; + for (const auto& originEntry : mLog) { + if (!originEntry.mData) { + continue; + } + + for (const auto& logEntry : originEntry.mData->mLogs) { + if (logEntry.mType != + nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) { + continue; + } + + hasFontFingerprinter = true; + } + + if (hasFontFingerprinter) { + break; + } + } + + Telemetry::Accumulate(Telemetry::FONT_FINGERPRINTING_PER_TAB, + hasFontFingerprinter); +} + +void ContentBlockingLog::ReportEmailTrackingLog( + nsIPrincipal* aFirstPartyPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aFirstPartyPrincipal); + + // We don't need to report if the first party is not a content. + if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) { + return; + } + + nsCOMPtr<nsIEffectiveTLDService> tldService = + do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + + if (!tldService) { + return; + } + + nsTHashtable<nsCStringHashKey> level1SiteSet; + nsTHashtable<nsCStringHashKey> level2SiteSet; + + for (const auto& originEntry : mLog) { + if (!originEntry.mData) { + continue; + } + + bool isLevel1EmailTracker = false; + bool isLevel2EmailTracker = false; + + for (const auto& logEntry : Reversed(originEntry.mData->mLogs)) { + // Check if the email tracking related event had been filed for the given + // origin entry. Note that we currently only block level 1 email trackers, + // so blocking event represents the page has embedded a level 1 tracker. + if (logEntry.mType == + nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_2_CONTENT) { + isLevel2EmailTracker = true; + break; + } + + if (logEntry.mType == + nsIWebProgressListener::STATE_BLOCKED_EMAILTRACKING_CONTENT || + logEntry.mType == nsIWebProgressListener:: + STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT) { + isLevel1EmailTracker = true; + break; + } + } + + if (isLevel1EmailTracker || isLevel2EmailTracker) { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), originEntry.mOrigin); + + if (NS_FAILED(rv)) { + continue; + } + + nsAutoCString baseDomain; + rv = tldService->GetBaseDomain(uri, 0, baseDomain); + + if (NS_FAILED(rv)) { + continue; + } + + if (isLevel1EmailTracker) { + Unused << level1SiteSet.EnsureInserted(baseDomain); + } else { + Unused << level2SiteSet.EnsureInserted(baseDomain); + } + } + } + + // Cache the email webapp domains pref value and register the callback + // function to update the cached value when the pref changes. + if (!gEmailWebAppDomainsPref) { + gEmailWebAppDomainsPref = new nsCString(); + + Preferences::RegisterCallbackAndCall(EmailWebAppDomainPrefChangeCallback, + kEmailWebAppDomainPrefName); + RunOnShutdown([]() { + Preferences::UnregisterCallback(EmailWebAppDomainPrefChangeCallback, + kEmailWebAppDomainPrefName); + gEmailWebAppDomainsPref = nullptr; + }); + } + + bool isTopEmailWebApp = + aFirstPartyPrincipal->IsURIInList(*gEmailWebAppDomainsPref); + uint32_t level1Count = level1SiteSet.Count(); + uint32_t level2Count = level2SiteSet.Count(); + + Telemetry::Accumulate( + Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB, + isTopEmailWebApp ? "base_emailapp"_ns : "base_normal"_ns, level1Count); + Telemetry::Accumulate( + Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB, + isTopEmailWebApp ? "content_emailapp"_ns : "content_normal"_ns, + level2Count); + Telemetry::Accumulate(Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB, + isTopEmailWebApp ? "all_emailapp"_ns : "all_normal"_ns, + level1Count + level2Count); +} + +ContentBlockingLog::OriginEntry* ContentBlockingLog::RecordLogInternal( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason, + const nsTArray<nsCString>& aTrackingFullHashes, + const Maybe<ContentBlockingNotifier::CanvasFingerprinter>& + aCanvasFingerprinter, + const Maybe<bool> aCanvasFingerprinterKnownText) { + DebugOnly<bool> isCookiesBlockedTracker = + aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER || + aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER; + MOZ_ASSERT_IF(aBlocked, aReason.isNothing()); + MOZ_ASSERT_IF(!isCookiesBlockedTracker, aReason.isNothing()); + MOZ_ASSERT_IF(isCookiesBlockedTracker && !aBlocked, aReason.isSome()); + + if (aOrigin.IsVoid()) { + return nullptr; + } + auto index = mLog.IndexOf(aOrigin, 0, Comparator()); + if (index != OriginDataTable::NoIndex) { + OriginEntry& entry = mLog[index]; + if (!entry.mData) { + return nullptr; + } + + if (RecordLogEntryInCustomField(aType, entry, aBlocked)) { + return &entry; + } + if (!entry.mData->mLogs.IsEmpty()) { + auto& last = entry.mData->mLogs.LastElement(); + if (last.mType == aType && last.mBlocked == aBlocked && + last.mCanvasFingerprinter == aCanvasFingerprinter && + last.mCanvasFingerprinterKnownText == aCanvasFingerprinterKnownText) { + ++last.mRepeatCount; + // Don't record recorded events. This helps compress our log. + // We don't care about if the the reason is the same, just keep the + // first one. + // Note: {aReason, aTrackingFullHashes} are not compared here and we + // simply keep the first for the reason, and merge hashes to make sure + // they can be correctly recorded. + for (const auto& hash : aTrackingFullHashes) { + if (!last.mTrackingFullHashes.Contains(hash)) { + last.mTrackingFullHashes.AppendElement(hash); + } + } + return &entry; + } + } + if (entry.mData->mLogs.Length() == + std::max(1u, StaticPrefs::browser_contentblocking_originlog_length())) { + // Cap the size at the maximum length adjustable by the pref + entry.mData->mLogs.RemoveElementAt(0); + } + entry.mData->mLogs.AppendElement( + LogEntry{aType, 1u, aBlocked, aReason, aTrackingFullHashes.Clone(), + aCanvasFingerprinter, aCanvasFingerprinterKnownText}); + + // Check suspicious fingerprinting activities if the origin hasn't already + // been marked. + // TODO(Bug 1864909): Moving the suspicious fingerprinting detection call + // out of here. + if ((aType == nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING || + aType == nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) && + !entry.mData->mHasSuspiciousFingerprintingActivity && + nsRFPService::CheckSuspiciousFingerprintingActivity( + entry.mData->mLogs)) { + entry.mData->mHasSuspiciousFingerprintingActivity = true; + } + return &entry; + } + + // The entry has not been found. + OriginEntry* entry = mLog.AppendElement(); + if (NS_WARN_IF(!entry || !entry->mData)) { + return nullptr; + } + + entry->mOrigin = aOrigin; + + if (aType == nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) { + entry->mData->mHasLevel1TrackingContentLoaded = aBlocked; + } else if (aType == + nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT) { + entry->mData->mHasLevel2TrackingContentLoaded = aBlocked; + } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) { + MOZ_ASSERT(entry->mData->mHasCookiesLoaded.isNothing()); + entry->mData->mHasCookiesLoaded.emplace(aBlocked); + } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) { + MOZ_ASSERT(entry->mData->mHasTrackerCookiesLoaded.isNothing()); + entry->mData->mHasTrackerCookiesLoaded.emplace(aBlocked); + } else if (aType == + nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) { + MOZ_ASSERT(entry->mData->mHasSocialTrackerCookiesLoaded.isNothing()); + entry->mData->mHasSocialTrackerCookiesLoaded.emplace(aBlocked); + } else { + entry->mData->mLogs.AppendElement( + LogEntry{aType, 1u, aBlocked, aReason, aTrackingFullHashes.Clone(), + aCanvasFingerprinter, aCanvasFingerprinterKnownText}); + + // Check suspicious fingerprinting activities if the origin hasn't been + // marked. + // TODO(Bug 1864909): Moving the suspicious fingerprinting detection call + // out of here. + if ((aType == nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING || + aType == nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) && + nsRFPService::CheckSuspiciousFingerprintingActivity( + entry->mData->mLogs)) { + entry->mData->mHasSuspiciousFingerprintingActivity = true; + } + } + + return entry; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/ContentBlockingLog.h b/toolkit/components/antitracking/ContentBlockingLog.h new file mode 100644 index 0000000000..0e4b7c4b09 --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingLog.h @@ -0,0 +1,381 @@ +/* -*- 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/. */ + +#ifndef mozilla_ContentBlockingLog_h +#define mozilla_ContentBlockingLog_h + +#include "mozilla/ContentBlockingNotifier.h" +#include "mozilla/JSONStringWriteFuncs.h" +#include "mozilla/Maybe.h" +#include "mozilla/StaticPrefs_browser.h" + +#include "mozilla/UniquePtr.h" +#include "nsIWebProgressListener.h" +#include "nsReadableUtils.h" +#include "nsTArray.h" +#include "nsWindowSizes.h" + +class nsIPrincipal; + +namespace mozilla { + +class nsRFPService; + +class ContentBlockingLog final { + typedef ContentBlockingNotifier::StorageAccessPermissionGrantedReason + StorageAccessPermissionGrantedReason; + + protected: + struct LogEntry { + uint32_t mType; + uint32_t mRepeatCount; + bool mBlocked; + Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason> + mReason; + nsTArray<nsCString> mTrackingFullHashes; + Maybe<ContentBlockingNotifier::CanvasFingerprinter> mCanvasFingerprinter; + Maybe<bool> mCanvasFingerprinterKnownText; + }; + + struct OriginDataEntry { + OriginDataEntry() + : mHasLevel1TrackingContentLoaded(false), + mHasLevel2TrackingContentLoaded(false), + mHasSuspiciousFingerprintingActivity(false) {} + + bool mHasLevel1TrackingContentLoaded; + bool mHasLevel2TrackingContentLoaded; + bool mHasSuspiciousFingerprintingActivity; + Maybe<bool> mHasCookiesLoaded; + Maybe<bool> mHasTrackerCookiesLoaded; + Maybe<bool> mHasSocialTrackerCookiesLoaded; + nsTArray<LogEntry> mLogs; + }; + + struct OriginEntry { + OriginEntry() { mData = MakeUnique<OriginDataEntry>(); } + + nsCString mOrigin; + UniquePtr<OriginDataEntry> mData; + }; + + friend class nsRFPService; + + typedef nsTArray<OriginEntry> OriginDataTable; + + struct Comparator { + public: + bool Equals(const OriginDataTable::value_type& aLeft, + const OriginDataTable::value_type& aRight) const { + return aLeft.mOrigin.Equals(aRight.mOrigin); + } + + bool Equals(const OriginDataTable::value_type& aLeft, + const nsACString& aRight) const { + return aLeft.mOrigin.Equals(aRight); + } + }; + + public: + static const nsLiteralCString kDummyOriginHash; + + ContentBlockingLog() = default; + ~ContentBlockingLog() = default; + + // Record the log in the parent process. This should be called only in the + // parent process and will replace the RecordLog below after we remove the + // ContentBlockingLog from content processes. + Maybe<uint32_t> RecordLogParent( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe< + ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason, + const nsTArray<nsCString>& aTrackingFullHashes, + const Maybe<ContentBlockingNotifier::CanvasFingerprinter>& + aCanvasFingerprinter, + const Maybe<bool> aCanvasFingerprinterKnownText); + + void RecordLog( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe< + ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason, + const nsTArray<nsCString>& aTrackingFullHashes) { + RecordLogInternal(aOrigin, aType, aBlocked, aReason, aTrackingFullHashes); + } + + void ReportLog(nsIPrincipal* aFirstPartyPrincipal); + void ReportCanvasFingerprintingLog(nsIPrincipal* aFirstPartyPrincipal); + void ReportFontFingerprintingLog(nsIPrincipal* aFirstPartyPrincipal); + void ReportEmailTrackingLog(nsIPrincipal* aFirstPartyPrincipal); + + nsAutoCString Stringify() { + nsAutoCString buffer; + + JSONStringRefWriteFunc js(buffer); + JSONWriter w(js); + w.Start(); + + for (const OriginEntry& entry : mLog) { + if (!entry.mData) { + continue; + } + + w.StartArrayProperty(entry.mOrigin, w.SingleLineStyle); + + StringifyCustomFields(entry, w); + for (const LogEntry& item : entry.mData->mLogs) { + w.StartArrayElement(w.SingleLineStyle); + { + w.IntElement(item.mType); + w.BoolElement(item.mBlocked); + w.IntElement(item.mRepeatCount); + if (item.mReason.isSome()) { + w.IntElement(item.mReason.value()); + } + } + w.EndArray(); + } + w.EndArray(); + } + + w.End(); + + return buffer; + } + + bool HasBlockedAnyOfType(uint32_t aType) const { + // Note: nothing inside this loop should return false, the goal for the + // loop is to scan the log to see if we find a matching entry, and if so + // we would return true, otherwise in the end of the function outside of + // the loop we take the common `return false;` statement. + for (const OriginEntry& entry : mLog) { + if (!entry.mData) { + continue; + } + + if (aType == + nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) { + if (entry.mData->mHasLevel1TrackingContentLoaded) { + return true; + } + } else if (aType == nsIWebProgressListener:: + STATE_LOADED_LEVEL_2_TRACKING_CONTENT) { + if (entry.mData->mHasLevel2TrackingContentLoaded) { + return true; + } + } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) { + if (entry.mData->mHasCookiesLoaded.isSome() && + entry.mData->mHasCookiesLoaded.value()) { + return true; + } + } else if (aType == + nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) { + if (entry.mData->mHasTrackerCookiesLoaded.isSome() && + entry.mData->mHasTrackerCookiesLoaded.value()) { + return true; + } + } else if (aType == + nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) { + if (entry.mData->mHasSocialTrackerCookiesLoaded.isSome() && + entry.mData->mHasSocialTrackerCookiesLoaded.value()) { + return true; + } + } else { + for (const auto& item : entry.mData->mLogs) { + if (((item.mType & aType) != 0) && item.mBlocked) { + return true; + } + } + } + } + return false; + } + + void AddSizeOfExcludingThis(nsWindowSizes& aSizes) const { + aSizes.mDOMSizes.mDOMOtherSize += + mLog.ShallowSizeOfExcludingThis(aSizes.mState.mMallocSizeOf); + + // Now add the sizes of each origin log queue. + for (const OriginEntry& entry : mLog) { + if (entry.mData) { + aSizes.mDOMSizes.mDOMOtherSize += + aSizes.mState.mMallocSizeOf(entry.mData.get()) + + entry.mData->mLogs.ShallowSizeOfExcludingThis( + aSizes.mState.mMallocSizeOf); + } + } + } + + uint32_t GetContentBlockingEventsInLog() { + uint32_t events = 0; + + // We iterate the whole log to produce the overview of blocked events. + for (const OriginEntry& entry : mLog) { + if (!entry.mData) { + continue; + } + + if (entry.mData->mHasLevel1TrackingContentLoaded) { + events |= nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT; + } + + if (entry.mData->mHasLevel2TrackingContentLoaded) { + events |= nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT; + } + + if (entry.mData->mHasSuspiciousFingerprintingActivity) { + events |= + nsIWebProgressListener::STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING; + } + + if (entry.mData->mHasCookiesLoaded.isSome() && + entry.mData->mHasCookiesLoaded.value()) { + events |= nsIWebProgressListener::STATE_COOKIES_LOADED; + } + + if (entry.mData->mHasTrackerCookiesLoaded.isSome() && + entry.mData->mHasTrackerCookiesLoaded.value()) { + events |= nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER; + } + + if (entry.mData->mHasSocialTrackerCookiesLoaded.isSome() && + entry.mData->mHasSocialTrackerCookiesLoaded.value()) { + events |= nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER; + } + + for (const auto& item : entry.mData->mLogs) { + if (item.mBlocked) { + events |= item.mType; + } + } + } + + return events; + } + + private: + OriginEntry* RecordLogInternal( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe< + ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason = Nothing(), + const nsTArray<nsCString>& aTrackingFullHashes = nsTArray<nsCString>(), + const Maybe<ContentBlockingNotifier::CanvasFingerprinter>& + aCanvasFingerprinter = Nothing(), + const Maybe<bool> aCanvasFingerprinterKnownText = Nothing()); + + bool RecordLogEntryInCustomField(uint32_t aType, OriginEntry& aEntry, + bool aBlocked) { + if (aType == + nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) { + aEntry.mData->mHasLevel1TrackingContentLoaded = aBlocked; + return true; + } + if (aType == + nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT) { + aEntry.mData->mHasLevel2TrackingContentLoaded = aBlocked; + return true; + } + if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) { + if (aEntry.mData->mHasCookiesLoaded.isSome()) { + aEntry.mData->mHasCookiesLoaded.ref() = aBlocked; + } else { + aEntry.mData->mHasCookiesLoaded.emplace(aBlocked); + } + return true; + } + if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) { + if (aEntry.mData->mHasTrackerCookiesLoaded.isSome()) { + aEntry.mData->mHasTrackerCookiesLoaded.ref() = aBlocked; + } else { + aEntry.mData->mHasTrackerCookiesLoaded.emplace(aBlocked); + } + return true; + } + if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) { + if (aEntry.mData->mHasSocialTrackerCookiesLoaded.isSome()) { + aEntry.mData->mHasSocialTrackerCookiesLoaded.ref() = aBlocked; + } else { + aEntry.mData->mHasSocialTrackerCookiesLoaded.emplace(aBlocked); + } + return true; + } + return false; + } + + void StringifyCustomFields(const OriginEntry& aEntry, JSONWriter& aWriter) { + if (aEntry.mData->mHasLevel1TrackingContentLoaded) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement( + nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT); + aWriter.BoolElement(true); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + if (aEntry.mData->mHasLevel2TrackingContentLoaded) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement( + nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT); + aWriter.BoolElement(true); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + if (aEntry.mData->mHasCookiesLoaded.isSome()) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement(nsIWebProgressListener::STATE_COOKIES_LOADED); + aWriter.BoolElement( + aEntry.mData->mHasCookiesLoaded.value()); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + if (aEntry.mData->mHasTrackerCookiesLoaded.isSome()) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement( + nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER); + aWriter.BoolElement( + aEntry.mData->mHasTrackerCookiesLoaded.value()); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + if (aEntry.mData->mHasSocialTrackerCookiesLoaded.isSome()) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement( + nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER); + aWriter.BoolElement( + aEntry.mData->mHasSocialTrackerCookiesLoaded.value()); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + if (aEntry.mData->mHasSuspiciousFingerprintingActivity) { + aWriter.StartArrayElement(aWriter.SingleLineStyle); + { + aWriter.IntElement( + nsIWebProgressListener::STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING); + aWriter.BoolElement(true); // blocked + aWriter.IntElement(1); // repeat count + } + aWriter.EndArray(); + } + } + + private: + OriginDataTable mLog; +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/ContentBlockingNotifier.cpp b/toolkit/components/antitracking/ContentBlockingNotifier.cpp new file mode 100644 index 0000000000..84f58020ef --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingNotifier.cpp @@ -0,0 +1,589 @@ +/* -*- 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 "ContentBlockingNotifier.h" +#include "AntiTrackingUtils.h" + +#include "mozilla/EventQueue.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "nsIClassifiedChannel.h" +#include "nsIRunnable.h" +#include "nsIScriptError.h" +#include "nsIURI.h" +#include "nsIOService.h" +#include "nsGlobalWindowOuter.h" +#include "nsJSUtils.h" +#include "mozIThirdPartyUtil.h" + +using namespace mozilla; +using namespace mozilla::dom; +using mozilla::dom::BrowsingContext; +using mozilla::dom::ContentChild; +using mozilla::dom::Document; + +static const uint32_t kMaxConsoleOutputDelayMs = 100; + +namespace { + +void RunConsoleReportingRunnable(already_AddRefed<nsIRunnable>&& 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<nsIRunnable> runnable(std::move(aRunnable)); + nsresult rv = runnable->Run(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } +} + +void ReportUnblockingToConsole( + uint64_t aWindowID, nsIPrincipal* aPrincipal, + const nsAString& aTrackingOrigin, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason) { + MOZ_ASSERT(aWindowID); + MOZ_ASSERT(aPrincipal); + + nsAutoString sourceLine; + uint32_t lineNumber = 0, columnNumber = 1; + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (cx) { + nsJSUtils::GetCallingLocation(cx, sourceLine, &lineNumber, &columnNumber); + } + + nsCOMPtr<nsIPrincipal> principal(aPrincipal); + nsAutoString trackingOrigin(aTrackingOrigin); + + RefPtr<Runnable> runnable = NS_NewRunnableFunction( + "ReportUnblockingToConsoleDelayed", + [aWindowID, sourceLine, lineNumber, columnNumber, principal, + trackingOrigin, aReason]() { + const char* messageWithSameOrigin = nullptr; + + switch (aReason) { + case ContentBlockingNotifier::eStorageAccessAPI: + case ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI: + messageWithSameOrigin = "CookieAllowedForOriginByStorageAccessAPI"; + break; + + case ContentBlockingNotifier::eOpenerAfterUserInteraction: + [[fallthrough]]; + case ContentBlockingNotifier::eOpener: + messageWithSameOrigin = "CookieAllowedForOriginByHeuristic"; + break; + } + + nsAutoCString origin; + nsresult rv = principal->GetOriginNoSuffix(origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Not adding grantedOrigin yet because we may not want it later. + AutoTArray<nsString, 2> params = {NS_ConvertUTF8toUTF16(origin), + trackingOrigin}; + + nsAutoString errorText; + rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eNECKO_PROPERTIES, messageWithSameOrigin, params, + errorText); + NS_ENSURE_SUCCESS_VOID(rv); + + nsContentUtils::ReportToConsoleByWindowID( + errorText, nsIScriptError::warningFlag, + ANTITRACKING_CONSOLE_CATEGORY, aWindowID, nullptr, sourceLine, + lineNumber, columnNumber); + }); + + RunConsoleReportingRunnable(runnable.forget()); +} + +void ReportBlockingToConsole(uint64_t aWindowID, nsIURI* aURI, + uint32_t aRejectedReason) { + MOZ_ASSERT(aWindowID); + MOZ_ASSERT(aURI); + MOZ_ASSERT( + aRejectedReason == 0 || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN)); + + if (aURI->SchemeIs("chrome") || aURI->SchemeIs("about")) { + return; + } + bool hasFlags; + nsresult rv = NS_URIChainHasFlags( + aURI, nsIProtocolHandler::URI_FORBIDS_COOKIE_ACCESS, &hasFlags); + if (NS_FAILED(rv) || hasFlags) { + // If the protocol doesn't support cookies, no need to report them blocked. + return; + } + + nsAutoString sourceLine; + uint32_t lineNumber = 0, columnNumber = 1; + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (cx) { + nsJSUtils::GetCallingLocation(cx, sourceLine, &lineNumber, &columnNumber); + } + + nsCOMPtr<nsIURI> uri(aURI); + + RefPtr<Runnable> runnable = NS_NewRunnableFunction( + "ReportBlockingToConsoleDelayed", [aWindowID, 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). + // XXX: The nsIWebProgressListener constants below are interpreted as + // signed integers on Windows and the compiler complains that they can't + // be narrowed to uint32_t. To prevent this, we cast them to uint32_t. + switch (aRejectedReason) { + case uint32_t( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION): + message = "CookieBlockedByPermission"; + category = "cookieBlockedPermission"_ns; + break; + + case uint32_t(nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER): + message = "CookieBlockedTracker"; + category = "cookieBlockedTracker"_ns; + break; + + case uint32_t(nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL): + message = "CookieBlockedAll"; + category = "cookieBlockedAll"_ns; + break; + + case uint32_t(nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN): + message = "CookieBlockedForeign"; + category = "cookieBlockedForeign"_ns; + break; + + case uint32_t( + nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN): + message = "CookiePartitionedForeign2"; + category = "cookiePartitionedForeign"_ns; + 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<nsIURI> exposableURI = + net::nsIOService::CreateExposableURI(uri); + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(exposableURI->GetSpecOrDefault(), + *params.AppendElement()); + + nsAutoString errorText; + nsresult rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eNECKO_PROPERTIES, message, params, errorText); + NS_ENSURE_SUCCESS_VOID(rv); + + nsContentUtils::ReportToConsoleByWindowID( + errorText, nsIScriptError::warningFlag, category, aWindowID, + nullptr, sourceLine, lineNumber, columnNumber); + }); + + RunConsoleReportingRunnable(runnable.forget()); +} + +void ReportBlockingToConsole(nsIChannel* aChannel, nsIURI* aURI, + uint32_t aRejectedReason) { + MOZ_ASSERT(aChannel && aURI); + uint64_t windowID = nsContentUtils::GetInnerWindowID(aChannel); + if (!windowID) { + // Get the window ID from the target BrowsingContext + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + RefPtr<dom::BrowsingContext> targetBrowsingContext; + loadInfo->GetTargetBrowsingContext(getter_AddRefs(targetBrowsingContext)); + + if (!targetBrowsingContext) { + return; + } + + WindowContext* windowContext = + targetBrowsingContext->GetCurrentWindowContext(); + if (!windowContext) { + return; + } + + windowID = windowContext->InnerWindowId(); + } + ReportBlockingToConsole(windowID, aURI, aRejectedReason); +} + +void NotifyBlockingDecision(nsIChannel* aTrackingChannel, + ContentBlockingNotifier::BlockingDecision aDecision, + uint32_t aRejectedReason, nsIURI* aURI) { + MOZ_ASSERT(aTrackingChannel); + + // This can be called in either the parent process or the child processes. + // When this is called in the child processes, we must have a window. + if (XRE_IsContentProcess()) { + nsCOMPtr<nsILoadContext> loadContext; + NS_QueryNotificationCallbacks(aTrackingChannel, loadContext); + if (!loadContext) { + return; + } + + nsCOMPtr<mozIDOMWindowProxy> window; + loadContext->GetAssociatedWindow(getter_AddRefs(window)); + if (!window) { + return; + } + + nsCOMPtr<nsPIDOMWindowOuter> outer = nsPIDOMWindowOuter::From(window); + if (!outer) { + return; + } + + // When this is called in the child processes with system privileges, + // the decision should always be ALLOW. We can stop here because both + // UI and content blocking log don't care this event. + if (nsGlobalWindowOuter::Cast(outer)->GetPrincipal() == + nsContentUtils::GetSystemPrincipal()) { + MOZ_DIAGNOSTIC_ASSERT(aDecision == + ContentBlockingNotifier::BlockingDecision::eAllow); + return; + } + } + + nsAutoCString trackingOrigin; + if (aURI) { + // Using an empty OriginAttributes is OK here, as we'll only be accessing + // OriginNoSuffix. + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(aURI, OriginAttributes{}); + principal->GetOriginNoSuffix(trackingOrigin); + } + + if (aDecision == ContentBlockingNotifier::BlockingDecision::eBlock) { + ContentBlockingNotifier::OnEvent(aTrackingChannel, true, aRejectedReason, + trackingOrigin); + + ReportBlockingToConsole(aTrackingChannel, aURI, aRejectedReason); + } + + // Now send the generic "cookies loaded" notifications, from the most generic + // to the most specific. + ContentBlockingNotifier::OnEvent(aTrackingChannel, false, + nsIWebProgressListener::STATE_COOKIES_LOADED, + trackingOrigin); + + nsCOMPtr<nsIClassifiedChannel> classifiedChannel = + do_QueryInterface(aTrackingChannel); + if (!classifiedChannel) { + return; + } + + uint32_t classificationFlags = + classifiedChannel->GetThirdPartyClassificationFlags(); + if (classificationFlags & + nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_TRACKING) { + ContentBlockingNotifier::OnEvent( + aTrackingChannel, false, + nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER, trackingOrigin); + } + + if (classificationFlags & + nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_SOCIALTRACKING) { + ContentBlockingNotifier::OnEvent( + aTrackingChannel, false, + nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER, + trackingOrigin); + } +} + +// Send a message to notify OnContentBlockingEvent in the parent, which will +// update the ContentBlockingLog in the parent. +void NotifyEventInChild( + nsIChannel* aTrackingChannel, bool aBlocked, uint32_t aRejectedReason, + const nsACString& aTrackingOrigin, + const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason, + const Maybe<ContentBlockingNotifier::CanvasFingerprinter> + aCanvasFingerprinter, + const Maybe<bool> aCanvasFingerprinterKnownText) { + MOZ_ASSERT(XRE_IsContentProcess()); + + // We don't need to find the top-level window here because the + // parent will do that for us. + nsCOMPtr<nsILoadContext> loadContext; + NS_QueryNotificationCallbacks(aTrackingChannel, loadContext); + if (!loadContext) { + return; + } + + nsCOMPtr<mozIDOMWindowProxy> window; + loadContext->GetAssociatedWindow(getter_AddRefs(window)); + if (!window) { + return; + } + + RefPtr<dom::BrowserChild> browserChild = dom::BrowserChild::GetFrom(window); + NS_ENSURE_TRUE_VOID(browserChild); + + nsTArray<nsCString> trackingFullHashes; + nsCOMPtr<nsIClassifiedChannel> classifiedChannel = + do_QueryInterface(aTrackingChannel); + + if (classifiedChannel) { + Unused << classifiedChannel->GetMatchedTrackingFullHashes( + trackingFullHashes); + } + + browserChild->NotifyContentBlockingEvent( + aRejectedReason, aTrackingChannel, aBlocked, aTrackingOrigin, + trackingFullHashes, aReason, aCanvasFingerprinter, + aCanvasFingerprinterKnownText); +} + +// Update the ContentBlockingLog of the top-level WindowGlobalParent of +// the tracking channel. +void NotifyEventInParent( + nsIChannel* aTrackingChannel, bool aBlocked, uint32_t aRejectedReason, + const nsACString& aTrackingOrigin, + const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason, + const Maybe<ContentBlockingNotifier::CanvasFingerprinter> + aCanvasFingerprinter, + const Maybe<bool> aCanvasFingerprinterKnownText) { + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr<nsILoadInfo> loadInfo = aTrackingChannel->LoadInfo(); + RefPtr<dom::BrowsingContext> bc; + loadInfo->GetBrowsingContext(getter_AddRefs(bc)); + + if (!bc || bc->IsDiscarded()) { + return; + } + + bc = bc->Top(); + RefPtr<dom::WindowGlobalParent> wgp = + bc->Canonical()->GetCurrentWindowGlobal(); + NS_ENSURE_TRUE_VOID(wgp); + + nsTArray<nsCString> trackingFullHashes; + nsCOMPtr<nsIClassifiedChannel> classifiedChannel = + do_QueryInterface(aTrackingChannel); + + if (classifiedChannel) { + Unused << classifiedChannel->GetMatchedTrackingFullHashes( + trackingFullHashes); + } + + wgp->NotifyContentBlockingEvent(aRejectedReason, aTrackingChannel, aBlocked, + aTrackingOrigin, trackingFullHashes, aReason, + aCanvasFingerprinter, + aCanvasFingerprinterKnownText); +} + +} // namespace + +/* static */ +void ContentBlockingNotifier::ReportUnblockingToConsole( + BrowsingContext* aBrowsingContext, const nsAString& aTrackingOrigin, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason) { + MOZ_ASSERT(aBrowsingContext); + MOZ_ASSERT_IF(XRE_IsContentProcess(), aBrowsingContext->Top()->IsInProcess()); + + uint64_t windowID = aBrowsingContext->GetCurrentInnerWindowId(); + + // The storage permission is granted under the top-level origin. + nsCOMPtr<nsIPrincipal> principal = + AntiTrackingUtils::GetPrincipal(aBrowsingContext->Top()); + if (NS_WARN_IF(!principal)) { + return; + } + + ::ReportUnblockingToConsole(windowID, principal, aTrackingOrigin, aReason); +} + +/* static */ +void ContentBlockingNotifier::OnDecision(nsIChannel* aChannel, + BlockingDecision aDecision, + uint32_t aRejectedReason) { + MOZ_ASSERT( + aRejectedReason == 0 || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN)); + MOZ_ASSERT(aDecision == BlockingDecision::eBlock || + aDecision == BlockingDecision::eAllow); + + if (!aChannel) { + return; + } + + nsCOMPtr<nsIURI> uri; + aChannel->GetURI(getter_AddRefs(uri)); + + // Can be called in EITHER the parent or child process. + NotifyBlockingDecision(aChannel, aDecision, aRejectedReason, uri); +} + +/* static */ +void ContentBlockingNotifier::OnDecision(nsPIDOMWindowInner* aWindow, + BlockingDecision aDecision, + uint32_t aRejectedReason) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT( + aRejectedReason == 0 || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN)); + MOZ_ASSERT(aDecision == BlockingDecision::eBlock || + aDecision == BlockingDecision::eAllow); + + Document* document = aWindow->GetExtantDoc(); + if (!document) { + return; + } + + nsIChannel* channel = document->GetChannel(); + if (!channel) { + return; + } + + nsIURI* uri = document->GetDocumentURI(); + + NotifyBlockingDecision(channel, aDecision, aRejectedReason, uri); +} + +/* static */ +void ContentBlockingNotifier::OnDecision(BrowsingContext* aBrowsingContext, + BlockingDecision aDecision, + uint32_t aRejectedReason) { + MOZ_ASSERT(aBrowsingContext); + MOZ_ASSERT_IF(XRE_IsContentProcess(), aBrowsingContext->IsInProcess()); + + if (aBrowsingContext->IsInProcess()) { + nsCOMPtr<nsPIDOMWindowOuter> outer = aBrowsingContext->GetDOMWindow(); + if (NS_WARN_IF(!outer)) { + return; + } + + nsCOMPtr<nsPIDOMWindowInner> inner = outer->GetCurrentInnerWindow(); + if (NS_WARN_IF(!inner)) { + return; + } + + ContentBlockingNotifier::OnDecision(inner, aDecision, aRejectedReason); + } else { + // we send an IPC to the content process when we don't have an in-process + // browsing context. This is not smart because this should be able to be + // done directly in the parent. The reason we are doing this is because we + // need the channel, which is not accessible in the parent when you only + // have a browsing context. + MOZ_ASSERT(XRE_IsParentProcess()); + + ContentParent* cp = aBrowsingContext->Canonical()->GetContentParent(); + Unused << cp->SendOnContentBlockingDecision(aBrowsingContext, aDecision, + aRejectedReason); + } +} + +/* static */ +void ContentBlockingNotifier::OnEvent(nsIChannel* aTrackingChannel, + uint32_t aRejectedReason, bool aBlocked) { + MOZ_ASSERT(XRE_IsParentProcess() && aTrackingChannel); + + nsCOMPtr<nsIURI> uri; + aTrackingChannel->GetURI(getter_AddRefs(uri)); + + nsAutoCString trackingOrigin; + if (uri) { + // Using empty OriginAttributes is OK here, as we only want to access + // OriginNoSuffix. + nsCOMPtr<nsIPrincipal> trackingPrincipal = + BasePrincipal::CreateContentPrincipal(uri, OriginAttributes{}); + trackingPrincipal->GetOriginNoSuffix(trackingOrigin); + } + + return ContentBlockingNotifier::OnEvent(aTrackingChannel, aBlocked, + aRejectedReason, trackingOrigin); +} + +/* static */ +void ContentBlockingNotifier::OnEvent( + nsIChannel* aTrackingChannel, bool aBlocked, uint32_t aRejectedReason, + const nsACString& aTrackingOrigin, + const Maybe<StorageAccessPermissionGrantedReason>& aReason, + const Maybe<CanvasFingerprinter>& aCanvasFingerprinter, + const Maybe<bool> aCanvasFingerprinterKnownText) { + if (XRE_IsParentProcess()) { + NotifyEventInParent(aTrackingChannel, aBlocked, aRejectedReason, + aTrackingOrigin, aReason, aCanvasFingerprinter, + aCanvasFingerprinterKnownText); + } else { + NotifyEventInChild(aTrackingChannel, aBlocked, aRejectedReason, + aTrackingOrigin, aReason, aCanvasFingerprinter, + aCanvasFingerprinterKnownText); + } +} diff --git a/toolkit/components/antitracking/ContentBlockingNotifier.h b/toolkit/components/antitracking/ContentBlockingNotifier.h new file mode 100644 index 0000000000..e7f7c622db --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingNotifier.h @@ -0,0 +1,98 @@ +/* -*- 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/. */ + +#ifndef mozilla_contentblockingnotifier_h +#define mozilla_contentblockingnotifier_h + +#include "nsStringFwd.h" +#include "mozilla/Maybe.h" + +#define ANTITRACKING_CONSOLE_CATEGORY "Content Blocking"_ns + +class nsIChannel; +class nsPIDOMWindowInner; +class nsPIDOMWindowOuter; + +namespace mozilla { +namespace dom { +class BrowsingContext; +} // namespace dom + +class ContentBlockingNotifier final { + public: + enum class BlockingDecision { + eBlock, + eAllow, + }; + enum StorageAccessPermissionGrantedReason { + eStorageAccessAPI, + eOpenerAfterUserInteraction, + eOpener, + ePrivilegeStorageAccessForOriginAPI, + }; + + // We try to classify observed canvas fingerprinting scripts into different + // classes, but we don't usually know the source/vendor of those scripts. The + // classification is based on a behavioral analysis, based on type of canvas, + // the extracted (e.g. toDataURL) size, the usage of functions like fillText + // etc. See `nsRFPService::MaybeReportCanvasFingerprinter` for the + // classification heuristic. + enum CanvasFingerprinter { + // Suspected fingerprint.com (FingerprintJS) + eFingerprintJS, + // Suspected Akamai fingerprinter + eAkamai, + // Unknown but distinct types of fingerprinters + eVariant1, + eVariant2, + eVariant3, + eVariant4, + // This just indicates that more than one canvas was extracted and is a + // very weak signal. + eMaybe + }; + + // This method can be called on the parent process or on the content process. + // The notification is propagated to the child channel if aChannel is a parent + // channel proxy. + // + // aDecision can be eBlock if we have decided to block some content, or eAllow + // if we have decided to allow the content through. + // + // aRejectedReason must be one of these values: + // * nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION + // * nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER + // * nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER + // * nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL + // * nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN + static void OnDecision(nsIChannel* aChannel, BlockingDecision aDecision, + uint32_t aRejectedReason); + + static void OnDecision(nsPIDOMWindowInner* aWindow, + BlockingDecision aDecision, uint32_t aRejectedReason); + + static void OnDecision(dom::BrowsingContext* aBrowsingContext, + BlockingDecision aDecision, uint32_t aRejectedReason); + + static void OnEvent(nsIChannel* aChannel, uint32_t aRejectedReason, + bool aBlocked = true); + + static void OnEvent( + nsIChannel* aChannel, bool aBlocked, uint32_t aRejectedReason, + const nsACString& aTrackingOrigin, + const ::mozilla::Maybe<StorageAccessPermissionGrantedReason>& aReason = + Nothing(), + const Maybe<CanvasFingerprinter>& aCanvasFingerprinter = Nothing(), + const Maybe<bool> aCanvasFingerprinterKnownText = Nothing()); + + static void ReportUnblockingToConsole( + dom::BrowsingContext* aBrowsingContext, const nsAString& aTrackingOrigin, + StorageAccessPermissionGrantedReason aReason); +}; + +} // namespace mozilla + +#endif // mozilla_contentblockingnotifier_h diff --git a/toolkit/components/antitracking/ContentBlockingTelemetryService.cpp b/toolkit/components/antitracking/ContentBlockingTelemetryService.cpp new file mode 100644 index 0000000000..3ea1e3f03d --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingTelemetryService.cpp @@ -0,0 +1,131 @@ +/* -*- 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 "ContentBlockingTelemetryService.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/PermissionManager.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Telemetry.h" + +#include "AntiTrackingLog.h" +#include "prtime.h" + +#include "nsIObserverService.h" +#include "nsIPermission.h" +#include "nsTArray.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(ContentBlockingTelemetryService, nsIObserver) + +static StaticRefPtr<ContentBlockingTelemetryService> + sContentBlockingTelemetryService; + +/* static */ +already_AddRefed<ContentBlockingTelemetryService> +ContentBlockingTelemetryService::GetSingleton() { + if (!sContentBlockingTelemetryService) { + sContentBlockingTelemetryService = new ContentBlockingTelemetryService(); + ClearOnShutdown(&sContentBlockingTelemetryService); + } + + RefPtr<ContentBlockingTelemetryService> service = + sContentBlockingTelemetryService; + + return service.forget(); +} + +NS_IMETHODIMP +ContentBlockingTelemetryService::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + if (strcmp(aTopic, "idle-daily") == 0) { + ReportStoragePermissionExpire(); + return NS_OK; + } + + return NS_OK; +} + +void ContentBlockingTelemetryService::ReportStoragePermissionExpire() { + MOZ_ASSERT(XRE_IsParentProcess()); + + LOG(("Start to report storage permission expire.")); + + PermissionManager* permManager = PermissionManager::GetInstance(); + if (NS_WARN_IF(!permManager)) { + LOG(("Permission manager is null, bailing out early")); + return; + } + + nsTArray<RefPtr<nsIPermission>> permissions; + nsresult rv = + permManager->GetAllWithTypePrefix("3rdPartyStorage"_ns, permissions); + + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Fail to get all storage access permissions.")); + return; + } + nsTArray<RefPtr<nsIPermission>> framePermissions; + rv = permManager->GetAllWithTypePrefix("3rdPartyFrameStorage"_ns, + framePermissions); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Fail to get all frame storage access permissions.")); + return; + } + if (!permissions.AppendElements(framePermissions, fallible)) { + LOG(("Fail to combine all storage access permissions.")); + return; + } + + nsTArray<uint32_t> records; + + for (const auto& permission : permissions) { + if (!permission) { + LOG(("Couldn't get the permission for unknown reasons")); + continue; + } + + uint32_t expireType; + rv = permission->GetExpireType(&expireType); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Couldn't get the expire type.")); + continue; + } + + // We only care about permissions that have a EXPIRE_TIME as the expire + // type. + if (expireType != nsIPermissionManager::EXPIRE_TIME) { + continue; + } + + // Collect how much longer the storage permission will be valid for, in + // days. + int64_t expirationTime = 0; + rv = permission->GetExpireTime(&expirationTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Couldn't get the expire time.")); + continue; + } + + expirationTime -= (PR_Now() / PR_USEC_PER_MSEC); + + // Skip expired permissions. + if (expirationTime <= 0) { + continue; + } + + int64_t expireDays = expirationTime / 1000 / 60 / 60 / 24; + + records.AppendElement(expireDays); + } + + if (!records.IsEmpty()) { + Telemetry::Accumulate(Telemetry::STORAGE_ACCESS_REMAINING_DAYS, records); + } +} diff --git a/toolkit/components/antitracking/ContentBlockingTelemetryService.h b/toolkit/components/antitracking/ContentBlockingTelemetryService.h new file mode 100644 index 0000000000..24fd57faec --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingTelemetryService.h @@ -0,0 +1,31 @@ +/* -*- 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/. */ + +#ifndef mozilla_contentblockingtelemetryservice_h +#define mozilla_contentblockingtelemetryservice_h + +#include "nsIObserver.h" + +namespace mozilla { + +class ContentBlockingTelemetryService final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + static already_AddRefed<ContentBlockingTelemetryService> GetSingleton(); + + private: + ContentBlockingTelemetryService() = default; + + ~ContentBlockingTelemetryService() = default; + + void ReportStoragePermissionExpire(); +}; + +} // namespace mozilla + +#endif // mozilla_contentblockingtelemetryservice_h diff --git a/toolkit/components/antitracking/ContentBlockingUserInteraction.cpp b/toolkit/components/antitracking/ContentBlockingUserInteraction.cpp new file mode 100644 index 0000000000..452b4dd409 --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingUserInteraction.cpp @@ -0,0 +1,101 @@ +/* -*- 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 "ContentBlockingUserInteraction.h" +#include "AntiTrackingUtils.h" + +#include "mozilla/BounceTrackingProtection.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/PermissionManager.h" +#include "nsIPrincipal.h" +#include "nsXULAppAPI.h" +#include "prtime.h" + +namespace mozilla { + +/* static */ +void ContentBlockingUserInteraction::Observe(nsIPrincipal* aPrincipal) { + if (!aPrincipal || aPrincipal->IsSystemPrincipal()) { + // The content process may have sent us garbage data. + return; + } + + if (XRE_IsParentProcess()) { + LOG_PRIN(("Saving the userInteraction for %s", _spec), aPrincipal); + + // The bounce tracking protection has its own interaction store. + RefPtr<BounceTrackingProtection> bounceTrackingProtection = + BounceTrackingProtection::GetSingleton(); + // May be nullptr if the feature is disabled. + if (bounceTrackingProtection) { + nsresult rv = bounceTrackingProtection->RecordUserActivation(aPrincipal); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("BounceTrackingProtection::RecordUserActivation failed.")); + } + } + + PermissionManager* permManager = PermissionManager::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)); + + if (StaticPrefs::privacy_antitracking_testing()) { + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + obs->NotifyObservers( + nullptr, "antitracking-test-user-interaction-perm-added", nullptr); + } + return; + } + + dom::ContentChild* cc = dom::ContentChild::GetSingleton(); + MOZ_ASSERT(cc); + + LOG_PRIN(("Asking the parent process to save the user-interaction for us: %s", + _spec), + aPrincipal); + cc->SendStoreUserInteractionAsPermission(aPrincipal); +} + +/* static */ +bool ContentBlockingUserInteraction::Exists(nsIPrincipal* aPrincipal) { + PermissionManager* permManager = PermissionManager::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; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/ContentBlockingUserInteraction.h b/toolkit/components/antitracking/ContentBlockingUserInteraction.h new file mode 100644 index 0000000000..503990ae82 --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingUserInteraction.h @@ -0,0 +1,29 @@ +/* -*- 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/. */ + +#ifndef mozilla_contentblockinguserinteraction_h +#define mozilla_contentblockinguserinteraction_h + +#define USER_INTERACTION_PERM "storageAccessAPI"_ns + +class nsIPrincipal; + +namespace mozilla { + +class ContentBlockingUserInteraction final { + public: + // Used to remember that we observed a user interaction that is significant + // for content blocking. + static void Observe(nsIPrincipal* aPrincipal); + + // Used to query whether we've observed a user interaction that is significant + // for content blocking for the given principal in the past. + static bool Exists(nsIPrincipal* aPrincipal); +}; + +} // namespace mozilla + +#endif // mozilla_contentblockinguserinteraction_h diff --git a/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.cpp b/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.cpp new file mode 100644 index 0000000000..9a58cf523f --- /dev/null +++ b/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.cpp @@ -0,0 +1,356 @@ +/* -*- 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 "DynamicFpiRedirectHeuristic.h" +#include "ContentBlockingAllowList.h" +#include "ContentBlockingUserInteraction.h" +#include "StorageAccessAPIHelper.h" + +#include "mozilla/net/HttpBaseChannel.h" +#include "mozilla/net/UrlClassifierCommon.h" +#include "mozilla/Telemetry.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsICookieJarSettings.h" +#include "nsICookieService.h" +#include "nsIEffectiveTLDService.h" +#include "nsINavHistoryService.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIScriptError.h" +#include "nsIURI.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsScriptSecurityManager.h" +#include "nsToolkitCompsCID.h" + +namespace mozilla { + +namespace { + +nsresult GetBaseDomain(nsIURI* aURI, nsACString& aBaseDomain) { + nsCOMPtr<nsIEffectiveTLDService> tldService = + do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + + if (!tldService) { + return NS_ERROR_FAILURE; + } + + return tldService->GetBaseDomain(aURI, 0, aBaseDomain); +} + +// check if there's any interacting visit within the given seconds +bool HasEligibleVisit( + nsIURI* aURI, + int64_t aSinceInSec = StaticPrefs:: + privacy_restrict3rdpartystorage_heuristic_recently_visited_time()) { + nsresult rv; + + nsAutoCString baseDomain; + rv = GetBaseDomain(aURI, baseDomain); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr<nsINavHistoryService> histSrv = + do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID); + if (!histSrv) { + return false; + } + nsCOMPtr<nsINavHistoryQuery> histQuery; + rv = histSrv->GetNewQuery(getter_AddRefs(histQuery)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + rv = histQuery->SetDomain(baseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + rv = histQuery->SetDomainIsHost(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + PRTime beginTime = PR_Now() - PRTime(PR_USEC_PER_SEC) * aSinceInSec; + rv = histQuery->SetBeginTime(beginTime); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsCOMPtr<nsINavHistoryQueryOptions> histQueryOpts; + rv = histSrv->GetNewQueryOptions(getter_AddRefs(histQueryOpts)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + rv = + histQueryOpts->SetResultType(nsINavHistoryQueryOptions::RESULTS_AS_VISIT); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + rv = histQueryOpts->SetMaxResults(1); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + rv = histQueryOpts->SetQueryType( + nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsCOMPtr<nsINavHistoryResult> histResult; + rv = histSrv->ExecuteQuery(histQuery, histQueryOpts, + getter_AddRefs(histResult)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsCOMPtr<nsINavHistoryContainerResultNode> histResultContainer; + rv = histResult->GetRoot(getter_AddRefs(histResultContainer)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + rv = histResultContainer->SetContainerOpen(true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + uint32_t childCount = 0; + rv = histResultContainer->GetChildCount(&childCount); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + rv = histResultContainer->SetContainerOpen(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return childCount > 0; +} + +void AddConsoleReport(nsIChannel* aNewChannel, nsIURI* aNewURI, + const nsACString& aOldOrigin, + const nsACString& aNewOrigin) { + nsCOMPtr<net::HttpBaseChannel> httpChannel = do_QueryInterface(aNewChannel); + if (!httpChannel) { + return; + } + + nsAutoCString uri; + nsresult rv = aNewURI->GetSpec(uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + AutoTArray<nsString, 2> params = {NS_ConvertUTF8toUTF16(aNewOrigin), + NS_ConvertUTF8toUTF16(aOldOrigin)}; + + httpChannel->AddConsoleReport(nsIScriptError::warningFlag, + ANTITRACKING_CONSOLE_CATEGORY, + nsContentUtils::eNECKO_PROPERTIES, uri, 0, 0, + "CookieAllowedForFpiByHeuristic"_ns, params); +} + +bool ShouldRedirectHeuristicApplyTrackingResource(nsIChannel* aOldChannel, + nsIURI* aOldURI, + nsIChannel* aNewChannel, + nsIURI* aNewURI) { + nsCOMPtr<nsIClassifiedChannel> classifiedOldChannel = + do_QueryInterface(aOldChannel); + if (!classifiedOldChannel) { + LOG_SPEC2(("Ignoring redirect for %s to %s because there is not " + "nsIClassifiedChannel interface", + _spec1, _spec2), + aOldURI, aNewURI); + return false; + } + + // We're looking at the first-party classification flags because we're + // interested in first-party redirects. + uint32_t oldClassificationFlags = + classifiedOldChannel->GetFirstPartyClassificationFlags(); + + if (net::UrlClassifierCommon::IsTrackingClassificationFlag( + oldClassificationFlags, NS_UsePrivateBrowsing(aOldChannel))) { + // This is a redirect from tracking. + LOG_SPEC2(("Ignoring redirect for %s to %s because it's from tracking ", + _spec1, _spec2), + aOldURI, aNewURI); + return false; + } + + return true; +} + +} // namespace + +void DynamicFpiRedirectHeuristic(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_antitracking_enableWebcompat() || + !StaticPrefs:: + privacy_restrict3rdpartystorage_heuristic_recently_visited()) { + return; + } + + nsCOMPtr<nsIHttpChannel> oldChannel = do_QueryInterface(aOldChannel); + nsCOMPtr<nsIHttpChannel> newChannel = do_QueryInterface(aNewChannel); + if (!oldChannel || !newChannel) { + return; + } + + LOG_SPEC(("Checking dfpi redirect-heuristic for %s", _spec), aOldURI); + + nsCOMPtr<nsILoadInfo> oldLoadInfo = aOldChannel->LoadInfo(); + MOZ_ASSERT(oldLoadInfo); + + nsCOMPtr<nsILoadInfo> newLoadInfo = aNewChannel->LoadInfo(); + MOZ_ASSERT(newLoadInfo); + + ExtContentPolicyType contentType = + oldLoadInfo->GetExternalContentPolicyType(); + if (contentType != ExtContentPolicy::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<nsICookieJarSettings> cookieJarSettings; + rv = oldLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the cookieJarSettings")); + return; + } + + int32_t behavior = cookieJarSettings->GetCookieBehavior(); + if (behavior != + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { + LOG( + ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out " + "early", + behavior)); + return; + } + + nsIScriptSecurityManager* ssm = + nsScriptSecurityManager::GetScriptSecurityManager(); + MOZ_ASSERT(ssm); + + nsCOMPtr<nsIPrincipal> oldPrincipal; + const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& chain = + oldLoadInfo->RedirectChain(); + if (!chain.IsEmpty()) { + rv = chain[0]->GetPrincipal(getter_AddRefs(oldPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't obtain the principal from the redirect chain")); + return; + } + } else { + rv = ssm->GetChannelResultPrincipal(aOldChannel, + getter_AddRefs(oldPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't obtain the principal from the old channel")); + return; + } + } + + nsCOMPtr<nsIPrincipal> newPrincipal; + rv = + ssm->GetChannelResultPrincipal(aNewChannel, getter_AddRefs(newPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't obtain the principal from the new channel")); + return; + } + + if (oldPrincipal->Equals(newPrincipal)) { + LOG(("No permission needed for same principals.")); + return; + } + + if (!ShouldRedirectHeuristicApplyTrackingResource(aOldChannel, aOldURI, + aNewChannel, aNewURI)) { + LOG_SPEC2(("Ignoring redirect for %s to %s because tracking test failed", + _spec1, _spec2), + aOldURI, aNewURI); + return; + } + + if (!ContentBlockingUserInteraction::Exists(oldPrincipal) || + !ContentBlockingUserInteraction::Exists(newPrincipal)) { + LOG_SPEC2(("Ignoring redirect for %s to %s because no user-interaction on " + "both pages", + _spec1, _spec2), + aOldURI, aNewURI); + return; + } + + nsAutoCString oldOrigin; + rv = oldPrincipal->GetOriginNoSuffix(oldOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the origin from the old Principal")); + return; + } + + nsAutoCString newOrigin; + rv = newPrincipal->GetOriginNoSuffix(newOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the origin from the new Principal")); + return; + } + + if (!HasEligibleVisit(aOldURI) || !HasEligibleVisit(aNewURI)) { + LOG(("No previous visit record, bailing out early.")); + return; + } + + // Check if the new principal is a third party principal + bool aResult; + rv = newPrincipal->IsThirdPartyPrincipal(oldPrincipal, &aResult); + + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Error while checking if new principal is third party")); + return; + } + if (!aResult) { + LOG(("New principal is a first party principal")); + return; + } + + LOG(("Adding a first-party storage exception for %s...", + PromiseFlatCString(newOrigin).get())); + + LOG(("Saving the permission: oldOrigin=%s, grantedOrigin=%s", oldOrigin.get(), + newOrigin.get())); + + AddConsoleReport(aNewChannel, aNewURI, oldOrigin, newOrigin); + + Telemetry::AccumulateCategorical( + Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageGranted); + Telemetry::AccumulateCategorical( + Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::Redirect); + + // We don't care about this promise because the operation is actually sync. + RefPtr<StorageAccessAPIHelper::ParentAccessGrantPromise> promise = + StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess( + newPrincipal, oldPrincipal, + StorageAccessAPIHelper::StorageAccessPromptChoices::eAllow, false, + StaticPrefs::privacy_restrict3rdpartystorage_expiration_visited()); + Unused << promise; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.h b/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.h new file mode 100644 index 0000000000..cedb438cd1 --- /dev/null +++ b/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.h @@ -0,0 +1,20 @@ +/* -*- 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/. */ + +#ifndef mozilla_dynamicfpiredirectheuristic_h +#define mozilla_dynamicfpiredirectheuristic_h + +class nsIChannel; +class nsIURI; + +namespace mozilla { + +void DynamicFpiRedirectHeuristic(nsIChannel* aOldChannel, nsIURI* aOldURI, + nsIChannel* aNewChannel, nsIURI* aNewURI); + +} // namespace mozilla + +#endif // mozilla_dynamicfpiredirectheuristic_h diff --git a/toolkit/components/antitracking/PartitioningExceptionList.cpp b/toolkit/components/antitracking/PartitioningExceptionList.cpp new file mode 100644 index 0000000000..91cc40133c --- /dev/null +++ b/toolkit/components/antitracking/PartitioningExceptionList.cpp @@ -0,0 +1,221 @@ +/* -*- 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 "PartitioningExceptionList.h" + +#include "AntiTrackingLog.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { + +namespace { + +static constexpr std::array<nsLiteralCString, 2> kSupportedSchemes = { + {"https://"_ns, "http://"_ns}}; + +StaticRefPtr<PartitioningExceptionList> gPartitioningExceptionList; + +} // namespace + +NS_IMPL_ISUPPORTS(PartitioningExceptionList, + nsIPartitioningExceptionListObserver) + +bool PartitioningExceptionList::Check(const nsACString& aFirstPartyOrigin, + const nsACString& aThirdPartyOrigin) { + if (!StaticPrefs::privacy_antitracking_enableWebcompat()) { + LOG(("Partition exception list disabled via pref")); + return false; + } + + if (aFirstPartyOrigin.IsEmpty() || aFirstPartyOrigin == "null" || + aThirdPartyOrigin.IsEmpty() || aThirdPartyOrigin == "null") { + return false; + } + + LOG(("Check partitioning exception list for url %s and %s", + PromiseFlatCString(aFirstPartyOrigin).get(), + PromiseFlatCString(aThirdPartyOrigin).get())); + + for (PartitionExceptionListEntry& entry : GetOrCreate()->mExceptionList) { + if (OriginMatchesPattern(aFirstPartyOrigin, entry.mFirstParty) && + OriginMatchesPattern(aThirdPartyOrigin, entry.mThirdParty)) { + LOG(("Found partitioning exception list entry for %s and %s", + PromiseFlatCString(aFirstPartyOrigin).get(), + PromiseFlatCString(aThirdPartyOrigin).get())); + + return true; + } + } + + return false; +} + +PartitioningExceptionList* PartitioningExceptionList::GetOrCreate() { + if (!gPartitioningExceptionList) { + gPartitioningExceptionList = new PartitioningExceptionList(); + gPartitioningExceptionList->Init(); + + RunOnShutdown([&] { + gPartitioningExceptionList->Shutdown(); + gPartitioningExceptionList = nullptr; + }); + } + + return gPartitioningExceptionList; +} + +nsresult PartitioningExceptionList::Init() { + mService = + do_GetService("@mozilla.org/partitioning/exception-list-service;1"); + if (NS_WARN_IF(!mService)) { + return NS_ERROR_FAILURE; + } + + mService->RegisterAndRunExceptionListObserver(this); + return NS_OK; +} + +void PartitioningExceptionList::Shutdown() { + if (mService) { + mService->UnregisterExceptionListObserver(this); + mService = nullptr; + } + + mExceptionList.Clear(); +} + +NS_IMETHODIMP +PartitioningExceptionList::OnExceptionListUpdate(const nsACString& aList) { + mExceptionList.Clear(); + + nsresult rv; + for (const nsACString& item : aList.Split(';')) { + auto origins = item.Split(','); + auto originsIt = origins.begin(); + + if (originsIt == origins.end()) { + LOG(("Ignoring empty exception entry")); + continue; + } + + PartitionExceptionListEntry entry; + + rv = GetExceptionListPattern(*originsIt, entry.mFirstParty); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + ++originsIt; + + if (originsIt == origins.end()) { + LOG(("Ignoring incomplete exception entry")); + continue; + } + + rv = GetExceptionListPattern(*originsIt, entry.mThirdParty); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + if (entry.mFirstParty.mSuffix == "*" && entry.mThirdParty.mSuffix == "*") { + LOG(("Ignoring *,* exception entry")); + continue; + } + + LOG(("onExceptionListUpdate: %s%s - %s%s", entry.mFirstParty.mScheme.get(), + entry.mFirstParty.mSuffix.get(), entry.mThirdParty.mScheme.get(), + entry.mThirdParty.mSuffix.get())); + + mExceptionList.AppendElement(entry); + } + + return NS_OK; +} + +nsresult PartitioningExceptionList::GetSchemeFromOrigin( + const nsACString& aOrigin, nsACString& aScheme, + nsACString& aOriginNoScheme) { + NS_ENSURE_FALSE(aOrigin.IsEmpty(), NS_ERROR_INVALID_ARG); + + for (const auto& scheme : kSupportedSchemes) { + if (aOrigin.Length() <= scheme.Length() || + !StringBeginsWith(aOrigin, scheme)) { + continue; + } + aScheme = Substring(aOrigin, 0, scheme.Length()); + aOriginNoScheme = Substring(aOrigin, scheme.Length()); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +bool PartitioningExceptionList::OriginMatchesPattern( + const nsACString& aOrigin, const PartitionExceptionListPattern& aPattern) { + if (NS_WARN_IF(aOrigin.IsEmpty())) { + return false; + } + + if (aPattern.mSuffix == "*") { + return true; + } + + nsAutoCString scheme, originNoScheme; + nsresult rv = GetSchemeFromOrigin(aOrigin, scheme, originNoScheme); + NS_ENSURE_SUCCESS(rv, false); + + // Always strict match scheme. + if (scheme != aPattern.mScheme) { + return false; + } + + if (!aPattern.mIsWildCard) { + // aPattern is not a wildcard, match strict. + return originNoScheme == aPattern.mSuffix; + } + + // For wildcard patterns, check if origin suffix matches pattern suffix. + return StringEndsWith(originNoScheme, aPattern.mSuffix); +} + +// Parses a string with an origin or an origin-pattern into a +// PartitionExceptionListPattern. +nsresult PartitioningExceptionList::GetExceptionListPattern( + const nsACString& aOriginPattern, PartitionExceptionListPattern& aPattern) { + NS_ENSURE_FALSE(aOriginPattern.IsEmpty(), NS_ERROR_INVALID_ARG); + + if (aOriginPattern == "*") { + aPattern.mIsWildCard = true; + aPattern.mSuffix = "*"; + + return NS_OK; + } + + nsAutoCString originPatternNoScheme; + nsresult rv = GetSchemeFromOrigin(aOriginPattern, aPattern.mScheme, + originPatternNoScheme); + NS_ENSURE_SUCCESS(rv, rv); + + if (StringBeginsWith(originPatternNoScheme, "*"_ns)) { + NS_ENSURE_TRUE(originPatternNoScheme.Length() > 2, NS_ERROR_INVALID_ARG); + + aPattern.mIsWildCard = true; + aPattern.mSuffix = Substring(originPatternNoScheme, 1); + + return NS_OK; + } + + aPattern.mIsWildCard = false; + aPattern.mSuffix = originPatternNoScheme; + + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/PartitioningExceptionList.h b/toolkit/components/antitracking/PartitioningExceptionList.h new file mode 100644 index 0000000000..250a636340 --- /dev/null +++ b/toolkit/components/antitracking/PartitioningExceptionList.h @@ -0,0 +1,65 @@ +/* -*- 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/. */ + +#ifndef mozilla_PartitioningExceptionList_h +#define mozilla_PartitioningExceptionList_h + +#include "nsCOMPtr.h" +#include "nsIPartitioningExceptionListService.h" +#include "nsTArray.h" +#include "nsString.h" + +class nsIChannel; +class nsIPrincipal; + +namespace mozilla { + +class PartitioningExceptionList : public nsIPartitioningExceptionListObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPARTITIONINGEXCEPTIONLISTOBSERVER + + static bool Check(const nsACString& aFirstPartyOrigin, + const nsACString& aThirdPartyOrigin); + + private: + static PartitioningExceptionList* GetOrCreate(); + + PartitioningExceptionList() = default; + virtual ~PartitioningExceptionList() = default; + + nsresult Init(); + void Shutdown(); + + struct PartitionExceptionListPattern { + nsCString mScheme; + nsCString mSuffix; + bool mIsWildCard = false; + }; + + struct PartitionExceptionListEntry { + PartitionExceptionListPattern mFirstParty; + PartitionExceptionListPattern mThirdParty; + }; + + static nsresult GetSchemeFromOrigin(const nsACString& aOrigin, + nsACString& aScheme, + nsACString& aOriginNoScheme); + + static bool OriginMatchesPattern( + const nsACString& aOrigin, const PartitionExceptionListPattern& aPattern); + + static nsresult GetExceptionListPattern( + const nsACString& aOriginPattern, + PartitionExceptionListPattern& aPattern); + + nsCOMPtr<nsIPartitioningExceptionListService> mService; + nsTArray<PartitionExceptionListEntry> mExceptionList; +}; + +} // namespace mozilla + +#endif // mozilla_PartitioningExceptionList_h diff --git a/toolkit/components/antitracking/PartitioningExceptionListService.sys.mjs b/toolkit/components/antitracking/PartitioningExceptionListService.sys.mjs new file mode 100644 index 0000000000..8df2bc651d --- /dev/null +++ b/toolkit/components/antitracking/PartitioningExceptionListService.sys.mjs @@ -0,0 +1,142 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", +}); + +const COLLECTION_NAME = "partitioning-exempt-urls"; +const PREF_NAME = "privacy.restrict3rdpartystorage.skip_list"; + +class Feature { + constructor() { + this.prefName = PREF_NAME; + this.observers = new Set(); + this.prefValue = []; + this.remoteEntries = []; + + if (this.prefName) { + let prefValue = Services.prefs.getStringPref(this.prefName, null); + this.prefValue = prefValue ? prefValue.split(";") : []; + Services.prefs.addObserver(this.prefName, this); + } + } + + async addAndRunObserver(observer) { + this.observers.add(observer); + this.notifyObservers(observer); + } + + removeObserver(observer) { + this.observers.delete(observer); + } + + observe(subject, topic, data) { + if (topic != "nsPref:changed" || data != this.prefName) { + console.error(`Unexpected event ${topic} with ${data}`); + return; + } + + let prefValue = Services.prefs.getStringPref(this.prefName, null); + this.prefValue = prefValue ? prefValue.split(";") : []; + this.notifyObservers(); + } + + onRemoteSettingsUpdate(entries) { + this.remoteEntries = []; + + for (let entry of entries) { + this.remoteEntries.push( + `${entry.firstPartyOrigin},${entry.thirdPartyOrigin}` + ); + } + } + + notifyObservers(observer = null) { + let entries = this.prefValue.concat(this.remoteEntries); + let entriesAsString = entries.join(";").toLowerCase(); + if (observer) { + observer.onExceptionListUpdate(entriesAsString); + } else { + for (let obs of this.observers) { + obs.onExceptionListUpdate(entriesAsString); + } + } + } +} + +export function PartitioningExceptionListService() {} + +PartitioningExceptionListService.prototype = { + classID: Components.ID("{ab94809d-33f0-4f28-af38-01efbd3baf22}"), + QueryInterface: ChromeUtils.generateQI([ + "nsIPartitioningExceptionListService", + ]), + + _initialized: false, + + async lazyInit() { + if (this._initialized) { + return; + } + + this.feature = new Feature(); + + let rs = lazy.RemoteSettings(COLLECTION_NAME); + rs.on("sync", event => { + let { + data: { current }, + } = event; + this.onUpdateEntries(current); + }); + + this._initialized = true; + + let entries; + // If the remote settings list hasn't been populated yet we have to make sure + // to do it before firing the first notification. + // This has to be run after _initialized is set because we'll be + // blocked while getting entries from RemoteSetting, and we don't want + // LazyInit is executed again. + try { + // The data will be initially available from the local DB (via a + // resource:// URI). + entries = await rs.get(); + } catch (e) {} + + // RemoteSettings.get() could return null, ensure passing a list to + // onUpdateEntries. + this.onUpdateEntries(entries || []); + }, + + onUpdateEntries(entries) { + if (!this.feature) { + return; + } + this.feature.onRemoteSettingsUpdate(entries); + this.feature.notifyObservers(); + }, + + registerAndRunExceptionListObserver(observer) { + // We don't await this; the caller is C++ and won't await this function, + // and because we prevent re-entering into this method, once it's been + // called once any subsequent calls will early-return anyway - so + // awaiting that would be meaningless. Instead, `Feature` implementations + // make sure not to call into observers until they have data, and we + // make sure to let feature instances know whether we have data + // immediately. + this.lazyInit(); + + this.feature.addAndRunObserver(observer); + }, + + unregisterExceptionListObserver(observer) { + if (!this.feature) { + return; + } + this.feature.removeObserver(observer); + }, +}; diff --git a/toolkit/components/antitracking/PurgeTrackerService.sys.mjs b/toolkit/components/antitracking/PurgeTrackerService.sys.mjs new file mode 100644 index 0000000000..08b5612287 --- /dev/null +++ b/toolkit/components/antitracking/PurgeTrackerService.sys.mjs @@ -0,0 +1,541 @@ +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const THREE_DAYS_MS = 3 * 24 * 60 * 1000; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gClassifier", + "@mozilla.org/url-classifier/dbservice;1", + "nsIURIClassifier" +); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gStorageActivityService", + "@mozilla.org/storage/activity-service;1", + "nsIStorageActivityService" +); + +ChromeUtils.defineLazyGetter(lazy, "gClassifierFeature", () => { + return lazy.gClassifier.getFeatureByName("tracking-annotation"); +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => { + return console.createInstance({ + prefix: "*** PurgeTrackerService:", + maxLogLevelPref: "privacy.purge_trackers.logging.level", + }); +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gConsiderEntityList", + "privacy.purge_trackers.consider_entity_list" +); + +export function PurgeTrackerService() {} + +PurgeTrackerService.prototype = { + classID: Components.ID("{90d1fd17-2018-4e16-b73c-a04a26fa6dd4}"), + QueryInterface: ChromeUtils.generateQI(["nsIPurgeTrackerService"]), + + // Purging is batched for cookies to avoid clearing too much data + // at once. This flag tells us whether this is the first daily iteration. + _firstIteration: true, + + // We can only know asynchronously if a host is matched by the tracking + // protection list, so we cache the result for faster future lookups. + _trackingState: new Map(), + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "idle-daily": + // only allow one idle-daily listener to trigger until the list has been fully parsed. + Services.obs.removeObserver(this, "idle-daily"); + this.purgeTrackingCookieJars(); + break; + case "profile-after-change": + Services.obs.addObserver(this, "idle-daily"); + break; + } + }, + + async isTracker(principal) { + if (principal.isNullPrincipal || principal.isSystemPrincipal) { + return false; + } + let host; + try { + host = principal.asciiHost; + } catch (error) { + return false; + } + + if (!this._trackingState.has(host)) { + // Temporarily set to false to avoid doing several lookups if a site has + // several subframes on the same domain. + this._trackingState.set(host, false); + + await new Promise(resolve => { + try { + lazy.gClassifier.asyncClassifyLocalWithFeatures( + principal.URI, + [lazy.gClassifierFeature], + Ci.nsIUrlClassifierFeature.blocklist, + list => { + if (list.length) { + this._trackingState.set(host, true); + } + resolve(); + } + ); + } catch { + // Error in asyncClassifyLocalWithFeatures, it is not a tracker. + this._trackingState.set(host, false); + resolve(); + } + }); + } + + return this._trackingState.get(host); + }, + + isAllowedThirdParty(firstPartyOriginNoSuffix, thirdPartyHost) { + let uri = Services.io.newURI( + `${firstPartyOriginNoSuffix}/?resource=${thirdPartyHost}` + ); + lazy.logger.debug(`Checking entity list state for`, uri.spec); + return new Promise(resolve => { + try { + lazy.gClassifier.asyncClassifyLocalWithFeatures( + uri, + [lazy.gClassifierFeature], + Ci.nsIUrlClassifierFeature.entitylist, + list => { + let sameList = !!list.length; + lazy.logger.debug(`Is ${uri.spec} on the entity list?`, sameList); + resolve(sameList); + } + ); + } catch { + resolve(false); + } + }); + }, + + async maybePurgePrincipal(principal) { + let origin = principal.origin; + lazy.logger.debug(`Maybe purging ${origin}.`); + + // First, check if any site with that base domain had received + // user interaction in the last N days. + let hasInteraction = this._baseDomainsWithInteraction.has( + principal.baseDomain + ); + // Exit early unless we want to see if we're dealing with a tracker, + // for telemetry. + if (hasInteraction && !Services.telemetry.canRecordPrereleaseData) { + lazy.logger.debug(`${origin} has user interaction, exiting.`); + return; + } + + // Second, confirm that we're looking at a tracker. + let isTracker = await this.isTracker(principal); + if (!isTracker) { + lazy.logger.debug(`${origin} is not a tracker, exiting.`); + return; + } + + if (hasInteraction) { + let expireTimeMs = this._baseDomainsWithInteraction.get( + principal.baseDomain + ); + + // Collect how much longer the user interaction will be valid for, in hours. + let timeRemaining = Math.floor( + (expireTimeMs - Date.now()) / 1000 / 60 / 60 / 24 + ); + let permissionAgeHistogram = Services.telemetry.getHistogramById( + "COOKIE_PURGING_TRACKERS_USER_INTERACTION_REMAINING_DAYS" + ); + permissionAgeHistogram.add(timeRemaining); + + this._telemetryData.notPurged.add(principal.baseDomain); + + lazy.logger.debug(`${origin} is a tracker with interaction, exiting.`); + return; + } + + let isAllowedThirdParty = false; + if ( + lazy.gConsiderEntityList || + Services.telemetry.canRecordPrereleaseData + ) { + for (let firstPartyPrincipal of this._principalsWithInteraction) { + if ( + await this.isAllowedThirdParty( + firstPartyPrincipal.originNoSuffix, + principal.asciiHost + ) + ) { + isAllowedThirdParty = true; + break; + } + } + } + + if (isAllowedThirdParty && lazy.gConsiderEntityList) { + lazy.logger.debug( + `${origin} has interaction on the entity list, exiting.` + ); + return; + } + + lazy.logger.log("Deleting data from:", origin); + + await new Promise(resolve => { + Services.clearData.deleteDataFromPrincipal( + principal, + false, + Ci.nsIClearDataService.CLEAR_ALL_CACHES | + Ci.nsIClearDataService.CLEAR_COOKIES | + Ci.nsIClearDataService.CLEAR_DOM_STORAGES | + Ci.nsIClearDataService.CLEAR_CLIENT_AUTH_REMEMBER_SERVICE | + Ci.nsIClearDataService.CLEAR_EME | + Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES | + Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS | + Ci.nsIClearDataService.CLEAR_AUTH_TOKENS | + Ci.nsIClearDataService.CLEAR_AUTH_CACHE | + Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD, + resolve + ); + }); + lazy.logger.log(`Data deleted from:`, origin); + + this._telemetryData.purged.add(principal.baseDomain); + }, + + resetPurgeList() { + // We've reached the end of the cookies. + // Restore the idle-daily listener so it will purge again tomorrow. + Services.obs.addObserver(this, "idle-daily"); + // Set the date to 0 so we will start at the beginning of the list next time. + Services.prefs.setStringPref( + "privacy.purge_trackers.date_in_cookie_database", + "0" + ); + }, + + submitTelemetry() { + let { purged, notPurged, durationIntervals } = this._telemetryData; + let now = Date.now(); + let lastPurge = Number( + Services.prefs.getStringPref("privacy.purge_trackers.last_purge", now) + ); + + let intervalHistogram = Services.telemetry.getHistogramById( + "COOKIE_PURGING_INTERVAL_HOURS" + ); + let hoursBetween = Math.floor((now - lastPurge) / 1000 / 60 / 60); + intervalHistogram.add(hoursBetween); + + Services.prefs.setStringPref( + "privacy.purge_trackers.last_purge", + now.toString() + ); + + let purgedHistogram = Services.telemetry.getHistogramById( + "COOKIE_PURGING_ORIGINS_PURGED" + ); + purgedHistogram.add(purged.size); + + let notPurgedHistogram = Services.telemetry.getHistogramById( + "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION" + ); + notPurgedHistogram.add(notPurged.size); + + let duration = durationIntervals + .map(([start, end]) => end - start) + .reduce((acc, cur) => acc + cur, 0); + + let durationHistogram = Services.telemetry.getHistogramById( + "COOKIE_PURGING_DURATION_MS" + ); + durationHistogram.add(duration); + }, + + /* + * Checks Cookie Permission a given 2 principals + * if either prinicpial cookie permissions are to prevent purging + * the function would return true + */ + checkCookiePermissions(httpsPrincipal, httpPrincipal) { + let httpsCookiePermission; + let httpCookiePermission; + + if (httpPrincipal) { + httpCookiePermission = Services.perms.testPermissionFromPrincipal( + httpPrincipal, + "cookie" + ); + } + + if (httpsPrincipal) { + httpsCookiePermission = Services.perms.testPermissionFromPrincipal( + httpsPrincipal, + "cookie" + ); + } + + if ( + httpCookiePermission == Ci.nsICookiePermission.ACCESS_ALLOW || + httpsCookiePermission == Ci.nsICookiePermission.ACCESS_ALLOW + ) { + return true; + } + + return false; + }, + /** + * This loops through all cookies saved in the database and checks if they are a tracking cookie, if it is it checks + * that they have an interaction permission which is still valid. If the Permission is not valid we delete all data + * associated with the site that owns that cookie. + */ + async purgeTrackingCookieJars() { + let purgeEnabled = Services.prefs.getBoolPref( + "privacy.purge_trackers.enabled", + false + ); + + let sanitizeOnShutdownEnabled = Services.prefs.getBoolPref( + "privacy.sanitize.sanitizeOnShutdown", + false + ); + + let clearHistoryOnShutdown = Services.prefs.getBoolPref( + "privacy.clearOnShutdown.history", + false + ); + + let clearSiteSettingsOnShutdown = Services.prefs.getBoolPref( + "privacy.clearOnShutdown.siteSettings", + false + ); + + // This is a hotfix for bug 1672394. It avoids purging if the user has enabled mechanisms + // that regularly clear the storageAccessAPI permission, such as clearing history or + // "site settings" (permissions) on shutdown. + if ( + sanitizeOnShutdownEnabled && + (clearHistoryOnShutdown || clearSiteSettingsOnShutdown) + ) { + lazy.logger.log( + ` + Purging canceled because interaction permissions are cleared on shutdown. + sanitizeOnShutdownEnabled: ${sanitizeOnShutdownEnabled}, + clearHistoryOnShutdown: ${clearHistoryOnShutdown}, + clearSiteSettingsOnShutdown: ${clearSiteSettingsOnShutdown}, + ` + ); + this.resetPurgeList(); + return; + } + + // Purge cookie jars for following cookie behaviors. + // * BEHAVIOR_REJECT_FOREIGN + // * BEHAVIOR_LIMIT_FOREIGN + // * BEHAVIOR_REJECT_TRACKER (ETP) + // * BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN (dFPI) + let cookieBehavior = Services.cookies.getCookieBehavior(false); + + let activeWithCookieBehavior = + cookieBehavior == Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN || + cookieBehavior == Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN || + cookieBehavior == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER || + cookieBehavior == + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + + if (!activeWithCookieBehavior || !purgeEnabled) { + lazy.logger.log( + `returning early, activeWithCookieBehavior: ${activeWithCookieBehavior}, purgeEnabled: ${purgeEnabled}` + ); + this.resetPurgeList(); + return; + } + lazy.logger.log("Purging trackers enabled, beginning batch."); + // How many cookies to loop through in each batch before we quit + const MAX_PURGE_COUNT = Services.prefs.getIntPref( + "privacy.purge_trackers.max_purge_count", + 100 + ); + + if (this._firstIteration) { + this._telemetryData = { + durationIntervals: [], + purged: new Set(), + notPurged: new Set(), + }; + + this._baseDomainsWithInteraction = new Map(); + this._principalsWithInteraction = []; + for (let perm of Services.perms.getAllWithTypePrefix( + "storageAccessAPI" + )) { + this._baseDomainsWithInteraction.set( + perm.principal.baseDomain, + perm.expireTime + ); + this._principalsWithInteraction.push(perm.principal); + } + } + + // Record how long this iteration took for telemetry. + // This is a tuple of start and end time, the second + // part will be added at the end of this function. + let duration = [Cu.now()]; + + /** + * We record the creationTime of the last cookie we looked at and + * start from there next time. This way even if new cookies are added or old ones are deleted we + * have a reliable way of finding our spot. + **/ + let saved_date = Services.prefs.getStringPref( + "privacy.purge_trackers.date_in_cookie_database", + "0" + ); + + let maybeClearPrincipals = new Map(); + + // TODO We only need the host name and creationTime, this gives too much info. See bug 1610373. + let cookies = Services.cookies.getCookiesSince(saved_date); + cookies = cookies.slice(0, MAX_PURGE_COUNT); + + for (let cookie of cookies) { + let httpPrincipal; + let httpsPrincipal; + + let origin = + "http://" + + cookie.rawHost + + ChromeUtils.originAttributesToSuffix(cookie.originAttributes); + try { + httpPrincipal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + } catch (e) { + lazy.logger.error( + `Creating principal from origin ${origin} led to error ${e}.` + ); + } + + origin = + "https://" + + cookie.rawHost + + ChromeUtils.originAttributesToSuffix(cookie.originAttributes); + try { + httpsPrincipal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + } catch (e) { + lazy.logger.error( + `Creating principal from origin ${origin} led to error ${e}.` + ); + } + + // Checking to see if the Cookie Permissions is set to prevent Cookie from + // purging for either the HTTPS or HTTP conncetions + let purgeCheck = this.checkCookiePermissions( + httpsPrincipal, + httpPrincipal + ); + + if (httpPrincipal && !purgeCheck) { + maybeClearPrincipals.set(httpPrincipal.origin, httpPrincipal); + } + if (httpsPrincipal && !purgeCheck) { + maybeClearPrincipals.set(httpsPrincipal.origin, httpsPrincipal); + } + + saved_date = cookie.creationTime; + } + + // We only consider recently active storage and don't batch it, + // so only do this in the first iteration. + if (this._firstIteration) { + let startDate = Date.now() - THREE_DAYS_MS; + let storagePrincipals = lazy.gStorageActivityService.getActiveOrigins( + startDate * 1000, + Date.now() * 1000 + ); + + for (let principal of storagePrincipals.enumerate()) { + // Check Principal Domains Cookie Permissions for both Schemes + // To ensure it does not bypass the cookie permissions set by the user + if (principal.schemeIs("https") || principal.schemeIs("http")) { + let otherURI; + let otherPrincipal; + + if (principal.schemeIs("https")) { + otherURI = principal.URI.mutate().setScheme("http").finalize(); + } else if (principal.schemeIs("http")) { + otherURI = principal.URI.mutate().setScheme("https").finalize(); + } + + try { + otherPrincipal = + Services.scriptSecurityManager.createContentPrincipal( + otherURI, + {} + ); + } catch (e) { + lazy.logger.error( + `Creating principal from URI ${otherURI} led to error ${e}.` + ); + } + + if (!this.checkCookiePermissions(principal, otherPrincipal)) { + maybeClearPrincipals.set(principal.origin, principal); + } + } else { + maybeClearPrincipals.set(principal.origin, principal); + } + } + } + + for (let principal of maybeClearPrincipals.values()) { + await this.maybePurgePrincipal(principal); + } + + Services.prefs.setStringPref( + "privacy.purge_trackers.date_in_cookie_database", + saved_date + ); + + duration.push(Cu.now()); + this._telemetryData.durationIntervals.push(duration); + + // We've reached the end, no need to repeat again until next idle-daily. + if (!cookies.length || cookies.length < 100) { + lazy.logger.log( + "All cookie purging finished, resetting list until tomorrow." + ); + this.resetPurgeList(); + this.submitTelemetry(); + this._firstIteration = true; + return; + } + + lazy.logger.log("Batch finished, queueing next batch."); + this._firstIteration = false; + Services.tm.idleDispatchToMainThread(() => { + this.purgeTrackingCookieJars(); + }); + }, +}; diff --git a/toolkit/components/antitracking/SettingsChangeObserver.cpp b/toolkit/components/antitracking/SettingsChangeObserver.cpp new file mode 100644 index 0000000000..eb2ba5bbd3 --- /dev/null +++ b/toolkit/components/antitracking/SettingsChangeObserver.cpp @@ -0,0 +1,116 @@ +/* -*- 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 "SettingsChangeObserver.h" +#include "ContentBlockingUserInteraction.h" + +#include "mozilla/Services.h" +#include "mozilla/Preferences.h" +#include "nsIObserverService.h" +#include "nsIPermission.h" +#include "nsTArray.h" + +using namespace mozilla; + +namespace { + +UniquePtr<nsTArray<SettingsChangeObserver::AntiTrackingSettingsChangedCallback>> + gSettingsChangedCallbacks; + +} + +NS_IMPL_ISUPPORTS(SettingsChangeObserver, nsIObserver) + +NS_IMETHODIMP SettingsChangeObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, "xpcom-shutdown")) { + nsCOMPtr<nsIObserverService> 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<nsIPermission> 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(); + } + } +} + +// static +void SettingsChangeObserver::OnAntiTrackingSettingsChanged( + const SettingsChangeObserver::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<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + RefPtr<SettingsChangeObserver> 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<nsTArray<AntiTrackingSettingsChangedCallback>>(); + + initialized = true; + } + + gSettingsChangedCallbacks->AppendElement(aCallback); +} diff --git a/toolkit/components/antitracking/SettingsChangeObserver.h b/toolkit/components/antitracking/SettingsChangeObserver.h new file mode 100644 index 0000000000..961942deac --- /dev/null +++ b/toolkit/components/antitracking/SettingsChangeObserver.h @@ -0,0 +1,39 @@ +/* -*- 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/. */ + +#ifndef mozilla_settingschangeobserver_h +#define mozilla_settingschangeobserver_h + +#include "nsIObserver.h" + +#include <functional> + +namespace mozilla { + +class SettingsChangeObserver final : public nsIObserver { + ~SettingsChangeObserver() = default; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + // This API allows consumers to get notified when the anti-tracking component + // settings change. After this callback is called, an anti-tracking check + // that has been previously performed with the same parameters may now return + // a different result. + using AntiTrackingSettingsChangedCallback = std::function<void()>; + static void OnAntiTrackingSettingsChanged( + const AntiTrackingSettingsChangedCallback& aCallback); + + static void PrivacyPrefChanged(const char* aPref = nullptr, void* = nullptr); + + private: + static void RunAntiTrackingSettingsChangedCallbacks(); +}; + +} // namespace mozilla + +#endif // mozilla_settingschangeobserver_h diff --git a/toolkit/components/antitracking/StorageAccess.cpp b/toolkit/components/antitracking/StorageAccess.cpp new file mode 100644 index 0000000000..c18efb2b4c --- /dev/null +++ b/toolkit/components/antitracking/StorageAccess.cpp @@ -0,0 +1,878 @@ +/* -*- 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 "StorageAccess.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/Components.h" +#include "mozilla/dom/Document.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/PermissionManager.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StorageAccess.h" +#include "nsAboutProtocolUtils.h" +#include "nsContentUtils.h" +#include "nsGlobalWindowInner.h" +#include "nsICookiePermission.h" +#include "nsICookieService.h" +#include "nsICookieJarSettings.h" +#include "nsIHttpChannel.h" +#include "nsIPermission.h" +#include "nsIWebProgressListener.h" +#include "nsIClassifiedChannel.h" +#include "nsNetUtil.h" +#include "nsScriptSecurityManager.h" +#include "nsSandboxFlags.h" +#include "AntiTrackingUtils.h" +#include "AntiTrackingLog.h" +#include "ContentBlockingAllowList.h" +#include "mozIThirdPartyUtil.h" + +using namespace mozilla; +using namespace mozilla::dom; +using mozilla::net::CookieJarSettings; + +// This internal method returns ACCESS_DENY if the access is denied, +// ACCESS_DEFAULT if unknown, some other access code if granted. +uint32_t mozilla::detail::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; +} + +/* + * Checks if storage for a given principal is permitted by the user's + * preferences. + * + * Ways this function is used: + * - aPrincipal, aWindow, optional aURI, others don't care: does this principal + * have storage access, testing this window's sandboxing and if it is + * third-party. If aURI is provided, we use that for the window's third party + * comparisons. + * - aPrincipal, aChannel, aWindow=nullptr, others don't care: does this + * principal have storage access, testing if this channel is third-party. Note + * that this ignores aURI. + * - aPrincipal, optional aCookieJarSettings, aWindow=nullptr, aChannel=nullptr, + * aURI don't care: does this principal have storage access (assuming it is in a + * first-party context and not sandboxed). If we aren't given a + * cookieJarSettings, we build one with the principal. + * + * In all of these cases, we test: + * - if aPrincipal is a NullPrincipal, denying + * - if this is for an about URI, allowing (maybe with private browsing + * constraints) We test the aWindow's extant doc's URI's, aURI's, and + * aPrincipal's scheme to be "about". + * + * We also send a decision to the ContentBlockingNotifier if we have aWindow or + * aChannel and didn't stop at the NullPrincipal or about: checks. + * + * Used in the implementation of StorageAllowedForWindow, + * StorageAllowedForDocument, StorageAllowedForChannel and + * StorageAllowedForServiceWorker. + */ +static StorageAccess InternalStorageAllowedCheck( + nsIPrincipal* aPrincipal, nsPIDOMWindowInner* aWindow, nsIURI* aURI, + nsIChannel* aChannel, nsICookieJarSettings* aCookieJarSettings, + uint32_t& aRejectedReason) { + MOZ_ASSERT(aPrincipal); + + aRejectedReason = 0; + + StorageAccess access = StorageAccess::eAllow; + + // We don't allow storage on the null principal, in general. Even if the + // calling context is chrome. + if (aPrincipal->GetIsNullPrincipal()) { + return StorageAccess::eDeny; + } + + nsCOMPtr<nsIURI> documentURI; + if (aWindow) { + // If the document is sandboxed, then it is not permitted to use storage + Document* document = aWindow->GetExtantDoc(); + if (document && document->GetSandboxFlags() & SANDBOXED_ORIGIN) { + return StorageAccess::eDeny; + } + + // Check if we are in private browsing, and record that fact + if (nsContentUtils::IsInPrivateBrowsing(document)) { + access = StorageAccess::ePrivateBrowsing; + } + + // Get the document URI for the below about: URI check. + documentURI = document ? document->GetDocumentURI() : nullptr; + } + + // About URIs are allowed to access storage, even if they don't have chrome + // privileges. If this is not desired, than the consumer will have to + // implement their own restriction functionality. + // + // This is due to backwards-compatibility and the state of storage access + // before the introducton of InternalStorageAllowedCheck: + // + // BEFORE: + // localStorage, caches: allowed in 3rd-party iframes always + // IndexedDB: allowed in 3rd-party iframes only if 3rd party URI is an about: + // URI within a specific allowlist + // + // AFTER: + // localStorage, caches: allowed in 3rd-party iframes by default. Preference + // can be set to disable in 3rd-party, which will not disallow in about: + // URIs. + // IndexedDB: allowed in 3rd-party iframes by default. Preference can be set + // to disable in 3rd-party, which will disallow in about: URIs, unless they + // are within a specific allowlist. + // + // This means that behavior for storage with internal about: URIs should not + // be affected, which is desireable due to the lack of automated testing for + // about: URIs with these preferences set, and the importance of the correct + // functioning of these URIs even with custom preferences. + // + // We need to check the aURI or the document URI here instead of only checking + // the URI from the principal. Because the principal might not have a URI if + // it is a system principal. + if ((aURI && aURI->SchemeIs("about") && + !NS_IsContentAccessibleAboutURI(aURI)) || + (documentURI && documentURI->SchemeIs("about") && + !NS_IsContentAccessibleAboutURI(documentURI)) || + aPrincipal->SchemeIs("about")) { + return access; + } + + bool disabled = true; + if (aWindow) { + nsIURI* documentURI = aURI ? aURI : aWindow->GetDocumentURI(); + disabled = !documentURI || + !ShouldAllowAccessFor(aWindow, documentURI, &aRejectedReason); + ContentBlockingNotifier::OnDecision( + aWindow, + disabled ? ContentBlockingNotifier::BlockingDecision::eBlock + : ContentBlockingNotifier::BlockingDecision::eAllow, + aRejectedReason); + } else if (aChannel) { + disabled = false; + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + if (!NS_WARN_IF(NS_FAILED(rv))) { + disabled = !ShouldAllowAccessFor(aChannel, uri, &aRejectedReason); + } + ContentBlockingNotifier::OnDecision( + aChannel, + disabled ? ContentBlockingNotifier::BlockingDecision::eBlock + : ContentBlockingNotifier::BlockingDecision::eAllow, + aRejectedReason); + } else { + MOZ_ASSERT(aPrincipal); + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = aCookieJarSettings; + if (!cookieJarSettings) { + cookieJarSettings = net::CookieJarSettings::Create(aPrincipal); + } + disabled = !ShouldAllowAccessFor(aPrincipal, aCookieJarSettings); + } + + if (!disabled) { + return access; + } + + // We want to have a partitioned storage only for trackers. + if (aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER)) { + return StorageAccess::ePartitionTrackersOrDeny; + } + + // We want to have a partitioned storage for all third parties. + if (aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN)) { + return StorageAccess::ePartitionForeignOrDeny; + } + + return StorageAccess::eDeny; +} + +/** + * Wrapper around InternalStorageAllowedCheck which caches the check result on + * the inner window to improve performance. nsGlobalWindowInner is responsible + * for invalidating the cache state if storage access changes during window + * lifetime. + */ +static StorageAccess InternalStorageAllowedCheckCached( + nsIPrincipal* aPrincipal, nsPIDOMWindowInner* aWindow, nsIURI* aURI, + nsIChannel* aChannel, nsICookieJarSettings* aCookieJarSettings, + uint32_t& aRejectedReason) { + // If enabled, check if we have already computed the storage access field + // for this window. This avoids repeated calls to + // InternalStorageAllowedCheck. + nsGlobalWindowInner* win = nullptr; + if (aWindow) { + win = nsGlobalWindowInner::Cast(aWindow); + + Maybe<StorageAccess> storageAccess = + win->GetStorageAllowedCache(aRejectedReason); + if (storageAccess.isSome()) { + return storageAccess.value(); + } + } + + StorageAccess result = InternalStorageAllowedCheck( + aPrincipal, aWindow, aURI, aChannel, aCookieJarSettings, aRejectedReason); + if (win) { + // Remember check result for the lifetime of the window. It's the windows + // responsibility to invalidate this field if storage access changes + // because a storage access permission is granted. + win->SetStorageAllowedCache(result, aRejectedReason); + } + + return result; +} + +namespace mozilla { + +StorageAccess StorageAllowedForWindow(nsPIDOMWindowInner* aWindow, + uint32_t* aRejectedReason) { + uint32_t rejectedReason; + if (!aRejectedReason) { + aRejectedReason = &rejectedReason; + } + + *aRejectedReason = 0; + + if (Document* document = aWindow->GetExtantDoc()) { + nsCOMPtr<nsIPrincipal> principal = document->NodePrincipal(); + // Note that GetChannel() below may return null, but that's OK, since the + // callee is able to deal with a null channel argument, and if passed null, + // will only fail to notify the UI in case storage gets blocked. + nsIChannel* channel = document->GetChannel(); + return InternalStorageAllowedCheckCached( + principal, aWindow, nullptr, channel, document->CookieJarSettings(), + *aRejectedReason); + } + + // No document? Try checking Private Browsing Mode without document + if (const nsCOMPtr<nsIGlobalObject> global = aWindow->AsGlobal()) { + if (const nsCOMPtr<nsIPrincipal> principal = global->PrincipalOrNull()) { + if (principal->GetPrivateBrowsingId() > 0) { + return StorageAccess::ePrivateBrowsing; + } + } + } + + // Everything failed? Let's return a generic rejected reason. + return StorageAccess::eDeny; +} + +StorageAccess StorageAllowedForDocument(const Document* aDoc) { + StorageAccess cookieAllowed = CookieAllowedForDocument(aDoc); + if (StaticPrefs:: + privacy_partition_always_partition_third_party_non_cookie_storage() && + cookieAllowed > StorageAccess::eDeny) { + return StorageAccess::ePartitionForeignOrDeny; + } + return cookieAllowed; +} + +StorageAccess CookieAllowedForDocument(const Document* aDoc) { + MOZ_ASSERT(aDoc); + + if (nsPIDOMWindowInner* inner = aDoc->GetInnerWindow()) { + nsCOMPtr<nsIPrincipal> principal = aDoc->NodePrincipal(); + // Note that GetChannel() below may return null, but that's OK, since the + // callee is able to deal with a null channel argument, and if passed null, + // will only fail to notify the UI in case storage gets blocked. + nsIChannel* channel = aDoc->GetChannel(); + + uint32_t rejectedReason = 0; + return InternalStorageAllowedCheckCached( + principal, inner, nullptr, channel, + const_cast<Document*>(aDoc)->CookieJarSettings(), rejectedReason); + } + + return StorageAccess::eDeny; +} + +StorageAccess StorageAllowedForNewWindow(nsIPrincipal* aPrincipal, nsIURI* aURI, + nsPIDOMWindowInner* aParent) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aURI); + // parent may be nullptr + + uint32_t rejectedReason = 0; + nsCOMPtr<nsICookieJarSettings> cjs; + if (aParent && aParent->GetExtantDoc()) { + cjs = aParent->GetExtantDoc()->CookieJarSettings(); + } else { + cjs = net::CookieJarSettings::Create(aPrincipal); + } + return InternalStorageAllowedCheck(aPrincipal, aParent, aURI, nullptr, cjs, + rejectedReason); +} + +StorageAccess StorageAllowedForChannel(nsIChannel* aChannel) { + MOZ_DIAGNOSTIC_ASSERT(nsContentUtils::GetSecurityManager()); + MOZ_DIAGNOSTIC_ASSERT(aChannel); + + nsCOMPtr<nsIPrincipal> principal; + Unused << nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + aChannel, getter_AddRefs(principal)); + NS_ENSURE_TRUE(principal, StorageAccess::eDeny); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = + loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + NS_ENSURE_SUCCESS(rv, StorageAccess::eDeny); + + uint32_t rejectedReason = 0; + StorageAccess result = InternalStorageAllowedCheck( + principal, nullptr, nullptr, aChannel, cookieJarSettings, rejectedReason); + + return result; +} + +StorageAccess StorageAllowedForServiceWorker( + nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings) { + uint32_t rejectedReason = 0; + return InternalStorageAllowedCheck(aPrincipal, nullptr, nullptr, nullptr, + aCookieJarSettings, rejectedReason); +} + +bool ShouldPartitionStorage(StorageAccess aAccess) { + return aAccess == StorageAccess::ePartitionTrackersOrDeny || + aAccess == StorageAccess::ePartitionForeignOrDeny; +} + +bool ShouldPartitionStorage(uint32_t aRejectedReason) { + return aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER) || + aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN); +} + +bool StoragePartitioningEnabled(StorageAccess aAccess, + nsICookieJarSettings* aCookieJarSettings) { + return aAccess == StorageAccess::ePartitionForeignOrDeny && + aCookieJarSettings->GetCookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; +} + +bool StoragePartitioningEnabled(uint32_t aRejectedReason, + nsICookieJarSettings* aCookieJarSettings) { + return aRejectedReason == + static_cast<uint32_t>( + nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN) && + aCookieJarSettings->GetCookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; +} + +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(); +} + +bool CookiesBehaviorRejectsThirdPartyContexts(Document* aDocument) { + MOZ_ASSERT(aDocument); + + // WebExtensions principals always get BEHAVIOR_ACCEPT as cookieBehavior + // (See Bug 1406675 and Bug 1525917 for rationale). + if (BasePrincipal::Cast(aDocument->NodePrincipal())->AddonPolicy()) { + return false; + } + + return aDocument->CookieJarSettings()->GetRejectThirdPartyContexts(); +} + +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<nsICookieJarSettings> 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(); +} + +bool 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 = detail::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 || + 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; + } + + // The document has been allowlisted. We can return from here directly. + if (document->HasStorageAccessPermissionGrantedByAllowList()) { + return true; + } + + 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<nsIClassifiedChannel> 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_UNREACHABLE( + "This should be an exhaustive list of cookie behaviors possible here."); + } + + 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::UsingStorageAccess 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->UsingStorageAccess(); + + if (!allowed) { + *aRejectedReason = blockedReason; + } else { + if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug) && + aWindow->UsingStorageAccess()) { + LOG(("Permission stored in the window. All good.")); + } + } + + return allowed; +} + +bool 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<nsIURI> 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<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsICookieJarSettings> 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<nsIPrincipal> 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 = detail::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<nsIHttpChannel> 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<mozIThirdPartyUtil> thirdPartyUtil = + components::ThirdPartyUtil::Service(); + 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; + } + + // The channel has been allowlisted. We can return from here. + if (loadInfo->GetStoragePermission() == + nsILoadInfo::StoragePermissionAllowListed) { + return true; + } + + 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<nsIClassifiedChannel> 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_UNREACHABLE( + "This should be an exhaustive list of cookie behaviors possible here."); + } + + RefPtr<BrowsingContext> 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. + + // UsingStorageAccess 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<nsPIDOMWindowInner> inner = + AntiTrackingUtils::GetInnerWindow(targetBC); + if (inner && inner->UsingStorageAccess()) { + LOG(("Permission stored in the window. All good.")); + return true; + } + } + + bool allowed = + loadInfo->GetStoragePermission() != nsILoadInfo::NoStoragePermission; + if (!allowed) { + *aRejectedReason = blockedReason; + } + + return allowed; +} + +bool ShouldAllowAccessFor(nsIPrincipal* aPrincipal, + nsICookieJarSettings* aCookieJarSettings) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCookieJarSettings); + + uint32_t access = + detail::CheckCookiePermissionForPrincipal(aCookieJarSettings, aPrincipal); + + if (access != nsICookiePermission::ACCESS_DEFAULT) { + return access != nsICookiePermission::ACCESS_DENY; + } + + int32_t behavior = CookiesBehavior(aPrincipal, aCookieJarSettings); + return behavior != nsICookieService::BEHAVIOR_REJECT; +} + +/* static */ +bool 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 (!CookiesBehaviorRejectsThirdPartyContexts(parentDocument)) { + LOG(("Disabled by the pref (%d), bail out early", + CookiesBehavior(parentDocument))); + 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 = detail::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; + } + + nsIPrincipal* parentPrincipal = parentDocument->NodePrincipal(); + + nsCOMPtr<nsIPrincipal> principal = BasePrincipal::CreateContentPrincipal( + aURI, parentPrincipal->OriginAttributesRef()); + + nsAutoCString type; + AntiTrackingUtils::CreateStoragePermissionKey(principal, type); + + return AntiTrackingUtils::CheckStoragePermission( + parentPrincipal, type, + nsContentUtils::IsInPrivateBrowsing(parentDocument), nullptr, 0); +} +} // namespace mozilla diff --git a/toolkit/components/antitracking/StorageAccess.h b/toolkit/components/antitracking/StorageAccess.h new file mode 100644 index 0000000000..4dbd5355c0 --- /dev/null +++ b/toolkit/components/antitracking/StorageAccess.h @@ -0,0 +1,165 @@ +/* -*- 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/. */ + +#ifndef mozilla_StorageAccess_h +#define mozilla_StorageAccess_h + +#include <cstdint> + +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" + +#include "mozilla/dom/BrowsingContext.h" + +class nsIChannel; +class nsICookieJarSettings; +class nsIPrincipal; +class nsIURI; +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { +class Document; +} + +// The order of these entries matters, as we use std::min for total ordering +// of permissions. Private Browsing is considered to be more limiting +// then session scoping +enum class StorageAccess { + // The storage should be partitioned for third-party resources. if the + // caller is unable to do it, deny the storage access. + ePartitionForeignOrDeny = -2, + // The storage should be partitioned for third-party trackers. if the caller + // is unable to do it, deny the storage access. + ePartitionTrackersOrDeny = -1, + // Don't allow access to the storage + eDeny = 0, + // Allow access to the storage, but only if it is secure to do so in a + // private browsing context. + ePrivateBrowsing = 1, + // Allow access to the storage, but only persist it for the current session + eSessionScoped = 2, + // Allow access to the storage + eAllow = 3, + // Keep this at the end. Used for serialization, but not a valid value. + eNumValues = 4, +}; + +/* + * Checks if storage for the given window is permitted by a combination of + * the user's preferences, and whether the window is a third-party iframe. + * + * This logic is intended to be shared between the different forms of + * persistent storage which are available to web pages. Cookies don't use + * this logic, and security logic related to them must be updated separately. + */ +StorageAccess StorageAllowedForWindow(nsPIDOMWindowInner* aWindow, + uint32_t* aRejectedReason = nullptr); + +/* + * Checks if storage for the given document is permitted by a combination of + * the user's preferences, and whether the document's window is a third-party + * iframe. + * + * Note, this may be used on documents during the loading process where + * the window's extant document has not been set yet. The code in + * StorageAllowedForWindow(), however, will not work in these cases. + */ +StorageAccess StorageAllowedForDocument(const dom::Document* aDoc); + +StorageAccess CookieAllowedForDocument(const dom::Document* aDoc); + +/* + * Checks if storage should be allowed for a new window with the given + * principal, load URI, and parent. + */ +StorageAccess StorageAllowedForNewWindow(nsIPrincipal* aPrincipal, nsIURI* aURI, + nsPIDOMWindowInner* aParent); + +/* + * Checks if storage should be allowed for the given channel. The check will + * be based on the channel result principal and, depending on preferences and + * permissions, mozIThirdPartyUtil.isThirdPartyChannel(). + */ +StorageAccess StorageAllowedForChannel(nsIChannel* aChannel); + +/* + * Checks if storage for the given principal is permitted by the user's + * preferences. This method should be used only by ServiceWorker loading. + */ +StorageAccess StorageAllowedForServiceWorker( + nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings); + +bool ShouldPartitionStorage(StorageAccess aAccess); + +bool ShouldPartitionStorage(uint32_t aRejectedReason); + +bool StoragePartitioningEnabled(StorageAccess aAccess, + nsICookieJarSettings* aCookieJarSettings); + +bool StoragePartitioningEnabled(uint32_t aRejectedReason, + nsICookieJarSettings* aCookieJarSettings); + +// This method returns true if the URI has first party storage access when +// loaded inside the passed 3rd party context tracking resource window. +// If the window is first party context, please use +// ApproximateAllowAccessForWithoutChannel(); +// +// aRejectedReason could be set to one of these values if passed and if the +// storage permission is not granted: +// * nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION +// * nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER +// * nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER +// * nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL +// * nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN +// +// If you update this function, you almost certainly want to consider +// updating the other overloaded functions +// (and ApproximateAllowAccessForWithoutChannel). +bool ShouldAllowAccessFor(nsPIDOMWindowInner* a3rdPartyTrackingWindow, + nsIURI* aURI, uint32_t* aRejectedReason); + +// Note: you should use ShouldAllowAccessFor() passing the nsIChannel! Use +// this method _only_ if the channel is not available. For first party +// window, it's impossible to know if the aURI is a tracking resource +// synchronously, so here we return the best guest: if we are sure that the +// permission is granted for the origin of aURI, this method returns true, +// otherwise false. +// +// If you update this function, you almost certainly want to consider +// updating the ShouldAllowAccessFor functions. +bool ApproximateAllowAccessForWithoutChannel( + nsPIDOMWindowInner* aFirstPartyWindow, nsIURI* aURI); + +// It returns true if the URI has access to the first party storage. +// aChannel can be a 3rd party channel, or not. +// See ShouldAllowAccessFor(window) to see the possible values of +// aRejectedReason. +// +// If you update this function, you almost certainly want to consider +// updating the other overloaded functions +// (and ApproximateAllowAccessForWithoutChannel). +bool ShouldAllowAccessFor(nsIChannel* aChannel, nsIURI* aURI, + uint32_t* aRejectedReason); + +// This method checks if the principal has the permission to access to the +// first party storage. +// Warning: only use this function when aPrincipal is first-party. +// +// If you update this function, you almost certainly want to consider +// updating the other overloaded functions +// (and ApproximateAllowAccessForWithoutChannel). +bool ShouldAllowAccessFor(nsIPrincipal* aPrincipal, + nsICookieJarSettings* aCookieJarSettings); + +namespace detail { +uint32_t CheckCookiePermissionForPrincipal( + nsICookieJarSettings* aCookieJarSettings, nsIPrincipal* aPrincipal); +} + +} // namespace mozilla + +#endif // mozilla_StorageAccess_h diff --git a/toolkit/components/antitracking/StorageAccessAPIHelper.cpp b/toolkit/components/antitracking/StorageAccessAPIHelper.cpp new file mode 100644 index 0000000000..5baa2c2557 --- /dev/null +++ b/toolkit/components/antitracking/StorageAccessAPIHelper.cpp @@ -0,0 +1,1244 @@ +/* -*- 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 "StorageAccessAPIHelper.h" +#include "AntiTrackingUtils.h" +#include "TemporaryAccessGrantObserver.h" + +#include "mozilla/Components.h" +#include "mozilla/ContentBlockingAllowList.h" +#include "mozilla/ContentBlockingUserInteraction.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/BrowsingContextGroup.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/FeaturePolicy.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/PermissionManager.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/Telemetry.h" +#include "mozIThirdPartyUtil.h" +#include "nsContentUtils.h" +#include "nsIClassifiedChannel.h" +#include "nsICookiePermission.h" +#include "nsICookieService.h" +#include "nsIPermission.h" +#include "nsIPrincipal.h" +#include "nsIURI.h" +#include "nsIURIClassifier.h" +#include "nsIUrlClassifierFeature.h" +#include "nsIOService.h" +#include "nsIWebProgressListener.h" +#include "nsScriptSecurityManager.h" +#include "StorageAccess.h" +#include "nsStringFwd.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; +} + +} // namespace + +/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise> +StorageAccessAPIHelper::AllowAccessForHelper( + nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + nsCOMPtr<nsIPrincipal>* aTrackingPrincipal, nsACString& aTrackingOrigin, + uint64_t* aTopLevelWindowId, uint32_t* aBehavior) { + MOZ_ASSERT(aParentContext); + MOZ_ASSERT(aTrackingPrincipal); + MOZ_ASSERT(aTopLevelWindowId); + MOZ_ASSERT(aBehavior); + + switch (aReason) { + case ContentBlockingNotifier::eOpener: + if (!StaticPrefs::privacy_antitracking_enableWebcompat() || + !StaticPrefs:: + privacy_restrict3rdpartystorage_heuristic_window_open()) { + LOG( + ("Bailing out early because the window open heuristic is disabled " + "by pref")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + break; + case ContentBlockingNotifier::eOpenerAfterUserInteraction: + if (!StaticPrefs::privacy_antitracking_enableWebcompat() || + !StaticPrefs:: + privacy_restrict3rdpartystorage_heuristic_opened_window_after_interaction()) { + LOG( + ("Bailing out early because the window open after interaction " + "heuristic is disabled by pref")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + break; + default: + break; + } + + if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) { + nsAutoCString origin; + aPrincipal->GetOriginNoSuffix(origin); + LOG(("Adding a first-party storage exception for %s, triggered by %s", + PromiseFlatCString(origin).get(), + AntiTrackingUtils::GrantedReasonToString(aReason).get())); + } + + RefPtr<dom::WindowContext> 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. + *aBehavior = *parentWindowContext->GetCookieBehavior(); + if (!CookieJarSettings::IsRejectThirdPartyContexts(*aBehavior)) { + LOG( + ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out " + "early", + *aBehavior)); + return StorageAccessPermissionGrantPromise::CreateAndResolve(true, + __func__); + } + + MOZ_ASSERT( + *aBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || + *aBehavior == + 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(); + + LOG(("The current resource is %s-party", + isParentThirdParty ? "third" : "first")); + + // We are a first party resource. + if (!isParentThirdParty) { + nsAutoCString origin; + nsresult rv = aPrincipal->GetOriginNoSuffix(origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the origin from the URI")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + + aTrackingOrigin = origin; + *aTrackingPrincipal = aPrincipal; + *aTopLevelWindowId = aParentContext->GetCurrentInnerWindowId(); + if (NS_WARN_IF(!*aTopLevelWindowId)) { + 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 (*aBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER && + !parentWindowContext->GetIsThirdPartyTrackingResourceWindow()) { + LOG(("Our window isn't a third-party tracking window")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + if (*aBehavior == + 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, + *aTopLevelWindowId)) { + 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(*aTrackingPrincipal), + aTrackingOrigin)) { + LOG( + ("Error while computing the parent principal and tracking origin, " + "bailing out early")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + } + } + + // 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); + + return nullptr; +} + +// 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). +// +// We will follow below algorithm to decide if we can continue to run in +// the current process, otherwise, we need to ask the parent to continue +// the work. +// 1. Check if aParentContext is an in-process browsing context. If it isn't, +// we cannot proceed in the content process because we need the +// principal of the parent window. Otherwise, we go to step 2. +// 2. Check if the grant reason is ePrivilegeStorageAccessForOriginAPI. In +// this case, we don't need to check the user interaction of the tracking +// origin. So, we can proceed in the content process. Otherwise, go to +// step 3. +// 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. + +/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise> +StorageAccessAPIHelper::AllowAccessForOnParentProcess( + nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + const StorageAccessAPIHelper::PerformPermissionGrant& aPerformFinalChecks) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aParentContext); + + uint32_t behavior; + uint64_t topLevelWindowId; + nsCOMPtr<nsIPrincipal> trackingPrincipal; + nsAutoCString trackingOrigin; + + RefPtr<StorageAccessPermissionGrantPromise> returnPromise = + AllowAccessForHelper(aPrincipal, aParentContext, aReason, + &trackingPrincipal, trackingOrigin, + &topLevelWindowId, &behavior); + if (returnPromise) { + return returnPromise; + } + + return StorageAccessAPIHelper::CompleteAllowAccessForOnParentProcess( + aParentContext, topLevelWindowId, trackingPrincipal, trackingOrigin, + behavior, aReason, aPerformFinalChecks); +} + +/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise> +StorageAccessAPIHelper::AllowAccessForOnChildProcess( + nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + const StorageAccessAPIHelper::PerformPermissionGrant& aPerformFinalChecks) { + MOZ_ASSERT(XRE_IsContentProcess()); + MOZ_ASSERT(aParentContext); + + uint32_t behavior; + uint64_t topLevelWindowId; + nsCOMPtr<nsIPrincipal> trackingPrincipal; + nsAutoCString trackingOrigin; + + RefPtr<StorageAccessPermissionGrantPromise> returnPromise = + AllowAccessForHelper(aPrincipal, aParentContext, aReason, + &trackingPrincipal, trackingOrigin, + &topLevelWindowId, &behavior); + if (returnPromise) { + return returnPromise; + } + + // We should run in the parent process when the tracking origin is + // third-party with respect to it's parent window. This is because we can't + // test if the user has interacted with the third-party origin in the child + // process. + if (aParentContext->IsInProcess()) { + bool isThirdParty; + nsCOMPtr<nsIPrincipal> 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); + if (aReason == + ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI || + !isThirdParty) { + return StorageAccessAPIHelper::CompleteAllowAccessForOnChildProcess( + aParentContext, topLevelWindowId, trackingPrincipal, trackingOrigin, + behavior, aReason, aPerformFinalChecks); + } + } + + // Only support PerformPermissionGrant 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<BrowsingContext> bc = aParentContext; + return cc + ->SendCompleteAllowAccessFor(aParentContext, topLevelWindowId, + 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()); + StorageAccessAPIHelper::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. +// +// 4. ePrivilegeStorageAccessForOriginAPI +// aParentContext is the browsing context of the top window which calls the +// privilege API. So, it is always in-process. And we don't need to check the +// user interaction permission for the tracking origin in this case. We can +// run in the same process. +/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise> +StorageAccessAPIHelper::CompleteAllowAccessForOnParentProcess( + dom::BrowsingContext* aParentContext, uint64_t aTopLevelWindowId, + nsIPrincipal* aTrackingPrincipal, const nsACString& aTrackingOrigin, + uint32_t aCookieBehavior, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + const PerformPermissionGrant& aPerformFinalChecks) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aParentContext); + + nsCOMPtr<nsIPrincipal> trackingPrincipal; + nsAutoCString trackingOrigin; + if (!aTrackingPrincipal) { + // User interaction is the only case that tracking principal is not + // available. + MOZ_ASSERT(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. + // + // For ePrivilegeStorageAccessForOriginAPI, we explicitly don't check the user + // interaction for the tracking origin. + + bool isInPrefList = false; + trackingPrincipal->IsURIInPrefList( + "privacy.restrict3rdpartystorage." + "userInteractionRequiredForHosts", + &isInPrefList); + if (aReason != ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI && + 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, + 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<StorageAccessPermissionGrantPromise> { + MOZ_ASSERT(!aParentContext->IsInProcess()); + // 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) { + dom::ContentParent* cp = aParentContext->Canonical()->GetContentParent(); + Unused << cp->SendOnAllowAccessFor(aParentContext, trackingOrigin, + aCookieBehavior, aReason); + } + + Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason> + reportReason; + // We can directly report here if we can know the origin of the top. + ContentBlockingNotifier::ReportUnblockingToConsole( + aParentContext, NS_ConvertUTF8toUTF16(trackingOrigin), aReason); + // Set the report reason to nothing if we've already reported. + reportReason = Nothing(); + + LOG(("Saving the permission: trackingOrigin=%s", trackingOrigin.get())); + bool frameOnly = StaticPrefs::dom_storage_access_frame_only() && + aReason == ContentBlockingNotifier::eStorageAccessAPI; + return SaveAccessForOriginOnParentProcess(aTopLevelWindowId, aParentContext, + trackingPrincipal, aAllowMode, + frameOnly) + ->Then(GetCurrentSerialEventTarget(), __func__, + [aReason, trackingPrincipal]( + ParentAccessGrantPromise::ResolveOrRejectValue&& aValue) { + if (!aValue.IsResolve()) { + return StorageAccessPermissionGrantPromise::CreateAndReject( + false, __func__); + } + // We only wish to observe user interaction in the case of a + // "normal" requestStorageAccess grant. We do not observe user + // interaction where the priveledged API is used. Acquiring + // the storageAccessAPI permission for the first time will only + // occur through the clicking accept on the doorhanger. + if (aReason == ContentBlockingNotifier::eStorageAccessAPI) { + ContentBlockingUserInteraction::Observe(trackingPrincipal); + } + return StorageAccessPermissionGrantPromise::CreateAndResolve( + StorageAccessAPIHelper::eAllow, __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 */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise> +StorageAccessAPIHelper::CompleteAllowAccessForOnChildProcess( + dom::BrowsingContext* aParentContext, uint64_t aTopLevelWindowId, + nsIPrincipal* aTrackingPrincipal, const nsACString& aTrackingOrigin, + uint32_t aCookieBehavior, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + const PerformPermissionGrant& aPerformFinalChecks) { + MOZ_ASSERT_IF(XRE_IsContentProcess(), aParentContext->IsInProcess()); + MOZ_ASSERT(XRE_IsContentProcess()); + MOZ_ASSERT(aParentContext); + MOZ_ASSERT(aTrackingPrincipal); + + nsCOMPtr<nsIPrincipal> trackingPrincipal; + nsAutoCString trackingOrigin; + trackingOrigin = aTrackingOrigin; + trackingPrincipal = aTrackingPrincipal; + + 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. + // + // For ePrivilegeStorageAccessForOriginAPI, we explicitly don't check the user + // interaction for the tracking origin. + + bool isInPrefList = false; + aTrackingPrincipal->IsURIInPrefList( + "privacy.restrict3rdpartystorage." + "userInteractionRequiredForHosts", + &isInPrefList); + if (aReason != ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI && + isInPrefList && + !ContentBlockingUserInteraction::Exists(aTrackingPrincipal)) { + LOG_PRIN(("Tracking principal (%s) hasn't been interacted with before, " + "refusing to add a first-party storage permission to access it", + _spec), + aTrackingPrincipal); + ContentBlockingNotifier::OnDecision( + aParentContext, ContentBlockingNotifier::BlockingDecision::eBlock, + 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<StorageAccessPermissionGrantPromise> { + // Inform the window we granted permission for. This has to be done in the + // window's process. As a child this is always the case. + StorageAccessAPIHelper::OnAllowAccessFor(aParentContext, trackingOrigin, + aCookieBehavior, aReason); + + Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason> + reportReason; + // We can directly report here if we can know the origin of the top. + if (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); + } + + 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. + bool frameOnly = StaticPrefs::dom_storage_access_frame_only() && + aReason == ContentBlockingNotifier::eStorageAccessAPI; + return cc + ->SendStorageAccessPermissionGrantedForOrigin( + aTopLevelWindowId, aParentContext, trackingPrincipal, + trackingOrigin, aAllowMode, reportReason, frameOnly) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aReason, trackingPrincipal]( + const ContentChild:: + StorageAccessPermissionGrantedForOriginPromise:: + ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + if (aValue.ResolveValue() && + (aReason == ContentBlockingNotifier::eStorageAccessAPI)) { + ContentBlockingUserInteraction::Observe(trackingPrincipal); + } + 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 StorageAccessAPIHelper::OnAllowAccessFor( + dom::BrowsingContext* aParentContext, const nsACString& 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 storage permission is granted + // if it is not a frame-only permission grant which does not propogate. + if (aReason != ContentBlockingNotifier::StorageAccessPermissionGrantedReason:: + eStorageAccessAPI || + !StaticPrefs::dom_storage_access_frame_only()) { + StorageAccessAPIHelper::UpdateAllowAccessOnCurrentProcess(aParentContext, + aTrackingOrigin); + } + + // Let's inform the parent window. + nsCOMPtr<nsPIDOMWindowInner> 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, + nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER, aTrackingOrigin, + Some(aReason)); +} + +/* static */ +RefPtr<mozilla::StorageAccessAPIHelper::ParentAccessGrantPromise> +StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess( + uint64_t aTopLevelWindowId, BrowsingContext* aParentContext, + nsIPrincipal* aTrackingPrincipal, int aAllowMode, bool aFrameOnly, + uint64_t aExpirationTime) { + MOZ_ASSERT(aTopLevelWindowId != 0); + MOZ_ASSERT(aTrackingPrincipal); + + if (!aTrackingPrincipal || aTrackingPrincipal->IsSystemPrincipal() || + aTrackingPrincipal->GetIsNullPrincipal() || + aTrackingPrincipal->GetIsExpandedPrincipal()) { + LOG(("aTrackingPrincipal is of invalid principal type")); + return ParentAccessGrantPromise::CreateAndReject(false, __func__); + } + + nsAutoCString trackingOrigin; + nsresult rv = aTrackingPrincipal->GetOriginNoSuffix(trackingOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return ParentAccessGrantPromise::CreateAndReject(false, __func__); + } + + RefPtr<WindowGlobalParent> 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, only it is not a frame-only permission grant which + // does not propogate. + if (!aFrameOnly) { + StorageAccessAPIHelper::UpdateAllowAccessOnParentProcess(aParentContext, + trackingOrigin); + } + + return StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess( + wgp->DocumentPrincipal(), aTrackingPrincipal, aAllowMode, aFrameOnly, + aExpirationTime); +} + +/* static */ +RefPtr<mozilla::StorageAccessAPIHelper::ParentAccessGrantPromise> +StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess( + nsIPrincipal* aParentPrincipal, nsIPrincipal* aTrackingPrincipal, + int aAllowMode, bool aFrameOnly, 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__); + }; + + if (aTrackingPrincipal->IsSystemPrincipal() || + aTrackingPrincipal->GetIsNullPrincipal() || + aTrackingPrincipal->GetIsExpandedPrincipal()) { + LOG(("aTrackingPrincipal is of invalid principal type")); + return ParentAccessGrantPromise::CreateAndReject(false, __func__); + } + + nsAutoCString trackingOrigin; + nsresult rv = aTrackingPrincipal->GetOriginNoSuffix(trackingOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return ParentAccessGrantPromise::CreateAndReject(false, __func__); + } + + LOG_PRIN(("Saving a first-party storage permission on %s for " + "trackingOrigin=%s", + _spec, trackingOrigin.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; + 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; + if (aFrameOnly) { + bool success = AntiTrackingUtils::CreateStorageFramePermissionKey( + aTrackingPrincipal, type); + if (NS_WARN_IF(!success)) { + return ParentAccessGrantPromise::CreateAndReject(false, __func__); + } + } else { + AntiTrackingUtils::CreateStoragePermissionKey(trackingOrigin, 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<nsIObserverService> 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__); +} + +// static +Maybe<bool> +StorageAccessAPIHelper::CheckCookiesPermittedDecidesStorageAccessAPI( + nsICookieJarSettings* aCookieJarSettings, + nsIPrincipal* aRequestingPrincipal) { + MOZ_ASSERT(aCookieJarSettings); + MOZ_ASSERT(aRequestingPrincipal); + uint32_t cookiePermission = detail::CheckCookiePermissionForPrincipal( + aCookieJarSettings, aRequestingPrincipal); + if (cookiePermission == nsICookiePermission::ACCESS_ALLOW || + cookiePermission == nsICookiePermission::ACCESS_SESSION) { + return Some(true); + } + + if (cookiePermission == nsICookiePermission::ACCESS_DENY) { + return Some(false); + } + + if (ContentBlockingAllowList::Check(aCookieJarSettings)) { + return Some(true); + } + return Nothing(); +} + +/* static */ RefPtr<MozPromise<Maybe<bool>, nsresult, true>> +StorageAccessAPIHelper:: + AsyncCheckCookiesPermittedDecidesStorageAccessAPIOnChildProcess( + dom::BrowsingContext* aBrowsingContext, + nsIPrincipal* aRequestingPrincipal) { + MOZ_ASSERT(XRE_IsContentProcess()); + + ContentChild* cc = ContentChild::GetSingleton(); + MOZ_ASSERT(cc); + + return cc + ->SendTestCookiePermissionDecided(aBrowsingContext, aRequestingPrincipal) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [](const ContentChild::TestCookiePermissionDecidedPromise:: + ResolveOrRejectValue& aPromise) { + if (aPromise.IsResolve()) { + return MozPromise<Maybe<bool>, nsresult, true>::CreateAndResolve( + aPromise.ResolveValue(), __func__); + } + return MozPromise<Maybe<bool>, nsresult, true>::CreateAndReject( + NS_ERROR_UNEXPECTED, __func__); + }); +} + +// static +Maybe<bool> StorageAccessAPIHelper::CheckBrowserSettingsDecidesStorageAccessAPI( + nsICookieJarSettings* aCookieJarSettings, bool aThirdParty, + bool aIsOnThirdPartySkipList, bool aIsThirdPartyTracker) { + MOZ_ASSERT(aCookieJarSettings); + uint32_t behavior = aCookieJarSettings->GetCookieBehavior(); + switch (behavior) { + case nsICookieService::BEHAVIOR_ACCEPT: + return Some(true); + case nsICookieService::BEHAVIOR_REJECT_FOREIGN: + if (!aThirdParty) { + return Some(true); + } + return Some(false); + case nsICookieService::BEHAVIOR_REJECT: + return Some(false); + case nsICookieService::BEHAVIOR_LIMIT_FOREIGN: + if (!aThirdParty) { + return Some(true); + } + return Some(false); + case nsICookieService::BEHAVIOR_REJECT_TRACKER: + if (!aIsThirdPartyTracker) { + return Some(true); + } + if (aIsOnThirdPartySkipList) { + return Some(true); + } + return Nothing(); + case nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN: + if (aIsOnThirdPartySkipList) { + return Some(true); + } + return Nothing(); + default: + MOZ_ASSERT_UNREACHABLE("Must not have undefined cookie behavior"); + } + MOZ_ASSERT_UNREACHABLE("Must not have undefined cookie behavior"); + return Nothing(); +} + +// static +Maybe<bool> StorageAccessAPIHelper::CheckCallingContextDecidesStorageAccessAPI( + Document* aDocument, bool aRequestingStorageAccess) { + MOZ_ASSERT(aDocument); + + if (!aDocument->IsCurrentActiveDocument()) { + return Some(false); + } + + if (aRequestingStorageAccess) { + // Perform a Permission Policy Request + dom::FeaturePolicy* policy = aDocument->FeaturePolicy(); + MOZ_ASSERT(policy); + + if (!policy->AllowsFeature(u"storage-access"_ns, + dom::Optional<nsAString>())) { + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"), + aDocument, nsContentUtils::eDOM_PROPERTIES, + "RequestStorageAccessPermissionsPolicy"); + return Some(false); + } + } + + RefPtr<BrowsingContext> bc = aDocument->GetBrowsingContext(); + if (!bc) { + return Some(false); + } + + // Check if NodePrincipal is not null + if (!aDocument->NodePrincipal()) { + return Some(false); + } + + // If the document doesn't have a secure context, reject. The Static Pref is + // used to pass existing tests that do not fulfil this check. + if (StaticPrefs::dom_storage_access_dont_grant_insecure_contexts() && + !aDocument->NodePrincipal()->GetIsOriginPotentiallyTrustworthy()) { + // Report the error to the console if we are requesting access + if (aRequestingStorageAccess) { + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"), + aDocument, nsContentUtils::eDOM_PROPERTIES, + "RequestStorageAccessNotSecureContext"); + } + return Some(false); + } + + // If the document has a null origin, reject. + if (aDocument->NodePrincipal()->GetIsNullPrincipal()) { + // Report an error to the console for this case if we are requesting access + if (aRequestingStorageAccess) { + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"), + aDocument, nsContentUtils::eDOM_PROPERTIES, + "RequestStorageAccessNullPrincipal"); + } + return Some(false); + } + + if (!AntiTrackingUtils::IsThirdPartyDocument(aDocument)) { + return Some(true); + } + + if (aDocument->IsTopLevelContentDocument()) { + return Some(true); + } + + if (aRequestingStorageAccess) { + if (aDocument->StorageAccessSandboxed()) { + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"), + aDocument, nsContentUtils::eDOM_PROPERTIES, + "RequestStorageAccessSandboxed"); + return Some(false); + } + } + return Nothing(); +} + +// static +Maybe<bool> +StorageAccessAPIHelper::CheckSameSiteCallingContextDecidesStorageAccessAPI( + dom::Document* aDocument, bool aRequireUserActivation) { + MOZ_ASSERT(aDocument); + if (aRequireUserActivation) { + if (!aDocument->HasValidTransientUserGestureActivation()) { + // Report an error to the console for this case + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"), + aDocument, nsContentUtils::eDOM_PROPERTIES, + "RequestStorageAccessUserGesture"); + return Some(false); + } + } + + nsIChannel* chan = aDocument->GetChannel(); + if (!chan) { + return Some(false); + } + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + if (loadInfo->GetIsThirdPartyContextToTopWindow()) { + return Some(false); + } + + // If the document has a null origin, reject. + if (aDocument->NodePrincipal()->GetIsNullPrincipal()) { + // Report an error to the console for this case + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + nsLiteralCString("requestStorageAccess"), + aDocument, nsContentUtils::eDOM_PROPERTIES, + "RequestStorageAccessNullPrincipal"); + return Some(false); + } + return Maybe<bool>(); +} + +// static +Maybe<bool> +StorageAccessAPIHelper::CheckExistingPermissionDecidesStorageAccessAPI( + dom::Document* aDocument, bool aRequestingStorageAccess) { + MOZ_ASSERT(aDocument); + if (aDocument->StorageAccessSandboxed()) { + if (aRequestingStorageAccess) { + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"), + aDocument, nsContentUtils::eDOM_PROPERTIES, + "RequestStorageAccessSandboxed"); + } + return Some(false); + } + if (aDocument->UsingStorageAccess()) { + return Some(true); + } + return Nothing(); +} + +// static +RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise> +StorageAccessAPIHelper::RequestStorageAccessAsyncHelper( + dom::Document* aDocument, nsPIDOMWindowInner* aInnerWindow, + dom::BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal, + bool aHasUserInteraction, bool aRequireUserInteraction, bool aFrameOnly, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aNotifier, + bool aRequireGrant) { + MOZ_ASSERT(aDocument); + MOZ_ASSERT(XRE_IsContentProcess()); + + if (!aRequireGrant) { + // Try to allow access for the given principal. + return StorageAccessAPIHelper::AllowAccessForOnChildProcess( + aPrincipal, aBrowsingContext, aNotifier); + } + + RefPtr<nsIPrincipal> principal(aPrincipal); + + // This is a lambda function that has some variables bound to it. It will be + // called later in CompleteAllowAccessFor inside of AllowAccessFor. + auto performPermissionGrant = aDocument->CreatePermissionGrantPromise( + aInnerWindow, principal, aHasUserInteraction, aRequireUserInteraction, + Nothing(), aFrameOnly); + + // Try to allow access for the given principal. + return StorageAccessAPIHelper::AllowAccessForOnChildProcess( + principal, aBrowsingContext, aNotifier, performPermissionGrant); +} + +// There are two methods to handle permission 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 StorageAccessAPIHelper::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<nsPIDOMWindowInner> inner = + AntiTrackingUtils::GetInnerWindow(aContext); + if (inner) { + inner->SaveStorageAccessPermissionGranted(); + } + } + } + }); +} + +/* static */ +void StorageAccessAPIHelper::UpdateAllowAccessOnParentProcess( + BrowsingContext* aParentContext, const nsACString& aTrackingOrigin) { + MOZ_ASSERT(XRE_IsParentProcess()); + + nsAutoCString topKey; + nsCOMPtr<nsIPrincipal> topPrincipal = + AntiTrackingUtils::GetPrincipal(aParentContext->Top()); + PermissionManager::GetKeyForPrincipal(topPrincipal, false, true, 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 propagate 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<dom::WindowContext> ctx = + aParentContext->GetCurrentWindowContext(); + if (ctx && ctx->GetIsThirdPartyWindow()) { + continue; + } + } else { + nsCOMPtr<nsIPrincipal> principal = + AntiTrackingUtils::GetPrincipal(topContext); + if (!principal) { + continue; + } + + nsAutoCString key; + PermissionManager::GetKeyForPrincipal(principal, false, true, 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(); + } + }); + } +} diff --git a/toolkit/components/antitracking/StorageAccessAPIHelper.h b/toolkit/components/antitracking/StorageAccessAPIHelper.h new file mode 100644 index 0000000000..dd6f326604 --- /dev/null +++ b/toolkit/components/antitracking/StorageAccessAPIHelper.h @@ -0,0 +1,222 @@ +/* -*- 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/. */ + +#ifndef mozilla_antitrackingservice_h +#define mozilla_antitrackingservice_h + +#include "nsString.h" +#include "mozilla/ContentBlockingNotifier.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StaticPrefs_privacy.h" + +#include "nsIUrlClassifierFeature.h" + +class nsIChannel; +class nsICookieJarSettings; +class nsIPermission; +class nsIPrincipal; +class nsIURI; +class nsPIDOMWindowInner; +class nsPIDOMWindowOuter; + +namespace mozilla { + +class OriginAttributes; + +namespace dom { +class BrowsingContext; +class ContentParent; +class Document; +} // namespace dom + +class StorageAccessAPIHelper final { + public: + enum StorageAccessPromptChoices { eAllow, eAllowAutoGrant }; + + // Grant the permission for aOrigin to have access to the first party storage. + // This methods can handle 2 different scenarios: + // - aParentContext is a 3rd party context, it opens an aOrigin window and the + // user interacts with it. We want to grant the permission at the + // combination: top-level + aParentWindow + aOrigin. + // Ex: example.net loads an iframe tracker.com, which opens a popup + // tracker.org and the user interacts with it. tracker.org is allowed if + // loaded by tracker.com when loaded by example.net. + // - aParentContext is a first party context and a 3rd party resource + // (probably becuase of a script) opens a popup and the user interacts with + // it. We want to grant the permission for the 3rd party context to have + // access to the first party stoage when loaded in aParentWindow. Ex: + // example.net import tracker.com/script.js which does opens a popup and the + // user interacts with it. tracker.com is allowed when loaded by + // example.net. + typedef MozPromise<int, bool, true> StorageAccessPermissionGrantPromise; + typedef std::function<RefPtr<StorageAccessPermissionGrantPromise>()> + PerformPermissionGrant; + [[nodiscard]] static RefPtr<StorageAccessPermissionGrantPromise> + AllowAccessForOnParentProcess( + nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + const PerformPermissionGrant& aPerformFinalChecks = nullptr); + + [[nodiscard]] static RefPtr<StorageAccessPermissionGrantPromise> + AllowAccessForOnChildProcess( + nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + const PerformPermissionGrant& aPerformFinalChecks = nullptr); + + // This function handles tasks that have to be done in the process + // of the window that we just grant permission for. + static void OnAllowAccessFor( + dom::BrowsingContext* aParentContext, const nsACString& aTrackingOrigin, + uint32_t aCookieBehavior, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason); + + // For IPC only. + typedef MozPromise<nsresult, bool, true> ParentAccessGrantPromise; + static RefPtr<ParentAccessGrantPromise> SaveAccessForOriginOnParentProcess( + nsIPrincipal* aParentPrincipal, nsIPrincipal* aTrackingPrincipal, + int aAllowMode, bool aFrameOnly, + uint64_t aExpirationTime = + StaticPrefs::privacy_restrict3rdpartystorage_expiration()); + + static RefPtr<ParentAccessGrantPromise> SaveAccessForOriginOnParentProcess( + uint64_t aTopLevelWindowId, dom::BrowsingContext* aParentContext, + nsIPrincipal* aTrackingPrincipal, int aAllowMode, bool aFrameOnly, + uint64_t aExpirationTime = + StaticPrefs::privacy_restrict3rdpartystorage_expiration()); + + // This function checks if the document has explicit permission either to + // allow or deny access to cookies. This may be because of the "cookie" + // permission or because the domain is on the ContentBlockingAllowList + // e.g. because the user flipped the sheild. + // This returns: + // Some(true) if unpartitioned cookies will be permitted + // Some(false) if unpartitioned cookies will be blocked + // None if it is not clear from permission alone what to do + static Maybe<bool> CheckCookiesPermittedDecidesStorageAccessAPI( + nsICookieJarSettings* aCookieJarSettings, + nsIPrincipal* aRequestingPrincipal); + + // Calls CheckCookiesPermittedDecidesStorageAccessAPI in the Content Parent + // using aBrowsingContext's Top's Window Global's CookieJarSettings. + static RefPtr<MozPromise<Maybe<bool>, nsresult, true>> + AsyncCheckCookiesPermittedDecidesStorageAccessAPIOnChildProcess( + dom::BrowsingContext* aBrowsingContext, + nsIPrincipal* aRequestingPrincipal); + + // This function checks if the browser settings give explicit permission + // either to allow or deny access to cookies. This only checks the + // cookieBehavior setting. This requires an additional bool to indicate + // whether or not the context considered is third-party. This returns: + // Some(true) if unpartitioned cookies will be permitted + // Some(false) if unpartitioned cookies will be blocked + // None if it is not clear from settings alone what to do + static Maybe<bool> CheckBrowserSettingsDecidesStorageAccessAPI( + nsICookieJarSettings* aCookieJarSettings, bool aThirdParty, + bool aIsOnThirdPartySkipList, bool aIsThirdPartyTracker); + + // This function checks if the document's context (like if it is third-party + // or an iframe) gives an answer of how a the StorageAccessAPI call, that is + // meant to be called by an embedded third party, should return. + // This requires an argument that allows some checks to be run only if the + // caller of this function is performing a request for storage access. + // This returns: + // Some(true) if the calling context has access to cookies if it is not + // disallowed by the browser settings and cookie permissions + // Some(false) if the calling context should not have access to cookies if + // it is not expressly allowed by the browser settings and + // cookie permissions + // None if the calling context does not determine the document's access to + // unpartitioned cookies + static Maybe<bool> CheckCallingContextDecidesStorageAccessAPI( + dom::Document* aDocument, bool aRequestingStorageAccess); + + // This function checks if the document's context (like if it is third-party + // or an iframe) gives an answer of how a the StorageAccessAPI call that is + // meant to be called in a top-level context, should return. + // This returns: + // Some(true) if the calling context indicates calls to the top-level + // API must resolve if it is not + // disallowed by the browser settings and cookie permissions + // Some(false) if the calling context must reject when calling top level + // portions of the API if it is not expressly allowed by the + // browser settings and cookie permissions + // None if the calling context does not determine the outcome of the + // document's use of the top-level portions of the Storage Access API. + static Maybe<bool> CheckSameSiteCallingContextDecidesStorageAccessAPI( + dom::Document* aDocument, bool aRequireUserActivation); + + // This function checks if the document has already been granted or denied + // access to its unpartitioned cookies by the StorageAccessAPI + // This returns: + // Some(true) if the document has been granted access by the Storage Access + // API before + // Some(false) if the document has been denied access by the Storage Access + // API before + // None if the document has not been granted or denied access by the Storage + // Access API before + static Maybe<bool> CheckExistingPermissionDecidesStorageAccessAPI( + dom::Document* aDocument, bool aRequestingStorageAccess); + + // This function performs the asynchronous portion of checking if requests + // for storage access will be successful or not. This includes calling + // Document member functions that creating a permission prompt request and + // trying to perform an "autogrant" if aRequireGrant is true. + // This will return a promise whose values correspond to those of a + // ContentBlocking::AllowAccessFor call that ends the function. + static RefPtr<StorageAccessPermissionGrantPromise> + RequestStorageAccessAsyncHelper( + dom::Document* aDocument, nsPIDOMWindowInner* aInnerWindow, + dom::BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal, + bool aHasUserInteraction, bool aRequireUserInteraction, bool aFrameOnly, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aNotifier, + bool aRequireGrant); + + private: + friend class dom::ContentParent; + + // This function performs browser setting, cookie behavior and requesting + // context checks that might grant/reject storage access immediately using + // information provided by the inputs aPrincipal and aParentContext. To reduce + // redundancy the following out parameters with information also required in + // AllowAccessFor() are set in the function: aTrackingPrinciple, + // aTrackingOrigin, aTopLevelWindowId, aBehavior. If storage access can be + // granted/rejected due to settings/behavior returns a promise, else returns + // nullptr. + [[nodiscard]] static RefPtr< + StorageAccessAPIHelper::StorageAccessPermissionGrantPromise> + AllowAccessForHelper( + nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + nsCOMPtr<nsIPrincipal>* aTrackingPrincipal, nsACString& aTrackingOrigin, + uint64_t* aTopLevelWindowId, uint32_t* aBehavior); + + [[nodiscard]] static RefPtr<StorageAccessPermissionGrantPromise> + CompleteAllowAccessForOnParentProcess( + dom::BrowsingContext* aParentContext, uint64_t aTopLevelWindowId, + nsIPrincipal* aTrackingPrincipal, const nsACString& aTrackingOrigin, + uint32_t aCookieBehavior, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + const PerformPermissionGrant& aPerformFinalChecks = nullptr); + + [[nodiscard]] static RefPtr<StorageAccessPermissionGrantPromise> + CompleteAllowAccessForOnChildProcess( + dom::BrowsingContext* aParentContext, uint64_t aTopLevelWindowId, + nsIPrincipal* aTrackingPrincipal, const nsACString& aTrackingOrigin, + uint32_t aCookieBehavior, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + const PerformPermissionGrant& aPerformFinalChecks = nullptr); + + static void UpdateAllowAccessOnCurrentProcess( + dom::BrowsingContext* aParentContext, const nsACString& aTrackingOrigin); + + static void UpdateAllowAccessOnParentProcess( + dom::BrowsingContext* aParentContext, const nsACString& aTrackingOrigin); +}; + +} // namespace mozilla + +#endif // mozilla_antitrackingservice_h diff --git a/toolkit/components/antitracking/StoragePrincipalHelper.cpp b/toolkit/components/antitracking/StoragePrincipalHelper.cpp new file mode 100644 index 0000000000..10be1112ca --- /dev/null +++ b/toolkit/components/antitracking/StoragePrincipalHelper.cpp @@ -0,0 +1,677 @@ +/* -*- 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 "StoragePrincipalHelper.h" + +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StorageAccess.h" +#include "nsContentUtils.h" +#include "nsICookieJarSettings.h" +#include "nsICookieService.h" +#include "nsIDocShell.h" +#include "nsIEffectiveTLDService.h" +#include "nsIPrivateBrowsingChannel.h" +#include "AntiTrackingUtils.h" + +namespace mozilla { + +namespace { + +bool ShouldPartitionChannel(nsIChannel* aChannel, + nsICookieJarSettings* aCookieJarSettings) { + MOZ_ASSERT(aChannel); + + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return false; + } + + uint32_t rejectedReason = 0; + if (ShouldAllowAccessFor(aChannel, uri, &rejectedReason)) { + return false; + } + + // Let's use the storage principal only if we need to partition the cookie + // jar. We use the lower-level ContentBlocking API here to ensure this + // check doesn't send notifications. + if (!ShouldPartitionStorage(rejectedReason) || + !StoragePartitioningEnabled(rejectedReason, aCookieJarSettings)) { + return false; + } + + return true; +} + +bool ChooseOriginAttributes(nsIChannel* aChannel, OriginAttributes& aAttrs, + bool aForcePartitionedPrincipal) { + MOZ_ASSERT(aChannel); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsICookieJarSettings> cjs; + Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cjs)); + + if (!aForcePartitionedPrincipal && !ShouldPartitionChannel(aChannel, cjs)) { + return false; + } + + nsAutoString partitionKey; + Unused << cjs->GetPartitionKey(partitionKey); + + if (!partitionKey.IsEmpty()) { + aAttrs.SetPartitionKey(partitionKey); + return true; + } + + // Fallback to get first-party domain from top-level principal when we can't + // get it from CookieJarSetting. This might happen when a channel is not + // opened via http, for example, about page. + nsCOMPtr<nsIPrincipal> toplevelPrincipal = loadInfo->GetTopLevelPrincipal(); + if (!toplevelPrincipal) { + return false; + } + // Cast to BasePrincipal to continue to get acess to GetUri() + auto* basePrin = BasePrincipal::Cast(toplevelPrincipal); + nsCOMPtr<nsIURI> principalURI; + + nsresult rv = basePrin->GetURI(getter_AddRefs(principalURI)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + aAttrs.SetPartitionKey(principalURI); + return true; +} + +bool VerifyValidPartitionedPrincipalInfoForPrincipalInfoInternal( + const ipc::PrincipalInfo& aPartitionedPrincipalInfo, + const ipc::PrincipalInfo& aPrincipalInfo, + bool aIgnoreSpecForContentPrincipal, + bool aIgnoreDomainForContentPrincipal) { + if (aPartitionedPrincipalInfo.type() != aPrincipalInfo.type()) { + return false; + } + + if (aPartitionedPrincipalInfo.type() == + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) { + const mozilla::ipc::ContentPrincipalInfo& spInfo = + aPartitionedPrincipalInfo.get_ContentPrincipalInfo(); + const mozilla::ipc::ContentPrincipalInfo& pInfo = + aPrincipalInfo.get_ContentPrincipalInfo(); + + return spInfo.attrs().EqualsIgnoringPartitionKey(pInfo.attrs()) && + spInfo.originNoSuffix() == pInfo.originNoSuffix() && + (aIgnoreSpecForContentPrincipal || spInfo.spec() == pInfo.spec()) && + (aIgnoreDomainForContentPrincipal || + spInfo.domain() == pInfo.domain()) && + spInfo.baseDomain() == pInfo.baseDomain(); + } + + if (aPartitionedPrincipalInfo.type() == + mozilla::ipc::PrincipalInfo::TSystemPrincipalInfo) { + // Nothing to check here. + return true; + } + + if (aPartitionedPrincipalInfo.type() == + mozilla::ipc::PrincipalInfo::TNullPrincipalInfo) { + const mozilla::ipc::NullPrincipalInfo& spInfo = + aPartitionedPrincipalInfo.get_NullPrincipalInfo(); + const mozilla::ipc::NullPrincipalInfo& pInfo = + aPrincipalInfo.get_NullPrincipalInfo(); + + return spInfo.spec() == pInfo.spec() && + spInfo.attrs().EqualsIgnoringPartitionKey(pInfo.attrs()); + } + + if (aPartitionedPrincipalInfo.type() == + mozilla::ipc::PrincipalInfo::TExpandedPrincipalInfo) { + const mozilla::ipc::ExpandedPrincipalInfo& spInfo = + aPartitionedPrincipalInfo.get_ExpandedPrincipalInfo(); + const mozilla::ipc::ExpandedPrincipalInfo& pInfo = + aPrincipalInfo.get_ExpandedPrincipalInfo(); + + if (!spInfo.attrs().EqualsIgnoringPartitionKey(pInfo.attrs())) { + return false; + } + + if (spInfo.allowlist().Length() != pInfo.allowlist().Length()) { + return false; + } + + for (uint32_t i = 0; i < spInfo.allowlist().Length(); ++i) { + if (!VerifyValidPartitionedPrincipalInfoForPrincipalInfoInternal( + spInfo.allowlist()[i], pInfo.allowlist()[i], + aIgnoreSpecForContentPrincipal, + aIgnoreDomainForContentPrincipal)) { + return false; + } + } + + return true; + } + + MOZ_CRASH("Invalid principalInfo type"); + return false; +} + +} // namespace + +// static +nsresult StoragePrincipalHelper::Create(nsIChannel* aChannel, + nsIPrincipal* aPrincipal, + bool aForceIsolation, + nsIPrincipal** aStoragePrincipal) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aStoragePrincipal); + + auto scopeExit = MakeScopeExit([&] { + nsCOMPtr<nsIPrincipal> storagePrincipal = aPrincipal; + storagePrincipal.forget(aStoragePrincipal); + }); + + OriginAttributes attrs = aPrincipal->OriginAttributesRef(); + if (!ChooseOriginAttributes(aChannel, attrs, aForceIsolation)) { + return NS_OK; + } + + scopeExit.release(); + + nsCOMPtr<nsIPrincipal> storagePrincipal = + BasePrincipal::Cast(aPrincipal)->CloneForcingOriginAttributes(attrs); + + // If aPrincipal is not a ContentPrincipal, e.g. a NullPrincipal, the clone + // call will return a nullptr. + NS_ENSURE_TRUE(storagePrincipal, NS_ERROR_FAILURE); + + storagePrincipal.forget(aStoragePrincipal); + return NS_OK; +} + +// static +nsresult StoragePrincipalHelper::CreatePartitionedPrincipalForServiceWorker( + nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings, + nsIPrincipal** aPartitionedPrincipal) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aPartitionedPrincipal); + + OriginAttributes attrs = aPrincipal->OriginAttributesRef(); + + nsAutoString partitionKey; + Unused << aCookieJarSettings->GetPartitionKey(partitionKey); + + if (!partitionKey.IsEmpty()) { + attrs.SetPartitionKey(partitionKey); + } + + nsCOMPtr<nsIPrincipal> partitionedPrincipal = + BasePrincipal::Cast(aPrincipal)->CloneForcingOriginAttributes(attrs); + + // If aPrincipal is not a ContentPrincipal, e.g. a NullPrincipal, the clone + // call will return a nullptr. + NS_ENSURE_TRUE(partitionedPrincipal, NS_ERROR_FAILURE); + + partitionedPrincipal.forget(aPartitionedPrincipal); + return NS_OK; +} + +// static +nsresult +StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( + nsIChannel* aChannel, OriginAttributes& aOriginAttributes) { + MOZ_ASSERT(aChannel); + + ChooseOriginAttributes(aChannel, aOriginAttributes, false); + return NS_OK; +} + +// static +bool StoragePrincipalHelper::VerifyValidStoragePrincipalInfoForPrincipalInfo( + const mozilla::ipc::PrincipalInfo& aStoragePrincipalInfo, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo) { + return VerifyValidPartitionedPrincipalInfoForPrincipalInfoInternal( + aStoragePrincipalInfo, aPrincipalInfo, false, false); +} + +// static +bool StoragePrincipalHelper::VerifyValidClientPrincipalInfoForPrincipalInfo( + const mozilla::ipc::PrincipalInfo& aClientPrincipalInfo, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo) { + return VerifyValidPartitionedPrincipalInfoForPrincipalInfoInternal( + aClientPrincipalInfo, aPrincipalInfo, true, true); +} + +// static +nsresult StoragePrincipalHelper::GetPrincipal(nsIChannel* aChannel, + PrincipalType aPrincipalType, + nsIPrincipal** aPrincipal) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(aPrincipal); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsICookieJarSettings> cjs; + Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cjs)); + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + MOZ_DIAGNOSTIC_ASSERT(ssm); + + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsIPrincipal> partitionedPrincipal; + + nsresult rv = + ssm->GetChannelResultPrincipals(aChannel, getter_AddRefs(principal), + getter_AddRefs(partitionedPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // The aChannel might not be opened in some cases, e.g. getting principal + // for the new channel during a redirect. So, the value + // `IsThirdPartyToTopWindow` is incorrect in this case because this value is + // calculated during opening a channel. And we need to know the value in order + // to get the correct principal. To fix this, we compute the value here even + // the channel hasn't been opened yet. + // + // Note that we don't need to compute the value if there is no browsing + // context ID assigned. This could happen in a GTest or XPCShell. + // + // ToDo: The AntiTrackingUtils::ComputeIsThirdPartyToTopWindow() is only + // available in the parent process. So, this can only work in the parent + // process. It's fine for now, but we should change this to also work in + // content processes. Bug 1736452 will address this. + // + if (XRE_IsParentProcess() && loadInfo->GetBrowsingContextID() != 0) { + AntiTrackingUtils::ComputeIsThirdPartyToTopWindow(aChannel); + } + + nsCOMPtr<nsIPrincipal> outPrincipal = principal; + + switch (aPrincipalType) { + case eRegularPrincipal: + break; + + case eStorageAccessPrincipal: + if (ShouldPartitionChannel(aChannel, cjs)) { + outPrincipal = partitionedPrincipal; + } + break; + + case ePartitionedPrincipal: + outPrincipal = partitionedPrincipal; + break; + + case eForeignPartitionedPrincipal: + // We only support foreign partitioned principal when dFPI is enabled. + if (cjs->GetCookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && + loadInfo->GetIsThirdPartyContextToTopWindow()) { + outPrincipal = partitionedPrincipal; + } + break; + } + + outPrincipal.forget(aPrincipal); + return NS_OK; +} + +// static +nsresult StoragePrincipalHelper::GetPrincipal(nsPIDOMWindowInner* aWindow, + PrincipalType aPrincipalType, + nsIPrincipal** aPrincipal) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aPrincipal); + + nsCOMPtr<dom::Document> doc = aWindow->GetExtantDoc(); + NS_ENSURE_STATE(doc); + + nsCOMPtr<nsIPrincipal> outPrincipal; + + switch (aPrincipalType) { + case eRegularPrincipal: + outPrincipal = doc->NodePrincipal(); + break; + + case eStorageAccessPrincipal: + outPrincipal = doc->EffectiveStoragePrincipal(); + break; + + case ePartitionedPrincipal: + outPrincipal = doc->PartitionedPrincipal(); + break; + + case eForeignPartitionedPrincipal: + // We only support foreign partitioned principal when dFPI is enabled. + if (doc->CookieJarSettings()->GetCookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && + AntiTrackingUtils::IsThirdPartyWindow(aWindow, nullptr)) { + outPrincipal = doc->PartitionedPrincipal(); + } else { + outPrincipal = doc->NodePrincipal(); + } + break; + } + + outPrincipal.forget(aPrincipal); + return NS_OK; +} + +// static +bool StoragePrincipalHelper::ShouldUsePartitionPrincipalForServiceWorker( + nsIDocShell* aDocShell) { + MOZ_ASSERT(aDocShell); + + // We don't use the partitioned principal for service workers if it's + // disabled. + if (!StaticPrefs::privacy_partition_serviceWorkers()) { + return false; + } + + RefPtr<dom::Document> document = aDocShell->GetExtantDocument(); + + // If we cannot get the document from the docShell, we turn to get its + // parent's document. + if (!document) { + nsCOMPtr<nsIDocShellTreeItem> parentItem; + aDocShell->GetInProcessSameTypeParent(getter_AddRefs(parentItem)); + + if (parentItem) { + document = parentItem->GetDocument(); + } + } + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + + if (document) { + cookieJarSettings = document->CookieJarSettings(); + } else { + // If there was no document, we create one cookieJarSettings here in order + // to get the cookieBehavior. We don't need a real value for RFP because + // we are only using this object to check default cookie behavior. + cookieJarSettings = + net::CookieJarSettings::Create(net::CookieJarSettings::eRegular, + /* shouldResistFingerpreinting */ false); + } + + // We only support partitioned service workers when dFPI is enabled. + if (cookieJarSettings->GetCookieBehavior() != + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { + return false; + } + + // Only the third-party context will need to use the partitioned principal. A + // first-party context is still using the regular principal for the service + // worker. + return AntiTrackingUtils::IsThirdPartyContext( + document ? document->GetBrowsingContext() + : aDocShell->GetBrowsingContext()); +} + +// static +bool StoragePrincipalHelper::ShouldUsePartitionPrincipalForServiceWorker( + dom::WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + + // We don't use the partitioned principal for service workers if it's + // disabled. + if (!StaticPrefs::privacy_partition_serviceWorkers()) { + return false; + } + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = + aWorkerPrivate->CookieJarSettings(); + + // We only support partitioned service workers when dFPI is enabled. + if (cookieJarSettings->GetCookieBehavior() != + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { + return false; + } + + return aWorkerPrivate->IsThirdPartyContextToTopWindow(); +} + +// static +bool StoragePrincipalHelper::GetOriginAttributes( + nsIChannel* aChannel, mozilla::OriginAttributes& aAttributes, + StoragePrincipalHelper::PrincipalType aPrincipalType) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + loadInfo->GetOriginAttributes(&aAttributes); + + bool isPrivate = false; + nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel); + if (pbChannel) { + nsresult rv = pbChannel->GetIsChannelPrivate(&isPrivate); + NS_ENSURE_SUCCESS(rv, false); + } else { + // Some channels may not implement nsIPrivateBrowsingChannel + nsCOMPtr<nsILoadContext> loadContext; + NS_QueryNotificationCallbacks(aChannel, loadContext); + isPrivate = loadContext && loadContext->UsePrivateBrowsing(); + } + aAttributes.SyncAttributesWithPrivateBrowsing(isPrivate); + + nsCOMPtr<nsICookieJarSettings> cjs; + + switch (aPrincipalType) { + case eRegularPrincipal: + break; + + case eStorageAccessPrincipal: + PrepareEffectiveStoragePrincipalOriginAttributes(aChannel, aAttributes); + break; + + case ePartitionedPrincipal: + ChooseOriginAttributes(aChannel, aAttributes, true); + break; + + case eForeignPartitionedPrincipal: + Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cjs)); + + // We only support foreign partitioned principal when dFPI is enabled. + // Otherwise, we will use the regular principal. + if (cjs->GetCookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && + loadInfo->GetIsThirdPartyContextToTopWindow()) { + ChooseOriginAttributes(aChannel, aAttributes, true); + } + break; + } + + return true; +} + +// static +bool StoragePrincipalHelper::GetRegularPrincipalOriginAttributes( + dom::Document* aDocument, OriginAttributes& aAttributes) { + aAttributes = mozilla::OriginAttributes(); + if (!aDocument) { + return false; + } + + nsCOMPtr<nsILoadGroup> loadGroup = aDocument->GetDocumentLoadGroup(); + if (loadGroup) { + return GetRegularPrincipalOriginAttributes(loadGroup, aAttributes); + } + + nsCOMPtr<nsIChannel> channel = aDocument->GetChannel(); + if (!channel) { + return false; + } + + return GetOriginAttributes(channel, aAttributes, eRegularPrincipal); +} + +// static +bool StoragePrincipalHelper::GetRegularPrincipalOriginAttributes( + nsILoadGroup* aLoadGroup, OriginAttributes& aAttributes) { + aAttributes = mozilla::OriginAttributes(); + if (!aLoadGroup) { + return false; + } + + nsCOMPtr<nsIInterfaceRequestor> callbacks; + aLoadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks)); + if (!callbacks) { + return false; + } + + nsCOMPtr<nsILoadContext> loadContext = do_GetInterface(callbacks); + if (!loadContext) { + return false; + } + + loadContext->GetOriginAttributes(aAttributes); + return true; +} + +// static +bool StoragePrincipalHelper::GetOriginAttributesForNetworkState( + nsIChannel* aChannel, OriginAttributes& aAttributes) { + return StoragePrincipalHelper::GetOriginAttributes( + aChannel, aAttributes, + StaticPrefs::privacy_partition_network_state() ? ePartitionedPrincipal + : eRegularPrincipal); +} + +// static +void StoragePrincipalHelper::GetOriginAttributesForNetworkState( + dom::Document* aDocument, OriginAttributes& aAttributes) { + aAttributes = aDocument->NodePrincipal()->OriginAttributesRef(); + + if (!StaticPrefs::privacy_partition_network_state()) { + return; + } + + aAttributes = aDocument->PartitionedPrincipal()->OriginAttributesRef(); +} + +// static +void StoragePrincipalHelper::UpdateOriginAttributesForNetworkState( + nsIURI* aFirstPartyURI, OriginAttributes& aAttributes) { + if (!StaticPrefs::privacy_partition_network_state()) { + return; + } + + aAttributes.SetPartitionKey(aFirstPartyURI); +} + +enum SupportedScheme { HTTP, HTTPS }; + +static bool GetOriginAttributesWithScheme(nsIChannel* aChannel, + OriginAttributes& aAttributes, + SupportedScheme aScheme) { + const nsString targetScheme = aScheme == HTTP ? u"http"_ns : u"https"_ns; + if (!StoragePrincipalHelper::GetOriginAttributesForNetworkState( + aChannel, aAttributes)) { + return false; + } + + if (aAttributes.mPartitionKey.IsEmpty() || + aAttributes.mPartitionKey[0] != '(') { + return true; + } + + nsAString::const_iterator start, end; + aAttributes.mPartitionKey.BeginReading(start); + aAttributes.mPartitionKey.EndReading(end); + + MOZ_DIAGNOSTIC_ASSERT(*start == '('); + start++; + + nsAString::const_iterator iter(start); + bool ok = FindCharInReadable(',', iter, end); + MOZ_DIAGNOSTIC_ASSERT(ok); + + if (!ok) { + return false; + } + + nsAutoString scheme; + scheme.Assign(Substring(start, iter)); + + if (scheme.Equals(targetScheme)) { + return true; + } + + nsAutoString key; + key += u"("_ns; + key += targetScheme; + key.Append(Substring(iter, end)); + aAttributes.SetPartitionKey(key); + + return true; +} + +// static +bool StoragePrincipalHelper::GetOriginAttributesForHSTS( + nsIChannel* aChannel, OriginAttributes& aAttributes) { + return GetOriginAttributesWithScheme(aChannel, aAttributes, HTTP); +} + +// static +bool StoragePrincipalHelper::GetOriginAttributesForHTTPSRR( + nsIChannel* aChannel, OriginAttributes& aAttributes) { + return GetOriginAttributesWithScheme(aChannel, aAttributes, HTTPS); +} + +// static +bool StoragePrincipalHelper::GetOriginAttributes( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + OriginAttributes& aAttributes) { + aAttributes = mozilla::OriginAttributes(); + + using Type = ipc::PrincipalInfo; + switch (aPrincipalInfo.type()) { + case Type::TContentPrincipalInfo: + aAttributes = aPrincipalInfo.get_ContentPrincipalInfo().attrs(); + break; + case Type::TNullPrincipalInfo: + aAttributes = aPrincipalInfo.get_NullPrincipalInfo().attrs(); + break; + case Type::TExpandedPrincipalInfo: + aAttributes = aPrincipalInfo.get_ExpandedPrincipalInfo().attrs(); + break; + case Type::TSystemPrincipalInfo: + break; + default: + return false; + } + + return true; +} + +bool StoragePrincipalHelper::PartitionKeyHasBaseDomain( + const nsAString& aPartitionKey, const nsACString& aBaseDomain) { + return PartitionKeyHasBaseDomain(aPartitionKey, + NS_ConvertUTF8toUTF16(aBaseDomain)); +} + +// static +bool StoragePrincipalHelper::PartitionKeyHasBaseDomain( + const nsAString& aPartitionKey, const nsAString& aBaseDomain) { + if (aPartitionKey.IsEmpty() || aBaseDomain.IsEmpty()) { + return false; + } + + nsString scheme; + nsString pkBaseDomain; + int32_t port; + bool success = OriginAttributes::ParsePartitionKey(aPartitionKey, scheme, + pkBaseDomain, port); + + if (!success) { + return false; + } + + return aBaseDomain.Equals(pkBaseDomain); +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/StoragePrincipalHelper.h b/toolkit/components/antitracking/StoragePrincipalHelper.h new file mode 100644 index 0000000000..f813417eb6 --- /dev/null +++ b/toolkit/components/antitracking/StoragePrincipalHelper.h @@ -0,0 +1,358 @@ +/* -*- 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/. */ + +#ifndef mozilla_StoragePrincipalHelper_h +#define mozilla_StoragePrincipalHelper_h + +#include <cstdint> +#include "ErrorList.h" +#include "nsStringFwd.h" + +/** + * StoragePrincipal + * ~~~~~~~~~~~~~~~~ + + * StoragePrincipal is the nsIPrincipal to be used to open the cookie jar of a + * resource's origin. Normally, the StoragePrincipal corresponds to the + * resource's origin, but, in some scenarios, it can be different: it has the + * `partitionKey` attribute set to the top-level “site” (i.e., scheme plus + * eTLD+1 of the origin of the top-level document). + * + * Each storage component should always use the StoragePrincipal instead of the + * 'real' one in order to implement the partitioning correctly. See the list of + * the components here: https://privacycg.github.io/storage-partitioning/ + * + * On the web, each resource has its own origin (see + * https://html.spec.whatwg.org/multipage/origin.html#concept-origin) and each + * origin has its own cookie jar, containing cookies, storage data, cache and so + * on. + * + * In gecko-world, the origin and its attributes are stored and managed by the + * nsIPrincipal interface. Both a resource's Principal and a resource's + * StoragePrincipal are nsIPrincipal interfaces and, normally, they are the same + * object. + * + * Naming and usage + * ~~~~~~~~~~~~~~~~ + * + * StoragePrincipal exposes four types of principals for a resource: + * - Regular Principal: + * A “first-party” principal derived from the origin of the resource. This + * does not have the `partitionKey` origin attribute set. + * - Partitioned Principal: + * The regular principal plus the partitionKey origin attribute set to + * the site of the top-level document (i.e., scheme plus eTLD+1). + * - Storage Access Principal: + * A dynamic principal that changes when a resource receives storage access. + * By default, when storage access is denied, this is equal to the + * Partitioned Principal. When storage access is granted, this is equal to + * the Regular Principal. + * - Foreign Partitioned Principal + * A principal that would be decided according to the fact that if the + * resource is a third party or not. If the resource is in a third-party + * context, this will be the partitioned principal. Otherwise, a regular + * principal will be used. Also, this doesn't like Storage Access Principal + * which changes according to storage access of a resource. Note that this + * is dFPI only; this prinipcal will always return regular principal when + * dFPI is disabled. + * + * Consumers of StoragePrincipal can request the principal type that meets their + * needs. For example, storage that should always be partitioned should choose + * the Partitioned Principal, while storage that should change with storage + * access grants should choose the Storage Access Principal. And the storage + * should be always partiitoned in the third-party context should use the + * Foreign Partitioned Principal. + * + * You can obtain these nsIPrincipal objects: + * + * From a Document: + * - Regular Principal: nsINode::NodePrincipal + * - Storage Access Principal: Document::EffectiveStoragePrincipal + * - Partitioned Principal: Document::PartitionedPrincipal + * + * From a Global object: + * - Regular Principal: nsIScriptObjectPrincipal::GetPrincipal + * - Storage Access Principal: + * nsIScriptObjectPrincipal::GetEffectiveStoragePrincipal + * - Partitioned Principal: nsIScriptObjectPrincipal::PartitionedPrincipal + * + * From a Worker: + * - Regular Principal: WorkerPrivate::GetPrincipal (main-thread) + * - Regular Principal: WorkerPrivate::GetPrincipalInfo (worker thread) + * - Storage Access Principal: WorkerPrivate::GetEffectiveStoragePrincipalInfo + * (worker-thread) + * + * For a nsIChannel, the final principals must be calculated and they can be + * obtained by calling: + * - Regular Principal: nsIScriptSecurityManager::getChannelResultPrincipal + * - Storage Access Principal: + * nsIScriptSecurityManager::getChannelResultStoragePrincipal + * - Partitioned and regular Principal: + * nsIScriptSecurityManager::getChannelResultPrincipals + * + * Each use of nsIPrincipal is unique and it should be reviewed by anti-tracking + * peers. But we can group the use of nsIPrincipal in these categories: + * + * - Network loading: use the Regular Principal + * - Cache, not directly visible by content (network cache, HSTS, image cache, + * etc): Use the Storage Access Principal (in the future we will use the + * Partitioned Principal, but this part is not done yet) + * - Storage APIs or anything that is written on disk (or kept in memory in + * private-browsing): use the Storage Access Principal + * - PostMessage: if in the agent-cluster, use the Regular Principal. Otherwise, + * use the Storage Access Principal + * + * Storage access permission + * ~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * When the storage access permission is granted, any of the Storage Access + * Principal getter methods will return the Regular Principal instead of the + * Partitioned Principal, and each storage component should consider the new + * principal only. + * + * The trackers and the 3rd parties (in dFPI) will have access to its + first-party + * cookie jar, escaping from its partitioning. + * + * Storage access permissions can be granted in several ways: + * - The Storage Access API + * (https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API) + * - ETP’s heuristics + * + (https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Privacy/Storage_access_policy#Storage_access_grants) + * - A dFPI-specific login heuristic + * (https://bugzilla.mozilla.org/show_bug.cgi?id=1616585#c12) + * + * There are several ways to receive storage-permission notifications. You can + * use these notifications to re-initialize components, to nullify or enable + them + * to use the “new” effective StoragePrincipal. The list of the notifications + is: + * + * - Add some code in nsGlobalWindowInner::StorageAccessPermissionGranted(). + * - WorkerScope::StorageAccessPermissionGranted for Workers. + * - observe the permission changes (not recommended) + * + * Scope of Storage Access + * ~~~~~~~~~~~~~~~~~~~~~~~ + * + * Immediately after access is granted, the permission is propagated and + notified + * to any contexts (windows and workers) in the same agent-cluster + * (BrowserContextGroup). + * + * This means that if A.com has 2 iframes with B.com, and one of the 2 Bs + obtains + * the storage access, the other B will be notified too. Other B.com, 3rd + parties + * in other agent clusters will not obtain the storage permission. + * + * When the page is reloaded or is loaded for the first time, if it contains + * B.com, and B.com has received the storage permission for the same first-party + * in a previous loading, B.com will have the storage access permission granted + * immediately. + * + * Cookies, LocalStorage, indexedDB + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * When granting storage permission, several storage and channel API getters and + * constructors will start exposing first-party cookie jar objects + (localStorage, + * BroadcastChannel, etc). + * + * There is a side effect of this change: If a tracker has a reference to these + * objects pre-storage permission granting, it will be able to interact with the + * partitioned and the non-partitioned cookie jar at the same time. Note that + * similar synchronization can be done server-side too. Because of this, we + don’t + * think that privacy-wise, this is an issue. + * + * localStorage supports StoragePrincipal, and will be switched after storage + * access is granted. Trackers listed in the pref + * privacy.restrict3rdpartystorage.partitionedHosts will use another special + * partitioned session-only storage called PartitionedLocalStorage. + * + * sessionStorage is not covered by StoragePrincipal, but is double-keyed using + * the top-level site when dFPI is active + * (https://bugzilla.mozilla.org/show_bug.cgi?id=1629707). + * + * SharedWorkers and BroadcastChannels + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * SharedWorker and BroadcastChannel instances latch the effective storage + * principal at the moment of their creation. Existing bindings to the + * partitioned storage principal will continue to exist and operate even as it + * becomes possible to create bindings associated with the Regular Principal. + * This makes it possible for such globals to bi-directionally bridge + information + * between partitioned and non-partitioned principals. + * + * This is true until the page is reloaded. After the reload, the partitioned + * cookie jar will no longer be accessible. + * + * We are planning to clear the partitioned site-data as soon as the page is + * reloaded or dismissed (not done yet - bug 1628313). + * + * {Dedicated,Shared,Service}Workers + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * The storage access permission propagation happens with a ControlRunnable. + This + * could impact the use of sync event-loops. Take a reference of the principal + * you want to use because it can change! + * + * ServiceWorkers are currently disabled for partitioned contexts. + * + * Client API uses the regular nsIPrincipal always because there is not a direct + * connection between this API and the cookie jar. If we want to support + * ServiceWorkers in partitioned context, this part must be revisited. + */ + +class nsIChannel; +class nsICookieJarSettings; +class nsIDocShell; +class nsILoadGroup; +class nsIPrincipal; +class nsIURI; +class nsPIDOMWindowInner; + +namespace mozilla { + +namespace dom { +class Document; +class WorkerPrivate; +} // namespace dom + +namespace ipc { +class PrincipalInfo; +} + +class OriginAttributes; + +class StoragePrincipalHelper final { + public: + static nsresult Create(nsIChannel* aChannel, nsIPrincipal* aPrincipal, + bool aForceIsolation, + nsIPrincipal** aStoragePrincipal); + + static nsresult CreatePartitionedPrincipalForServiceWorker( + nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings, + nsIPrincipal** aPartitionedPrincipal); + + static nsresult PrepareEffectiveStoragePrincipalOriginAttributes( + nsIChannel* aChannel, OriginAttributes& aOriginAttributes); + + // A helper function to verify storage principal info with the principal info. + static bool VerifyValidStoragePrincipalInfoForPrincipalInfo( + const mozilla::ipc::PrincipalInfo& aStoragePrincipalInfo, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo); + + // A helper function to verify client principal info with the principal info. + // + // Note that the client principal refers the principal of the client, which is + // supposed to be the foreign partitioned principal. + static bool VerifyValidClientPrincipalInfoForPrincipalInfo( + const mozilla::ipc::PrincipalInfo& aClientPrincipalInfo, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo); + + enum PrincipalType { + // This is the first-party principal. + eRegularPrincipal, + + // This is a dynamic principal based on the current state of the origin. If + // the origin has the storage permission granted, effective storagePrincipal + // will be the regular principal, otherwise, the partitioned Principal + // will be used. + eStorageAccessPrincipal, + + // This is the first-party principal, plus, First-party isolation attribute + // set. + ePartitionedPrincipal, + + // This principal returns different results based on whether its associated + // channel/window is in a third-party context. While in a third-party + // context, it returns the partitioned principal; otherwise, it returns the + // regular principal. + // + // Note that this principal is not a dynamic principal like + // `eStorageAccessPrincipal`, which changes depending on whether the storage + // access permission is granted. This principal doesn't take the storage + // access permission into consideration. Also, this principle is used in + // dFPI only, meaning that it always returns the regular principal when dFP + // Is disabled. + eForeignPartitionedPrincipal, + }; + + /** + * Extract the principal from the channel/document according to the given + * principal type. + */ + static nsresult GetPrincipal(nsIChannel* aChannel, + PrincipalType aPrincipalType, + nsIPrincipal** aPrincipal); + static nsresult GetPrincipal(nsPIDOMWindowInner* aWindow, + PrincipalType aPrincipalType, + nsIPrincipal** aPrincipal); + + // Check if we need to use the partitioned principal for the service worker of + // the given docShell. Please do not use this API unless you cannot get the + // foreign partitioned principal, e.g. creating the inital about:blank page. + static bool ShouldUsePartitionPrincipalForServiceWorker( + nsIDocShell* aDocShell); + + static bool ShouldUsePartitionPrincipalForServiceWorker( + dom::WorkerPrivate* aWorkerPrivate); + + /** + * Extract the right OriginAttributes from the channel's triggering + * principal. + */ + static bool GetOriginAttributes(nsIChannel* aChannel, + OriginAttributes& aAttributes, + PrincipalType aPrincipalType); + + static bool GetRegularPrincipalOriginAttributes( + dom::Document* aDocument, OriginAttributes& aAttributes); + + static bool GetRegularPrincipalOriginAttributes( + nsILoadGroup* aLoadGroup, OriginAttributes& aAttributes); + + // These methods return the correct originAttributes to be used for network + // state components (HSTS, network cache, image-cache, and so on). + static bool GetOriginAttributesForNetworkState(nsIChannel* aChannel, + OriginAttributes& aAttributes); + static void GetOriginAttributesForNetworkState(dom::Document* aDocument, + OriginAttributes& aAttributes); + static void UpdateOriginAttributesForNetworkState( + nsIURI* aFirstPartyURI, OriginAttributes& aAttributes); + + // For HSTS we want to force 'HTTP' in the partition key. + static bool GetOriginAttributesForHSTS(nsIChannel* aChannel, + OriginAttributes& aAttributes); + + // Like the function above, this function forces `HTTPS` in the partition key. + // The OA created by this function is mainly used in DNS cache. The spec + // specifies that the presence of HTTPS RR for an origin also indicates that + // all HTTP resources are available over HTTPS, so we use this function to + // ensure that all HTTPS RRs in DNS cache are accessed by HTTPS requests only. + static bool GetOriginAttributesForHTTPSRR(nsIChannel* aChannel, + OriginAttributes& aAttributes); + + // Get the origin attributes from a PrincipalInfo + static bool GetOriginAttributes( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + OriginAttributes& aAttributes); + + static bool PartitionKeyHasBaseDomain(const nsAString& aPartitionKey, + const nsACString& aBaseDomain); + + static bool PartitionKeyHasBaseDomain(const nsAString& aPartitionKey, + const nsAString& aBaseDomain); +}; + +} // namespace mozilla + +#endif // mozilla_StoragePrincipalHelper_h diff --git a/toolkit/components/antitracking/TemporaryAccessGrantObserver.cpp b/toolkit/components/antitracking/TemporaryAccessGrantObserver.cpp new file mode 100644 index 0000000000..b36b2f83c7 --- /dev/null +++ b/toolkit/components/antitracking/TemporaryAccessGrantObserver.cpp @@ -0,0 +1,97 @@ +/* -*- 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 "TemporaryAccessGrantObserver.h" + +#include "mozilla/PermissionManager.h" +#include "mozilla/Services.h" +#include "nsIObserverService.h" +#include "nsTHashtable.h" +#include "nsXULAppAPI.h" + +using namespace mozilla; + +StaticAutoPtr<TemporaryAccessGrantObserver::ObserversTable> + TemporaryAccessGrantObserver::sObservers; + +TemporaryAccessGrantObserver::TemporaryAccessGrantObserver( + PermissionManager* 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"); +} + +NS_IMPL_ISUPPORTS(TemporaryAccessGrantObserver, nsIObserver, nsINamed) + +// static +void TemporaryAccessGrantObserver::Create(PermissionManager* aPM, + nsIPrincipal* aPrincipal, + const nsACString& aType) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!sObservers) { + sObservers = new ObserversTable(); + } + sObservers->LookupOrInsertWith( + std::make_pair(nsCOMPtr<nsIPrincipal>(aPrincipal), nsCString(aType)), + [&]() -> nsCOMPtr<nsITimer> { + // Only create a new observer if we don't have a matching + // entry in our hashtable. + nsCOMPtr<nsITimer> timer; + RefPtr<TemporaryAccessGrantObserver> 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 TemporaryAccessGrantObserver::SetTimer(nsITimer* aTimer) { + mTimer = aTimer; + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + } +} + +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(std::make_pair(mPrincipal, mType)); + } else if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + sObservers = nullptr; + } + + return NS_OK; +} + +NS_IMETHODIMP +TemporaryAccessGrantObserver::GetName(nsACString& aName) { + aName.AssignLiteral("TemporaryAccessGrantObserver"); + return NS_OK; +} diff --git a/toolkit/components/antitracking/TemporaryAccessGrantObserver.h b/toolkit/components/antitracking/TemporaryAccessGrantObserver.h new file mode 100644 index 0000000000..adb83f398f --- /dev/null +++ b/toolkit/components/antitracking/TemporaryAccessGrantObserver.h @@ -0,0 +1,88 @@ +/* -*- 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/. */ + +#ifndef mozilla_temporaryaccessgrantobserver_h +#define mozilla_temporaryaccessgrantobserver_h + +#include "mozilla/PrincipalHashKey.h" +#include "mozilla/StaticPtr.h" +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsHashtablesFwd.h" +#include "nsTHashMap.h" +#include "nsINamed.h" +#include "nsIObserver.h" +#include "nsString.h" +#include "PLDHashTable.h" + +class nsITimer; +class TemporaryAccessGrantCacheKey; + +namespace mozilla { + +class PermissionManager; + +class TemporaryAccessGrantCacheKey : public PrincipalHashKey { + public: + typedef std::pair<nsCOMPtr<nsIPrincipal>, nsCString> KeyType; + typedef const KeyType* KeyTypePointer; + + explicit TemporaryAccessGrantCacheKey(KeyTypePointer aKey) + : PrincipalHashKey(aKey->first), mType(aKey->second) {} + TemporaryAccessGrantCacheKey(TemporaryAccessGrantCacheKey&& aOther) = default; + + ~TemporaryAccessGrantCacheKey() = default; + + KeyType GetKey() const { return std::make_pair(mPrincipal, mType); } + bool KeyEquals(KeyTypePointer aKey) const { + return PrincipalHashKey::KeyEquals(aKey->first) && mType == aKey->second; + } + + static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) { + if (!aKey) { + return 0; + } + + return HashGeneric(PrincipalHashKey::HashKey(aKey->first), + HashString(aKey->second)); + } + + enum { ALLOW_MEMMOVE = true }; + + private: + nsCString mType; +}; + +class TemporaryAccessGrantObserver final : public nsIObserver, public nsINamed { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSINAMED + + static void Create(PermissionManager* aPM, nsIPrincipal* aPrincipal, + const nsACString& aType); + + void SetTimer(nsITimer* aTimer); + + private: + TemporaryAccessGrantObserver(PermissionManager* aPM, nsIPrincipal* aPrincipal, + const nsACString& aType); + ~TemporaryAccessGrantObserver() = default; + + private: + using ObserversTable = + nsTHashMap<TemporaryAccessGrantCacheKey, nsCOMPtr<nsITimer>>; + static StaticAutoPtr<ObserversTable> sObservers; + nsCOMPtr<nsITimer> mTimer; + RefPtr<PermissionManager> mPM; + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCString mType; +}; + +} // namespace mozilla + +#endif // mozilla_temporaryaccessgrantobserver_h diff --git a/toolkit/components/antitracking/TrackingDBService.sys.mjs b/toolkit/components/antitracking/TrackingDBService.sys.mjs new file mode 100644 index 0000000000..cd1187d934 --- /dev/null +++ b/toolkit/components/antitracking/TrackingDBService.sys.mjs @@ -0,0 +1,397 @@ +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { Sqlite } from "resource://gre/modules/Sqlite.sys.mjs"; + +const SCHEMA_VERSION = 1; +const TRACKERS_BLOCKED_COUNT = "contentblocking.trackers_blocked_count"; + +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "DB_PATH", function () { + return PathUtils.join(PathUtils.profileDir, "protections.sqlite"); +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "social_enabled", + "privacy.socialtracking.block_cookies.enabled", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "fpp_enabled", + "privacy.fingerprintingProtection", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "milestoneMessagingEnabled", + "browser.contentblocking.cfr-milestone.enabled", + false +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "milestones", + "browser.contentblocking.cfr-milestone.milestones", + "[]", + null, + JSON.parse +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "oldMilestone", + "browser.contentblocking.cfr-milestone.milestone-achieved", + 0 +); + +// How often we check if the user is eligible for seeing a "milestone" +// doorhanger. 24 hours by default. +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "MILESTONE_UPDATE_INTERVAL", + "browser.contentblocking.cfr-milestone.update-interval", + 24 * 60 * 60 * 1000 +); + +ChromeUtils.defineESModuleGetters(lazy, { + AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", +}); + +/** + * All SQL statements should be defined here. + */ +const SQL = { + createEvents: + "CREATE TABLE events (" + + "id INTEGER PRIMARY KEY, " + + "type INTEGER NOT NULL, " + + "count INTEGER NOT NULL, " + + "timestamp DATE " + + ");", + + addEvent: + "INSERT INTO events (type, count, timestamp) " + + "VALUES (:type, 1, date(:date));", + + incrementEvent: "UPDATE events SET count = count + 1 WHERE id = :id;", + + selectByTypeAndDate: + "SELECT * FROM events " + + "WHERE type = :type " + + "AND timestamp = date(:date);", + + deleteEventsRecords: "DELETE FROM events;", + + removeRecordsSince: "DELETE FROM events WHERE timestamp >= date(:date);", + + selectByDateRange: + "SELECT * FROM events " + + "WHERE timestamp BETWEEN date(:dateFrom) AND date(:dateTo);", + + sumAllEvents: "SELECT sum(count) FROM events;", + + getEarliestDate: + "SELECT timestamp FROM events ORDER BY timestamp ASC LIMIT 1;", +}; + +/** + * Creates the database schema. + */ +async function createDatabase(db) { + await db.execute(SQL.createEvents); +} + +async function removeAllRecords(db) { + await db.execute(SQL.deleteEventsRecords); +} + +async function removeRecordsSince(db, date) { + await db.execute(SQL.removeRecordsSince, { date }); +} + +export function TrackingDBService() { + this._initPromise = this._initialize(); +} + +TrackingDBService.prototype = { + classID: Components.ID("{3c9c43b6-09eb-4ed2-9b87-e29f4221eef0}"), + QueryInterface: ChromeUtils.generateQI(["nsITrackingDBService"]), + // This is the connection to the database, opened in _initialize and closed on _shutdown. + _db: null, + waitingTasks: new Set(), + finishedShutdown: true, + + async ensureDB() { + await this._initPromise; + return this._db; + }, + + async _initialize() { + let db = await Sqlite.openConnection({ path: lazy.DB_PATH }); + + try { + // Check to see if we need to perform any migrations. + let dbVersion = parseInt(await db.getSchemaVersion()); + + // getSchemaVersion() returns a 0 int if the schema + // version is undefined. + if (dbVersion === 0) { + await createDatabase(db); + } else if (dbVersion < SCHEMA_VERSION) { + // TODO + // await upgradeDatabase(db, dbVersion, SCHEMA_VERSION); + } + + await db.setSchemaVersion(SCHEMA_VERSION); + } catch (e) { + // Close the DB connection before passing the exception to the consumer. + await db.close(); + throw e; + } + + lazy.AsyncShutdown.profileBeforeChange.addBlocker( + "TrackingDBService: Shutting down the content blocking database.", + () => this._shutdown() + ); + this.finishedShutdown = false; + this._db = db; + }, + + async _shutdown() { + let db = await this.ensureDB(); + this.finishedShutdown = true; + await Promise.all(Array.from(this.waitingTasks, task => task.finalize())); + await db.close(); + }, + + async recordContentBlockingLog(data) { + if (this.finishedShutdown) { + // The database has already been closed. + return; + } + let task = new lazy.DeferredTask(async () => { + try { + await this.saveEvents(data); + } finally { + this.waitingTasks.delete(task); + } + }, 0); + task.arm(); + this.waitingTasks.add(task); + }, + + identifyType(events) { + let result = null; + let isTracker = false; + for (let [state, blocked] of events) { + if ( + state & + Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT || + state & Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_2_TRACKING_CONTENT + ) { + isTracker = true; + } + if (blocked) { + if ( + state & + Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT || + state & + Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT + ) { + result = Ci.nsITrackingDBService.FINGERPRINTERS_ID; + } else if ( + lazy.fpp_enabled && + state & + Ci.nsIWebProgressListener.STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING + ) { + // The suspicious fingerprinting event gets filed in standard windows + // regardless of whether the fingerprinting protection is enabled. To + // avoid recording the case where our protection doesn't apply, we + // only record blocking suspicious fingerprinting if the + // fingerprinting protection is enabled in the normal windows. + // + // TODO(Bug 1864909): We don't need to check if fingerprinting + // protection is enabled once the event only gets filed when + // fingerprinting protection is enabled for the context. + result = Ci.nsITrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID; + } else if ( + // If STP is enabled and either a social tracker or cookie is blocked. + lazy.social_enabled && + (state & + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER || + state & + Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT) + ) { + result = Ci.nsITrackingDBService.SOCIAL_ID; + } else if ( + // If there is a tracker blocked. If there is a social tracker blocked, but STP is not enabled. + state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT || + state & Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT + ) { + result = Ci.nsITrackingDBService.TRACKERS_ID; + } else if ( + // If a tracking cookie was blocked attribute it to tracking cookies. + // This includes social tracking cookies since STP is not enabled. + state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER || + state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER + ) { + result = Ci.nsITrackingDBService.TRACKING_COOKIES_ID; + } else if ( + state & + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION || + state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL || + state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN + ) { + result = Ci.nsITrackingDBService.OTHER_COOKIES_BLOCKED_ID; + } else if ( + state & Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT + ) { + result = Ci.nsITrackingDBService.CRYPTOMINERS_ID; + } + } + } + // if a cookie is blocked for any reason, and it is identified as a tracker, + // then add to the tracking cookies count. + if ( + result == Ci.nsITrackingDBService.OTHER_COOKIES_BLOCKED_ID && + isTracker + ) { + result = Ci.nsITrackingDBService.TRACKING_COOKIES_ID; + } + + return result; + }, + + /** + * Saves data rows to the DB. + * @param data + * An array of JS objects representing row items to save. + */ + async saveEvents(data) { + let db = await this.ensureDB(); + let log = JSON.parse(data); + try { + await db.executeTransaction(async () => { + for (let thirdParty in log) { + // "type" will be undefined if there is no blocking event, or 0 if it is a + // cookie which is not a tracking cookie. These should not be added to the database. + let type = this.identifyType(log[thirdParty]); + if (type) { + // Send the blocked event to Telemetry + Services.telemetry.scalarAdd(TRACKERS_BLOCKED_COUNT, 1); + + // today is a date "YYY-MM-DD" which can compare with what is + // already saved in the database. + let today = new Date().toISOString().split("T")[0]; + let row = await db.executeCached(SQL.selectByTypeAndDate, { + type, + date: today, + }); + let todayEntry = row[0]; + + // If previous events happened today (local time), aggregate them. + if (todayEntry) { + let id = todayEntry.getResultByName("id"); + await db.executeCached(SQL.incrementEvent, { id }); + } else { + // Event is created on a new day, add a new entry. + await db.executeCached(SQL.addEvent, { type, date: today }); + } + } + } + }); + } catch (e) { + console.error(e); + } + + // If milestone CFR messaging is not enabled we don't need to update the milestone pref or send the event. + // We don't do this check too frequently, for performance reasons. + if ( + !lazy.milestoneMessagingEnabled || + (this.lastChecked && + Date.now() - this.lastChecked < lazy.MILESTONE_UPDATE_INTERVAL) + ) { + return; + } + this.lastChecked = Date.now(); + let totalSaved = await this.sumAllEvents(); + + let reachedMilestone = null; + let nextMilestone = null; + for (let [index, milestone] of lazy.milestones.entries()) { + if (totalSaved >= milestone) { + reachedMilestone = milestone; + nextMilestone = lazy.milestones[index + 1]; + } + } + + // Show the milestone message if the user is not too close to the next milestone. + // Or if there is no next milestone. + if ( + reachedMilestone && + (!nextMilestone || nextMilestone - totalSaved > 3000) && + (!lazy.oldMilestone || lazy.oldMilestone < reachedMilestone) + ) { + Services.obs.notifyObservers( + { + wrappedJSObject: { + event: "ContentBlockingMilestone", + }, + }, + "SiteProtection:ContentBlockingMilestone" + ); + } + }, + + async clearAll() { + let db = await this.ensureDB(); + await removeAllRecords(db); + }, + + async clearSince(date) { + let db = await this.ensureDB(); + date = new Date(date).toISOString(); + await removeRecordsSince(db, date); + }, + + async getEventsByDateRange(dateFrom, dateTo) { + let db = await this.ensureDB(); + dateFrom = new Date(dateFrom).toISOString(); + dateTo = new Date(dateTo).toISOString(); + return db.execute(SQL.selectByDateRange, { dateFrom, dateTo }); + }, + + async sumAllEvents() { + let db = await this.ensureDB(); + let results = await db.execute(SQL.sumAllEvents); + if (!results[0]) { + return 0; + } + let total = results[0].getResultByName("sum(count)"); + return total || 0; + }, + + async getEarliestRecordedDate() { + let db = await this.ensureDB(); + let date = await db.execute(SQL.getEarliestDate); + if (!date[0]) { + return null; + } + let earliestDate = date[0].getResultByName("timestamp"); + + // All of our dates are recorded as 00:00 GMT, add 12 hours to the timestamp + // to ensure we display the correct date no matter the user's location. + let hoursInMS12 = 12 * 60 * 60 * 1000; + let earliestDateInMS = new Date(earliestDate).getTime() + hoursInMS12; + + return earliestDateInMS || null; + }, +}; diff --git a/toolkit/components/antitracking/URLDecorationAnnotationsService.sys.mjs b/toolkit/components/antitracking/URLDecorationAnnotationsService.sys.mjs new file mode 100644 index 0000000000..ca285e972d --- /dev/null +++ b/toolkit/components/antitracking/URLDecorationAnnotationsService.sys.mjs @@ -0,0 +1,70 @@ +/* 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/. */ + +export function URLDecorationAnnotationsService() {} + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", +}); + +const COLLECTION_NAME = "anti-tracking-url-decoration"; +const PREF_NAME = "privacy.restrict3rdpartystorage.url_decorations"; + +URLDecorationAnnotationsService.prototype = { + classID: Components.ID("{5874af6d-5719-4e1b-b155-ef4eae7fcb32}"), + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsIURLDecorationAnnotationsService", + ]), + + _initialized: false, + _prefBranch: null, + + onDataAvailable(entries) { + // Use this technique in order to ensure the pref cannot be changed by the + // user e.g. through about:config. This preferences is only intended as a + // mechanism for reflecting this data to content processes. + if (this._prefBranch === null) { + this._prefBranch = Services.prefs.getDefaultBranch(""); + } + + const branch = this._prefBranch; + branch.unlockPref(PREF_NAME); + branch.setStringPref( + PREF_NAME, + entries.map(x => x.token.replace(/ /, "%20")).join(" ") + ); + branch.lockPref(PREF_NAME); + }, + + observe(aSubject, aTopic, aData) { + if (aTopic == "profile-after-change") { + this.ensureUpdated(); + } + }, + + ensureUpdated() { + if (this._initialized) { + return Promise.resolve(); + } + this._initialized = true; + + const client = lazy.RemoteSettings(COLLECTION_NAME); + client.on("sync", event => { + let { + data: { current }, + } = event; + this.onDataAvailable(current); + }); + + // Now trigger an update from the server if necessary to get a fresh copy + // of the data + return client.get({}).then(entries => { + this.onDataAvailable(entries); + return undefined; + }); + }, +}; diff --git a/toolkit/components/antitracking/URLDecorationStripper.cpp b/toolkit/components/antitracking/URLDecorationStripper.cpp new file mode 100644 index 0000000000..38af391945 --- /dev/null +++ b/toolkit/components/antitracking/URLDecorationStripper.cpp @@ -0,0 +1,80 @@ +/* -*- 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 "URLDecorationStripper.h" + +#include "mozilla/Preferences.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsEffectiveTLDService.h" +#include "nsIURI.h" +#include "nsIURIMutator.h" +#include "nsURLHelper.h" + +namespace { +static const char* kPrefName = + "privacy.restrict3rdpartystorage.url_decorations"; +} // namespace + +namespace mozilla { + +nsresult URLDecorationStripper::StripTrackingIdentifiers(nsIURI* aURI, + nsACString& aOutSpec) { + nsAutoString tokenList; + nsresult rv = Preferences::GetString(kPrefName, tokenList); + ToLowerCase(tokenList); + + nsAutoCString path; + rv = aURI->GetPathQueryRef(path); + NS_ENSURE_SUCCESS(rv, rv); + ToLowerCase(path); + + int32_t queryBegins = path.FindChar('?'); + // Only positive values are valid since the path must begin with a '/'. + if (queryBegins > 0) { + for (const nsAString& token : tokenList.Split(' ')) { + if (token.IsEmpty()) { + continue; + } + + nsAutoString value; + if (URLParams::Extract(Substring(path, queryBegins + 1), token, value) && + !value.IsVoid()) { + // Tracking identifier found in the URL! + return StripToRegistrableDomain(aURI, aOutSpec); + } + } + } + + return aURI->GetSpec(aOutSpec); +} + +nsresult URLDecorationStripper::StripToRegistrableDomain(nsIURI* aURI, + nsACString& aOutSpec) { + NS_MutateURI mutator(aURI); + mutator.SetPathQueryRef(""_ns).SetUserPass(""_ns); + + RefPtr<nsEffectiveTLDService> etldService = + nsEffectiveTLDService::GetInstance(); + NS_ENSURE_TRUE(etldService, NS_ERROR_FAILURE); + nsAutoCString baseDomain; + nsresult rv = etldService->GetBaseDomain(aURI, 0, baseDomain); + if (NS_SUCCEEDED(rv)) { + mutator.SetHost(baseDomain); + } else { + // If this is an IP address or something like "localhost", ignore the error. + if (rv != NS_ERROR_HOST_IS_IP_ADDRESS && + rv != NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) { + return rv; + } + } + + nsCOMPtr<nsIURI> uri; + rv = mutator.Finalize(uri); + NS_ENSURE_SUCCESS(rv, rv); + return uri->GetSpec(aOutSpec); +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/URLDecorationStripper.h b/toolkit/components/antitracking/URLDecorationStripper.h new file mode 100644 index 0000000000..9bad66f250 --- /dev/null +++ b/toolkit/components/antitracking/URLDecorationStripper.h @@ -0,0 +1,26 @@ +/* -*- 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/. */ + +#ifndef mozilla_URLDecorationStripper_h +#define mozilla_URLDecorationStripper_h + +#include "nsStringFwd.h" + +class nsIURI; + +namespace mozilla { + +class URLDecorationStripper final { + public: + static nsresult StripTrackingIdentifiers(nsIURI* aURI, nsACString& aOutSpec); + + private: + static nsresult StripToRegistrableDomain(nsIURI* aURI, nsACString& aOutSpec); +}; + +} // namespace mozilla + +#endif // mozilla_URLDecorationStripper_h diff --git a/toolkit/components/antitracking/URLQueryStringStripper.cpp b/toolkit/components/antitracking/URLQueryStringStripper.cpp new file mode 100644 index 0000000000..3b46738280 --- /dev/null +++ b/toolkit/components/antitracking/URLQueryStringStripper.cpp @@ -0,0 +1,429 @@ +/* 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 "URLQueryStringStripper.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Telemetry.h" + +#include "nsEffectiveTLDService.h" +#include "nsISupportsImpl.h" +#include "nsIURI.h" +#include "nsIURIMutator.h" +#include "nsUnicharUtils.h" +#include "nsURLHelper.h" +#include "mozilla/dom/StripOnShareRuleBinding.h" + +namespace { + +mozilla::StaticRefPtr<mozilla::URLQueryStringStripper> gQueryStringStripper; + +static const char kQueryStrippingEnabledPref[] = + "privacy.query_stripping.enabled"; +static const char kQueryStrippingEnabledPBMPref[] = + "privacy.query_stripping.enabled.pbmode"; +static const char kQueryStrippingOnShareEnabledPref[] = + "privacy.query_stripping.strip_on_share.enabled"; + +} // namespace + +namespace mozilla { + +NS_IMPL_ISUPPORTS(URLQueryStringStripper, nsIObserver, + nsIURLQueryStringStripper, nsIURLQueryStrippingListObserver) + +// static +already_AddRefed<URLQueryStringStripper> +URLQueryStringStripper::GetSingleton() { + if (!gQueryStringStripper) { + gQueryStringStripper = new URLQueryStringStripper(); + // Check initial pref state and enable service. We can pass nullptr, because + // OnPrefChange doesn't rely on the args. + URLQueryStringStripper::OnPrefChange(nullptr, nullptr); + + RunOnShutdown( + [&] { + DebugOnly<nsresult> rv = gQueryStringStripper->Shutdown(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "URLQueryStringStripper::Shutdown failed"); + gQueryStringStripper = nullptr; + }, + ShutdownPhase::XPCOMShutdown); + } + + return do_AddRef(gQueryStringStripper); +} + +URLQueryStringStripper::URLQueryStringStripper() { + mIsInitialized = false; + + nsresult rv = Preferences::RegisterCallback( + &URLQueryStringStripper::OnPrefChange, kQueryStrippingEnabledPBMPref); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = Preferences::RegisterCallback(&URLQueryStringStripper::OnPrefChange, + kQueryStrippingEnabledPref); + + rv = Preferences::RegisterCallback(&URLQueryStringStripper::OnPrefChange, + kQueryStrippingOnShareEnabledPref); + NS_ENSURE_SUCCESS_VOID(rv); +} + +NS_IMETHODIMP +URLQueryStringStripper::StripForCopyOrShare(nsIURI* aURI, + nsIURI** strippedURI) { + if (!StaticPrefs::privacy_query_stripping_strip_on_share_enabled()) { + return NS_ERROR_NOT_AVAILABLE; + } + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(strippedURI); + int aStripCount = 0; + + nsAutoCString query; + nsresult rv = aURI->GetQuery(query); + NS_ENSURE_SUCCESS(rv, rv); + // We don't need to do anything if there is no query string. + if (query.IsEmpty()) { + Telemetry::Accumulate(Telemetry::STRIP_ON_SHARE_PARAMS_REMOVED, 0); + return NS_OK; + } + nsAutoCString host; + rv = aURI->GetHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + URLParams params; + + URLParams::Parse(query, [&](nsString&& name, nsString&& value) { + nsAutoString lowerCaseName; + ToLowerCase(name, lowerCaseName); + // Look through the global rules. + dom::StripRule globalRule; + bool keyExists = mStripOnShareMap.Get("*"_ns, &globalRule); + // There should always be a global rule. + MOZ_ASSERT(keyExists); + for (const auto& param : globalRule.mQueryParams) { + if (param == lowerCaseName) { + aStripCount++; + return true; + } + } + + // Check for site specific rules. + dom::StripRule siteSpecificRule; + keyExists = mStripOnShareMap.Get(host, &siteSpecificRule); + if (keyExists) { + for (const auto& param : siteSpecificRule.mQueryParams) { + if (param == lowerCaseName) { + aStripCount++; + return true; + } + } + } + + params.Append(name, value); + return true; + }); + + Telemetry::Accumulate(Telemetry::STRIP_ON_SHARE_PARAMS_REMOVED, aStripCount); + + if (!aStripCount) { + return NS_OK; + } + + nsAutoString newQuery; + params.Serialize(newQuery, false); + + Unused << NS_MutateURI(aURI) + .SetQuery(NS_ConvertUTF16toUTF8(newQuery)) + .Finalize(strippedURI); + + // To calculate difference in length of the URL + // after stripping occurs for Telemetry + nsAutoCString specOriginalURI; + nsAutoCString specStrippedURI; + + rv = aURI->GetDisplaySpec(specOriginalURI); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(*strippedURI); + + rv = (*strippedURI)->GetDisplaySpec(specStrippedURI); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t lengthDiff = specOriginalURI.Length() - specStrippedURI.Length(); + Telemetry::Accumulate(Telemetry::STRIP_ON_SHARE_LENGTH_DECREASE, lengthDiff); + + return NS_OK; +} + +NS_IMETHODIMP +URLQueryStringStripper::Strip(nsIURI* aURI, bool aIsPBM, nsIURI** aOutput, + uint32_t* aStripCount) { + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aOutput); + NS_ENSURE_ARG_POINTER(aStripCount); + + *aStripCount = 0; + + if (aIsPBM) { + if (!StaticPrefs::privacy_query_stripping_enabled_pbmode()) { + return NS_OK; + } + } else { + if (!StaticPrefs::privacy_query_stripping_enabled()) { + return NS_OK; + } + } + + if (CheckAllowList(aURI)) { + return NS_OK; + } + + return StripQueryString(aURI, aOutput, aStripCount); +} + +// static +void URLQueryStringStripper::OnPrefChange(const char* aPref, void* aData) { + MOZ_ASSERT(gQueryStringStripper); + + bool prefEnablesComponent = + StaticPrefs::privacy_query_stripping_enabled() || + StaticPrefs::privacy_query_stripping_enabled_pbmode() || + StaticPrefs::privacy_query_stripping_strip_on_share_enabled(); + + nsresult rv; + if (prefEnablesComponent) { + rv = gQueryStringStripper->Init(); + } else { + rv = gQueryStringStripper->Shutdown(); + } + NS_ENSURE_SUCCESS_VOID(rv); +} + +nsresult URLQueryStringStripper::Init() { + nsresult rv; + if (mIsInitialized) { + rv = gQueryStringStripper->ManageObservers(); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + mIsInitialized = true; + + mListService = do_GetService("@mozilla.org/query-stripping-list-service;1"); + NS_ENSURE_TRUE(mListService, NS_ERROR_FAILURE); + rv = gQueryStringStripper->ManageObservers(); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +// (Un)registers a QPS/Strip-on-share observer according to the QPS prefs states +// and the strip-on-share pref state. This is called whenever one of the three +// prefs changes, to ensure that we are not observing one of the lists although +// the corresponding feature is not turned on. +nsresult URLQueryStringStripper::ManageObservers() { + MOZ_ASSERT(mListService); + nsresult rv; + // Register QPS observer. + // We are not listening to QPS but the feature is on, register a listener. + if (!mObservingQPS) { + if (StaticPrefs::privacy_query_stripping_enabled() || + StaticPrefs::privacy_query_stripping_enabled_pbmode()) { + rv = mListService->RegisterAndRunObserver(gQueryStringStripper); + NS_ENSURE_SUCCESS(rv, rv); + mObservingQPS = true; + } + } else { + // Unregister QPS observer. + // We are listening to QPS but the feature is off, unregister. + if (!StaticPrefs::privacy_query_stripping_enabled() && + !StaticPrefs::privacy_query_stripping_enabled_pbmode()) { + // Clean up QPS lists. + mList.Clear(); + mAllowList.Clear(); + rv = mListService->UnregisterObserver(this); + NS_ENSURE_SUCCESS(rv, rv); + mObservingQPS = false; + } + } + + // Register Strip on Share observer. + // We are not listening to strip-on-share but the feature is on, register an + // Observer. + if (!mObservingStripOnShare) { + if (StaticPrefs::privacy_query_stripping_strip_on_share_enabled()) { + rv = mListService->RegisterAndRunObserverStripOnShare( + gQueryStringStripper); + NS_ENSURE_SUCCESS(rv, rv); + mObservingStripOnShare = true; + } + } else { + // Unregister Strip on Share observer. + // We are listening to strip-on-share but the feature is off, unregister. + if (!StaticPrefs::privacy_query_stripping_strip_on_share_enabled()) { + // Clean up strip-on-share list + mStripOnShareMap.Clear(); + rv = mListService->UnregisterStripOnShareObserver(this); + NS_ENSURE_SUCCESS(rv, rv); + mObservingStripOnShare = false; + } + } + return NS_OK; +} + +nsresult URLQueryStringStripper::Shutdown() { + if (!mIsInitialized) { + return NS_OK; + } + nsresult rv = gQueryStringStripper->ManageObservers(); + NS_ENSURE_SUCCESS(rv, rv); + mIsInitialized = false; + mListService = nullptr; + return NS_OK; +} + +nsresult URLQueryStringStripper::StripQueryString(nsIURI* aURI, + nsIURI** aOutput, + uint32_t* aStripCount) { + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aOutput); + NS_ENSURE_ARG_POINTER(aStripCount); + + *aStripCount = 0; + + nsCOMPtr<nsIURI> uri(aURI); + + nsAutoCString query; + nsresult rv = aURI->GetQuery(query); + NS_ENSURE_SUCCESS(rv, rv); + + // We don't need to do anything if there is no query string. + if (query.IsEmpty()) { + return NS_OK; + } + + URLParams params; + + URLParams::Parse(query, [&](nsString&& name, nsString&& value) { + nsAutoString lowerCaseName; + + ToLowerCase(name, lowerCaseName); + + if (mList.Contains(lowerCaseName)) { + *aStripCount += 1; + + // Count how often a specific query param is stripped. For privacy reasons + // this will only count query params listed in the Histogram definition. + // Calls for any other query params will be discarded. + nsAutoCString telemetryLabel("param_"); + AppendUTF16toUTF8(lowerCaseName, telemetryLabel); + Telemetry::AccumulateCategorical( + Telemetry::QUERY_STRIPPING_COUNT_BY_PARAM, telemetryLabel); + + return true; + } + + params.Append(name, value); + return true; + }); + + // Return if there is no parameter has been stripped. + if (!*aStripCount) { + return NS_OK; + } + + nsAutoString newQuery; + params.Serialize(newQuery, false); + + Unused << NS_MutateURI(uri) + .SetQuery(NS_ConvertUTF16toUTF8(newQuery)) + .Finalize(aOutput); + + return NS_OK; +} + +bool URLQueryStringStripper::CheckAllowList(nsIURI* aURI) { + MOZ_ASSERT(aURI); + + // Get the site(eTLD+1) from the URI. + nsAutoCString baseDomain; + nsresult rv = + nsEffectiveTLDService::GetInstance()->GetBaseDomain(aURI, 0, baseDomain); + if (rv == NS_ERROR_HOST_IS_IP_ADDRESS || + rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) { + return false; + } + NS_ENSURE_SUCCESS(rv, false); + + return mAllowList.Contains(baseDomain); +} + +void URLQueryStringStripper::PopulateStripList(const nsAString& aList) { + mList.Clear(); + + for (const nsAString& item : aList.Split(' ')) { + mList.Insert(item); + } +} + +void URLQueryStringStripper::PopulateAllowList(const nsACString& aList) { + mAllowList.Clear(); + + for (const nsACString& item : aList.Split(',')) { + mAllowList.Insert(item); + } +} + +NS_IMETHODIMP +URLQueryStringStripper::OnQueryStrippingListUpdate( + const nsAString& aStripList, const nsACString& aAllowList) { + PopulateStripList(aStripList); + PopulateAllowList(aAllowList); + return NS_OK; +} + +NS_IMETHODIMP +URLQueryStringStripper::OnStripOnShareUpdate(const nsTArray<nsString>& aArgs, + JSContext* aCx) { + for (const auto& ruleString : aArgs) { + dom::StripRule rule; + if (NS_WARN_IF(!rule.Init(ruleString))) { + // Skipping malformed rules + continue; + } + for (const auto& topLevelSite : rule.mTopLevelSites) { + mStripOnShareMap.InsertOrUpdate(NS_ConvertUTF16toUTF8(topLevelSite), + rule); + } + } + return NS_OK; +} +// static +NS_IMETHODIMP +URLQueryStringStripper::TestGetStripList(nsACString& aStripList) { + aStripList.Truncate(); + + StringJoinAppend(aStripList, " "_ns, mList, + [](auto& aResult, const auto& aValue) { + aResult.Append(NS_ConvertUTF16toUTF8(aValue)); + }); + return NS_OK; +} + +/* nsIObserver */ +NS_IMETHODIMP +URLQueryStringStripper::Observe(nsISupports*, const char* aTopic, + const char16_t*) { + // Since this class is created at profile-after-change by the Category + // Manager, it's expected to implement nsIObserver; however, we have nothing + // interesting to do here. + MOZ_ASSERT(strcmp(aTopic, "profile-after-change") == 0); + + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/URLQueryStringStripper.h b/toolkit/components/antitracking/URLQueryStringStripper.h new file mode 100644 index 0000000000..89466cfd12 --- /dev/null +++ b/toolkit/components/antitracking/URLQueryStringStripper.h @@ -0,0 +1,65 @@ +/* -*- 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/. */ + +#ifndef mozilla_URLQueryStringStripper_h +#define mozilla_URLQueryStringStripper_h + +#include "nsIURLQueryStringStripper.h" +#include "nsIURLQueryStrippingListService.h" +#include "nsIObserver.h" +#include "mozilla/dom/StripOnShareRuleBinding.h" +#include "nsStringFwd.h" +#include "nsTHashSet.h" +#include "nsTHashMap.h" + +class nsIURI; + +namespace mozilla { + +class URLQueryStringStripper final : public nsIObserver, + public nsIURLQueryStringStripper, + public nsIURLQueryStrippingListObserver { + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIURLQUERYSTRIPPINGLISTOBSERVER + + NS_DECL_NSIURLQUERYSTRINGSTRIPPER + + public: + static already_AddRefed<URLQueryStringStripper> GetSingleton(); + + private: + URLQueryStringStripper(); + ~URLQueryStringStripper() = default; + + static void OnPrefChange(const char* aPref, void* aData); + nsresult ManageObservers(); + + [[nodiscard]] nsresult Init(); + [[nodiscard]] nsresult Shutdown(); + + [[nodiscard]] nsresult StripQueryString(nsIURI* aURI, nsIURI** aOutput, + uint32_t* aStripCount); + + bool CheckAllowList(nsIURI* aURI); + + void PopulateStripList(const nsAString& aList); + void PopulateAllowList(const nsACString& aList); + + nsTHashSet<nsString> mList; + nsTHashSet<nsCString> mAllowList; + nsCOMPtr<nsIURLQueryStrippingListService> mListService; + nsTHashMap<nsCString, dom::StripRule> mStripOnShareMap; + bool mIsInitialized; + // Indicates whether or not we currently have registered an observer + // for the QPS/strip-on-share list updates + bool mObservingQPS = false; + bool mObservingStripOnShare = false; +}; + +} // namespace mozilla + +#endif // mozilla_URLQueryStringStripper_h diff --git a/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs b/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs new file mode 100644 index 0000000000..baaa9f3824 --- /dev/null +++ b/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs @@ -0,0 +1,420 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", +}); + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const COLLECTION_NAME = "query-stripping"; +const SHARED_DATA_KEY = "URLQueryStripping"; +const PREF_STRIP_LIST_NAME = "privacy.query_stripping.strip_list"; +const PREF_ALLOW_LIST_NAME = "privacy.query_stripping.allow_list"; +const PREF_TESTING_ENABLED = "privacy.query_stripping.testing"; +const PREF_STRIP_IS_TEST = + "privacy.query_stripping.strip_on_share.enableTestMode"; + +ChromeUtils.defineLazyGetter(lazy, "logger", () => { + return console.createInstance({ + prefix: "URLQueryStrippingListService", + maxLogLevelPref: "privacy.query_stripping.listService.logLevel", + }); +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "testStripOnShare", + PREF_STRIP_IS_TEST +); + +// Lazy getter for the strip-on-share strip list. +ChromeUtils.defineLazyGetter(lazy, "StripOnShareList", async () => { + let response = await fetch( + "chrome://global/content/antitracking/StripOnShare.json" + ); + if (!response.ok) { + lazy.logger.error( + "Error fetching strip-on-share strip list" + response.status + ); + throw new Error( + "Error fetching strip-on-share strip list" + response.status + ); + } + return response.json(); +}); + +export class URLQueryStrippingListService { + classId = Components.ID("{afff16f0-3fd2-4153-9ccd-c6d9abd879e4}"); + QueryInterface = ChromeUtils.generateQI(["nsIURLQueryStrippingListService"]); + + #isInitialized = false; + #pendingInit = null; + #initResolver; + #stripOnShareTestList = null; + + #rs; + #onSyncCallback; + + constructor() { + lazy.logger.debug("constructor"); + this.observers = new Set(); + this.stripOnShareObservers = new Set(); + this.stripOnShareParams = null; + this.prefStripList = new Set(); + this.prefAllowList = new Set(); + this.remoteStripList = new Set(); + this.remoteAllowList = new Set(); + this.isParentProcess = + Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT; + } + + #onSync(event) { + lazy.logger.debug("onSync", event); + let { + data: { current }, + } = event; + this._onRemoteSettingsUpdate(current); + } + + async testSetList(testList) { + this.#stripOnShareTestList = testList; + await this._notifyStripOnShareObservers(); + } + + testHasStripOnShareObservers() { + return !!this.stripOnShareObservers.size; + } + + testHasQPSObservers() { + return !!this.observers.size; + } + + async #init() { + // If there is already an init pending wait for it to complete. + if (this.#pendingInit) { + lazy.logger.debug("#init: Waiting for pending init"); + await this.#pendingInit; + return; + } + + if (this.#isInitialized) { + lazy.logger.debug("#init: Skip, already initialized"); + return; + } + // Create a promise that resolves when init is complete. This allows us to + // handle incoming init calls while we're still initializing. + this.#pendingInit = new Promise(initResolve => { + this.#initResolver = initResolve; + }); + this.#isInitialized = true; + + lazy.logger.debug("#init: Run"); + + // We can only access the remote settings in the parent process. For content + // processes, we will use sharedData to sync the list to content processes. + if (this.isParentProcess) { + this.#rs = lazy.RemoteSettings(COLLECTION_NAME); + + if (!this.#onSyncCallback) { + this.#onSyncCallback = this.#onSync.bind(this); + this.#rs.on("sync", this.#onSyncCallback); + } + + // Get the initially available entries for remote settings. + let entries; + try { + entries = await this.#rs.get(); + } catch (e) {} + this._onRemoteSettingsUpdate(entries || []); + } else { + // Register the message listener for the remote settings update from the + // sharedData. + Services.cpmm.sharedData.addEventListener("change", this); + + // Get the remote settings data from the shared data. + let data = this._getListFromSharedData(); + + this._onRemoteSettingsUpdate(data); + } + + // Get the list from pref. + this._onPrefUpdate( + PREF_STRIP_LIST_NAME, + Services.prefs.getStringPref(PREF_STRIP_LIST_NAME, "") + ); + this._onPrefUpdate( + PREF_ALLOW_LIST_NAME, + Services.prefs.getStringPref(PREF_ALLOW_LIST_NAME, "") + ); + + Services.prefs.addObserver(PREF_STRIP_LIST_NAME, this); + Services.prefs.addObserver(PREF_ALLOW_LIST_NAME, this); + + Services.obs.addObserver(this, "xpcom-shutdown"); + + this.#initResolver(); + this.#pendingInit = null; + } + + async #shutdown() { + // Ensure any pending init is done before shutdown. + if (this.#pendingInit) { + await this.#pendingInit; + } + + // Already shut down. + if (!this.#isInitialized) { + return; + } + this.#isInitialized = false; + + lazy.logger.debug("#shutdown"); + + // Unregister RemoteSettings listener (if it was registered). + if (this.#onSyncCallback) { + this.#rs.off("sync", this.#onSyncCallback); + this.#onSyncCallback = null; + } + + Services.obs.removeObserver(this, "xpcom-shutdown"); + Services.prefs.removeObserver(PREF_STRIP_LIST_NAME, this); + Services.prefs.removeObserver(PREF_ALLOW_LIST_NAME, this); + } + + get hasObservers() { + return !this.observers.size && !this.stripOnShareObservers.size; + } + _onRemoteSettingsUpdate(entries) { + this.remoteStripList.clear(); + this.remoteAllowList.clear(); + + for (let entry of entries) { + for (let item of entry.stripList) { + this.remoteStripList.add(item); + } + + for (let item of entry.allowList) { + this.remoteAllowList.add(item); + } + } + + // Because only the parent process will get the remote settings update, so + // we will sync the list to the shared data so that content processes can + // get the list. + if (this.isParentProcess) { + Services.ppmm.sharedData.set(SHARED_DATA_KEY, { + stripList: this.remoteStripList, + allowList: this.remoteAllowList, + }); + + if (Services.prefs.getBoolPref(PREF_TESTING_ENABLED, false)) { + Services.ppmm.sharedData.flush(); + } + } + + this._notifyObservers(); + } + + _onPrefUpdate(pref, value) { + switch (pref) { + case PREF_STRIP_LIST_NAME: + this.prefStripList = new Set(value ? value.split(" ") : []); + break; + + case PREF_ALLOW_LIST_NAME: + this.prefAllowList = new Set(value ? value.split(",") : []); + break; + + default: + console.error(`Unexpected pref name ${pref}`); + return; + } + + this._notifyObservers(); + this._notifyStripOnShareObservers(); + } + + _getListFromSharedData() { + let data = Services.cpmm.sharedData.get(SHARED_DATA_KEY); + + return data ? [data] : []; + } + + _notifyObservers(observer) { + let stripEntries = new Set([ + ...this.prefStripList, + ...this.remoteStripList, + ]); + let allowEntries = new Set([ + ...this.prefAllowList, + ...this.remoteAllowList, + ]); + let stripEntriesAsString = Array.from(stripEntries).join(" ").toLowerCase(); + let allowEntriesAsString = Array.from(allowEntries).join(",").toLowerCase(); + + let observers = observer ? [observer] : this.observers; + + if (observer || this.observers.size) { + lazy.logger.debug("_notifyObservers", { + observerCount: observers.length, + runObserverAfterRegister: observer != null, + stripEntriesAsString, + allowEntriesAsString, + }); + } + + for (let obs of observers) { + obs.onQueryStrippingListUpdate( + stripEntriesAsString, + allowEntriesAsString + ); + } + } + + async _notifyStripOnShareObservers(observer) { + this.stripOnShareParams = await lazy.StripOnShareList; + + // Changing to different test list allows us to test + // site specific params as the websites that current have + // site specific params cannot be opened in a test env + if (lazy.testStripOnShare) { + this.stripOnShareParams = this.#stripOnShareTestList; + } + + if (!this.stripOnShareParams) { + lazy.logger.error("StripOnShare list is undefined"); + return; + } + + // Add the qps params to the global rules of the strip-on-share list. + let qpsParams = [...this.prefStripList, ...this.remoteStripList].map( + param => param.toLowerCase() + ); + + this.stripOnShareParams.global.queryParams.push(...qpsParams); + // Getting rid of duplicates. + this.stripOnShareParams.global.queryParams = [ + ...new Set(this.stripOnShareParams.global.queryParams), + ]; + + // Build an array of StripOnShareRules. + let rules = Object.values(this.stripOnShareParams); + let stringifiedRules = []; + // We need to stringify the rules so later we can initialise WebIDL dictionaries from them. + // The dictionaries init call needs stringified json. + rules.forEach(rule => { + stringifiedRules.push(JSON.stringify(rule)); + }); + + let observers = observer ? new Set([observer]) : this.stripOnShareObservers; + + if (observers.size) { + lazy.logger.debug("_notifyStripOnShareObservers", { + observerCount: observers.size, + runObserverAfterRegister: observer != null, + stringifiedRules, + }); + } + for (let obs of observers) { + obs.onStripOnShareUpdate(stringifiedRules); + } + } + + async registerAndRunObserver(observer) { + lazy.logger.debug("registerAndRunObserver", { + isInitialized: this.#isInitialized, + pendingInit: this.#pendingInit, + }); + + await this.#init(); + this.observers.add(observer); + this._notifyObservers(observer); + } + + async registerAndRunObserverStripOnShare(observer) { + lazy.logger.debug("registerAndRunObserverStripOnShare", { + isInitialized: this.#isInitialized, + pendingInit: this.#pendingInit, + }); + + await this.#init(); + this.stripOnShareObservers.add(observer); + await this._notifyStripOnShareObservers(observer); + } + + async unregisterObserver(observer) { + this.observers.delete(observer); + + if (this.hasObservers) { + lazy.logger.debug("Last observer unregistered, shutting down..."); + await this.#shutdown(); + } + } + + async unregisterStripOnShareObserver(observer) { + this.stripOnShareObservers.delete(observer); + + if (this.hasObservers) { + lazy.logger.debug("Last observer unregistered, shutting down..."); + await this.#shutdown(); + } + } + + async clearLists() { + if (!this.isParentProcess) { + return; + } + + // Ensure init. + await this.#init(); + + // Clear the lists of remote settings. + this._onRemoteSettingsUpdate([]); + + // Clear the user pref for the strip list. The pref change observer will + // handle the rest of the work. + Services.prefs.clearUserPref(PREF_STRIP_LIST_NAME); + Services.prefs.clearUserPref(PREF_ALLOW_LIST_NAME); + } + + observe(subject, topic, data) { + lazy.logger.debug("observe", { topic, data }); + switch (topic) { + case "xpcom-shutdown": + this.#shutdown(); + break; + case "nsPref:changed": + let prefValue = Services.prefs.getStringPref(data, ""); + this._onPrefUpdate(data, prefValue); + break; + default: + console.error(`Unexpected event ${topic}`); + } + } + + handleEvent(event) { + if (event.type != "change") { + return; + } + + if (!event.changedKeys.includes(SHARED_DATA_KEY)) { + return; + } + + let data = this._getListFromSharedData(); + this._onRemoteSettingsUpdate(data); + this._notifyObservers(); + } + + async testWaitForInit() { + if (this.#pendingInit) { + await this.#pendingInit; + } + + return this.#isInitialized; + } +} diff --git a/toolkit/components/antitracking/antitracking.manifest b/toolkit/components/antitracking/antitracking.manifest new file mode 100644 index 0000000000..5eb37f9a3f --- /dev/null +++ b/toolkit/components/antitracking/antitracking.manifest @@ -0,0 +1 @@ +category profile-after-change URLDecorationAnnotationsService @mozilla.org/tracking-url-decoration-service;1 process=main diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp new file mode 100644 index 0000000000..e5d9ccfea9 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp @@ -0,0 +1,596 @@ +/* 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 "BounceTrackingProtection.h" + +#include "BounceTrackingProtectionStorage.h" +#include "BounceTrackingState.h" +#include "BounceTrackingRecord.h" + +#include "BounceTrackingStateGlobal.h" +#include "ErrorList.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Logging.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/dom/Promise.h" +#include "nsDebug.h" +#include "nsHashPropertyBag.h" +#include "nsIClearDataService.h" +#include "nsIObserverService.h" +#include "nsIPrincipal.h" +#include "nsISupports.h" +#include "nsServiceManagerUtils.h" +#include "nscore.h" +#include "prtime.h" +#include "mozilla/dom/BrowsingContext.h" +#include "xpcpublic.h" + +#define TEST_OBSERVER_MSG_RECORD_BOUNCES_FINISHED "test-record-bounces-finished" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(BounceTrackingProtection, nsIBounceTrackingProtection); + +LazyLogModule gBounceTrackingProtectionLog("BounceTrackingProtection"); + +static StaticRefPtr<BounceTrackingProtection> sBounceTrackingProtection; + +static constexpr uint32_t TRACKER_PURGE_FLAGS = + nsIClearDataService::CLEAR_ALL_CACHES | nsIClearDataService::CLEAR_COOKIES | + nsIClearDataService::CLEAR_DOM_STORAGES | + nsIClearDataService::CLEAR_CLIENT_AUTH_REMEMBER_SERVICE | + nsIClearDataService::CLEAR_EME | nsIClearDataService::CLEAR_MEDIA_DEVICES | + nsIClearDataService::CLEAR_STORAGE_ACCESS | + nsIClearDataService::CLEAR_AUTH_TOKENS | + nsIClearDataService::CLEAR_AUTH_CACHE; + +// static +already_AddRefed<BounceTrackingProtection> +BounceTrackingProtection::GetSingleton() { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()) { + return nullptr; + } + + if (!sBounceTrackingProtection) { + sBounceTrackingProtection = new BounceTrackingProtection(); + + RunOnShutdown([] { sBounceTrackingProtection = nullptr; }); + } + + return do_AddRef(sBounceTrackingProtection); +} + +BounceTrackingProtection::BounceTrackingProtection() { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("constructor")); + + mStorage = new BounceTrackingProtectionStorage(); + + nsresult rv = mStorage->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Error, + ("storage init failed")); + return; + } + + // Schedule timer for tracker purging. The timer interval is determined by + // pref. + uint32_t purgeTimerPeriod = StaticPrefs:: + privacy_bounceTrackingProtection_bounceTrackingPurgeTimerPeriodSec(); + + // The pref can be set to 0 to disable interval purging. + if (purgeTimerPeriod == 0) { + return; + } + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("Scheduling mBounceTrackingPurgeTimer. Interval: %d seconds.", + purgeTimerPeriod)); + + rv = NS_NewTimerWithCallback( + getter_AddRefs(mBounceTrackingPurgeTimer), + [](auto) { + if (!sBounceTrackingProtection) { + return; + } + sBounceTrackingProtection->PurgeBounceTrackers()->Then( + GetMainThreadSerialEventTarget(), __func__, + [] { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: PurgeBounceTrackers finished after timer call.", + __FUNCTION__)); + }, + [] { NS_WARNING("RunPurgeBounceTrackers failed"); }); + }, + purgeTimerPeriod * PR_MSEC_PER_SEC, nsITimer::TYPE_REPEATING_SLACK, + "mBounceTrackingPurgeTimer"); + + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Failed to schedule timer for RunPurgeBounceTrackers."); +} + +nsresult BounceTrackingProtection::RecordStatefulBounces( + BounceTrackingState* aBounceTrackingState) { + NS_ENSURE_ARG_POINTER(aBounceTrackingState); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: aBounceTrackingState: %s", __FUNCTION__, + aBounceTrackingState->Describe().get())); + + // Assert: navigable’s bounce tracking record is not null. + BounceTrackingRecord* record = + aBounceTrackingState->GetBounceTrackingRecord(); + NS_ENSURE_TRUE(record, NS_ERROR_FAILURE); + + // Get the bounce tracker map and the user activation map. + RefPtr<BounceTrackingStateGlobal> globalState = + mStorage->GetOrCreateStateGlobal(aBounceTrackingState); + MOZ_ASSERT(globalState); + + // For each host in navigable’s bounce tracking record's bounce set: + for (const nsACString& host : record->GetBounceHosts()) { + // If host equals navigable’s bounce tracking record's initial host, + // continue. + if (host == record->GetInitialHost()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host == initialHost: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + // If host equals navigable’s bounce tracking record's final host, continue. + if (host == record->GetFinalHost()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host == finalHost: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + + // If user activation map contains host, continue. + if (globalState->HasUserActivation(host)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host with recent user activation: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + + // If stateful bounce tracking map contains host, continue. + if (globalState->HasBounceTracker(host)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip already existing host: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + + // If navigable’s bounce tracking record's storage access set does not + // contain host, continue. + if (StaticPrefs:: + privacy_bounceTrackingProtection_requireStatefulBounces() && + !record->GetStorageAccessHosts().Contains(host)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host without storage access: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + + // Set stateful bounce tracking map[host] to topDocument’s relevant settings + // object's current wall time. + PRTime now = PR_Now(); + MOZ_ASSERT(!globalState->HasBounceTracker(host)); + nsresult rv = globalState->RecordBounceTracker(host, now); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + ("%s: Added candidate to mBounceTrackers: %s, Time: %" PRIu64, + __FUNCTION__, PromiseFlatCString(host).get(), + static_cast<uint64_t>(now))); + } + + // Set navigable’s bounce tracking record to null. + aBounceTrackingState->ResetBounceTrackingRecord(); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Done, reset aBounceTrackingState: %s", __FUNCTION__, + aBounceTrackingState->Describe().get())); + + // If running in test automation, dispatch an observer message indicating + // we're finished recording bounces. + if (StaticPrefs::privacy_bounceTrackingProtection_enableTestMode()) { + nsCOMPtr<nsIObserverService> obsSvc = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE); + + RefPtr<nsHashPropertyBag> props = new nsHashPropertyBag(); + + nsresult rv = props->SetPropertyAsUint64( + u"browserId"_ns, aBounceTrackingState->GetBrowserId()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obsSvc->NotifyObservers( + ToSupports(props), TEST_OBSERVER_MSG_RECORD_BOUNCES_FINISHED, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult BounceTrackingProtection::RecordUserActivation( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + + NS_ENSURE_ARG_POINTER(aPrincipal); + NS_ENSURE_TRUE(aPrincipal->GetIsContentPrincipal(), NS_ERROR_FAILURE); + + nsAutoCString siteHost; + nsresult rv = aPrincipal->GetBaseDomain(siteHost); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + ("%s: siteHost: %s", __FUNCTION__, siteHost.get())); + + RefPtr<BounceTrackingStateGlobal> globalState = + mStorage->GetOrCreateStateGlobal(aPrincipal); + MOZ_ASSERT(globalState); + + return globalState->RecordUserActivation(siteHost, PR_Now()); +} + +NS_IMETHODIMP +BounceTrackingProtection::TestGetBounceTrackerCandidateHosts( + JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx, + nsTArray<nsCString>& aCandidates) { + MOZ_ASSERT(aCx); + + OriginAttributes oa; + if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + BounceTrackingStateGlobal* globalState = mStorage->GetOrCreateStateGlobal(oa); + MOZ_ASSERT(globalState); + + for (const nsACString& host : globalState->BounceTrackersMapRef().Keys()) { + aCandidates.AppendElement(host); + } + + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingProtection::TestGetUserActivationHosts( + JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx, + nsTArray<nsCString>& aHosts) { + MOZ_ASSERT(aCx); + + OriginAttributes oa; + if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + BounceTrackingStateGlobal* globalState = mStorage->GetOrCreateStateGlobal(oa); + MOZ_ASSERT(globalState); + + for (const nsACString& host : globalState->UserActivationMapRef().Keys()) { + aHosts.AppendElement(host); + } + + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingProtection::ClearAll() { + BounceTrackingState::ResetAll(); + return mStorage->Clear(); +} + +NS_IMETHODIMP +BounceTrackingProtection::ClearBySiteHostAndOA( + const nsACString& aSiteHost, JS::Handle<JS::Value> aOriginAttributes, + JSContext* aCx) { + NS_ENSURE_ARG_POINTER(aCx); + + OriginAttributes originAttributes; + if (!aOriginAttributes.isObject() || + !originAttributes.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + // Reset per tab state for tabs matching the given OriginAttributes. + BounceTrackingState::ResetAllForOriginAttributes(originAttributes); + + return mStorage->ClearBySiteHost(aSiteHost, &originAttributes); +} + +NS_IMETHODIMP +BounceTrackingProtection::ClearBySiteHost(const nsACString& aSiteHost) { + BounceTrackingState::ResetAll(); + + return mStorage->ClearBySiteHost(aSiteHost, nullptr); +} + +NS_IMETHODIMP +BounceTrackingProtection::ClearByTimeRange(PRTime aFrom, PRTime aTo) { + NS_ENSURE_TRUE(aFrom >= 0, NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(aFrom < aTo, NS_ERROR_INVALID_ARG); + + // Clear all BounceTrackingState, we don't keep track of time ranges. + BounceTrackingState::ResetAll(); + + return mStorage->ClearByTimeRange(aFrom, aTo); +} + +NS_IMETHODIMP +BounceTrackingProtection::ClearByOriginAttributesPattern( + const nsAString& aPattern) { + OriginAttributesPattern pattern; + if (!pattern.Init(aPattern)) { + return NS_ERROR_INVALID_ARG; + } + + // Reset all per-tab state matching the given OriginAttributesPattern. + BounceTrackingState::ResetAllForOriginAttributesPattern(pattern); + + return mStorage->ClearByOriginAttributesPattern(pattern); +} + +NS_IMETHODIMP +BounceTrackingProtection::TestRunPurgeBounceTrackers( + JSContext* aCx, mozilla::dom::Promise** aPromise) { + NS_ENSURE_ARG_POINTER(aCx); + NS_ENSURE_ARG_POINTER(aPromise); + + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (!globalObject) { + return NS_ERROR_UNEXPECTED; + } + + ErrorResult result; + RefPtr<dom::Promise> promise = dom::Promise::Create(globalObject, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + // PurgeBounceTrackers returns a MozPromise, wrap it in a dom::Promise + // required for XPCOM. + PurgeBounceTrackers()->Then( + GetMainThreadSerialEventTarget(), __func__, + [promise](const PurgeBounceTrackersMozPromise::ResolveValueType& + purgedSiteHosts) { + promise->MaybeResolve(purgedSiteHosts); + }, + [promise] { promise->MaybeRejectWithUndefined(); }); + + promise.forget(aPromise); + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingProtection::TestAddBounceTrackerCandidate( + JS::Handle<JS::Value> aOriginAttributes, const nsACString& aHost, + const PRTime aBounceTime, JSContext* aCx) { + MOZ_ASSERT(aCx); + + OriginAttributes oa; + if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + BounceTrackingStateGlobal* stateGlobal = mStorage->GetOrCreateStateGlobal(oa); + MOZ_ASSERT(stateGlobal); + + // Ensure aHost is lowercase to match nsIURI and nsIPrincipal. + nsAutoCString host(aHost); + ToLowerCase(host); + + // Can not have a host in both maps. + nsresult rv = stateGlobal->TestRemoveUserActivation(host); + NS_ENSURE_SUCCESS(rv, rv); + return stateGlobal->RecordBounceTracker(host, aBounceTime); +} + +NS_IMETHODIMP +BounceTrackingProtection::TestAddUserActivation( + JS::Handle<JS::Value> aOriginAttributes, const nsACString& aHost, + const PRTime aActivationTime, JSContext* aCx) { + MOZ_ASSERT(aCx); + + OriginAttributes oa; + if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + BounceTrackingStateGlobal* stateGlobal = mStorage->GetOrCreateStateGlobal(oa); + MOZ_ASSERT(stateGlobal); + + // Ensure aHost is lowercase to match nsIURI and nsIPrincipal. + nsAutoCString host(aHost); + ToLowerCase(host); + + return stateGlobal->RecordUserActivation(host, aActivationTime); +} + +RefPtr<BounceTrackingProtection::PurgeBounceTrackersMozPromise> +BounceTrackingProtection::PurgeBounceTrackers() { + // Run the purging algorithm for all global state objects. + for (const auto& entry : mStorage->StateGlobalMapRef()) { + const OriginAttributes& originAttributes = entry.GetKey(); + BounceTrackingStateGlobal* stateGlobal = entry.GetData(); + MOZ_ASSERT(stateGlobal); + + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString oaSuffix; + originAttributes.CreateSuffix(oaSuffix); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Running purge algorithm for OA: '%s'", __FUNCTION__, + oaSuffix.get())); + } + + PurgeBounceTrackersForStateGlobal(stateGlobal, originAttributes); + } + + // Wait for all data clearing operations to complete. mClearPromises contains + // one promise per host / clear task. + return ClearDataMozPromise::AllSettled(GetCurrentSerialEventTarget(), + mClearPromises) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [&](ClearDataMozPromise::AllSettledPromiseType::ResolveOrRejectValue&& + aResults) { + MOZ_ASSERT(aResults.IsResolve(), "AllSettled never rejects"); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + ("%s: Done. Cleared %zu hosts.", __FUNCTION__, + aResults.ResolveValue().Length())); + + nsTArray<nsCString> purgedSiteHosts; + // If any clear call failed reject. + for (auto& result : aResults.ResolveValue()) { + if (result.IsReject()) { + mClearPromises.Clear(); + return PurgeBounceTrackersMozPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + purgedSiteHosts.AppendElement(result.ResolveValue()); + } + + // No clearing errors, resolve. + mClearPromises.Clear(); + return PurgeBounceTrackersMozPromise::CreateAndResolve( + std::move(purgedSiteHosts), __func__); + }); +} + +nsresult BounceTrackingProtection::PurgeBounceTrackersForStateGlobal( + BounceTrackingStateGlobal* aStateGlobal, + const OriginAttributes& aOriginAttributes) { + MOZ_ASSERT(aStateGlobal); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: #mUserActivation: %d, #mBounceTrackers: %d", __FUNCTION__, + aStateGlobal->UserActivationMapRef().Count(), + aStateGlobal->BounceTrackersMapRef().Count())); + + // Purge already in progress. + if (!mClearPromises.IsEmpty()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip: Purge already in progress.", __FUNCTION__)); + return NS_ERROR_NOT_AVAILABLE; + } + + const PRTime now = PR_Now(); + // Convert the user activation lifetime into microseconds for calculation with + // PRTime values. The pref is a 32-bit value. Cast into 64-bit before + // multiplying so we get the correct result. + int64_t activationLifetimeUsec = + static_cast<int64_t>( + StaticPrefs:: + privacy_bounceTrackingProtection_bounceTrackingActivationLifetimeSec()) * + PR_USEC_PER_SEC; + + // 1. Remove hosts from the user activation map whose user activation flag has + // expired. + nsresult rv = + aStateGlobal->ClearUserActivationBefore(now - activationLifetimeUsec); + NS_ENSURE_SUCCESS(rv, rv); + + // 2. Go over bounce tracker candidate map and purge state. + rv = NS_OK; + nsCOMPtr<nsIClearDataService> clearDataService = + do_GetService("@mozilla.org/clear-data-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + mClearPromises.Clear(); + nsTArray<nsCString> purgedSiteHosts; + + // Collect hosts to remove from the bounce trackers map. We can not remove + // them while iterating over the map. + nsTArray<nsCString> bounceTrackerCandidatesToRemove; + + for (auto hostIter = aStateGlobal->BounceTrackersMapRef().ConstIter(); + !hostIter.Done(); hostIter.Next()) { + const nsACString& host = hostIter.Key(); + const PRTime& bounceTime = hostIter.Data(); + + // If bounceTime + bounce tracking grace period is after now, then continue. + // The host is still within the grace period and must not be purged. + if (bounceTime + + StaticPrefs:: + privacy_bounceTrackingProtection_bounceTrackingGracePeriodSec() * + PR_USEC_PER_SEC > + now) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host within bounce tracking grace period %s", + __FUNCTION__, PromiseFlatCString(host).get())); + + continue; + } + + // If there is a top-level traversable whose active document's origin's + // site's host equals host, then continue. + // TODO: Bug 1842047: Implement a more accurate check that calls into the + // browser implementations to determine whether the site is currently open + // on the top level. + bool hostIsActive; + rv = BounceTrackingState::HasBounceTrackingStateForSite(host, hostIsActive); + if (NS_WARN_IF(NS_FAILED(rv))) { + hostIsActive = false; + } + if (hostIsActive) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host which is active %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + + // No exception above applies, clear state for the given host. + + RefPtr<ClearDataMozPromise::Private> clearPromise = + new ClearDataMozPromise::Private(__func__); + RefPtr<ClearDataCallback> cb = new ClearDataCallback(clearPromise, host); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Purge state for host: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + + // TODO: Bug 1842067: Clear by site + OA. + rv = clearDataService->DeleteDataFromBaseDomain(host, false, + TRACKER_PURGE_FLAGS, cb); + if (NS_WARN_IF(NS_FAILED(rv))) { + clearPromise->Reject(0, __func__); + } + + mClearPromises.AppendElement(clearPromise); + + // Remove it from the bounce trackers map, it's about to be purged. If the + // clear call fails still remove it. We want to avoid an ever growing list + // of hosts in case of repeated failures. + bounceTrackerCandidatesToRemove.AppendElement(host); + } + + // Remove hosts from the bounce trackers map which we executed purge calls + // for. + return aStateGlobal->RemoveBounceTrackers(bounceTrackerCandidatesToRemove); +} + +// ClearDataCallback + +NS_IMPL_ISUPPORTS(BounceTrackingProtection::ClearDataCallback, + nsIClearDataCallback); + +// nsIClearDataCallback implementation +NS_IMETHODIMP BounceTrackingProtection::ClearDataCallback::OnDataDeleted( + uint32_t aFailedFlags) { + if (aFailedFlags) { + mPromise->Reject(aFailedFlags, __func__); + } else { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + ("%s: Cleared %s", __FUNCTION__, mHost.get())); + mPromise->Resolve(std::move(mHost), __func__); + } + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h new file mode 100644 index 0000000000..98c61504c0 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h @@ -0,0 +1,84 @@ +/* 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/. */ +#ifndef mozilla_BounceTrackingProtection_h__ +#define mozilla_BounceTrackingProtection_h__ + +#include "mozilla/Logging.h" +#include "mozilla/MozPromise.h" +#include "nsIBounceTrackingProtection.h" +#include "nsIClearDataService.h" + +class nsIPrincipal; +class nsITimer; + +namespace mozilla { + +class BounceTrackingState; +class BounceTrackingStateGlobal; +class BounceTrackingProtectionStorage; +class OriginAttributes; + +extern LazyLogModule gBounceTrackingProtectionLog; + +class BounceTrackingProtection final : public nsIBounceTrackingProtection { + NS_DECL_ISUPPORTS + NS_DECL_NSIBOUNCETRACKINGPROTECTION + + public: + static already_AddRefed<BounceTrackingProtection> GetSingleton(); + + // This algorithm is called when detecting the end of an extended navigation. + // This could happen if a user-initiated navigation is detected in process + // navigation start for bounce tracking, or if the client bounce detection + // timer expires after process response received for bounce tracking without + // observing a client redirect. + nsresult RecordStatefulBounces(BounceTrackingState* aBounceTrackingState); + + // Stores a user activation flag with a timestamp for the given principal. + nsresult RecordUserActivation(nsIPrincipal* aPrincipal); + + private: + BounceTrackingProtection(); + ~BounceTrackingProtection() = default; + + // Timer which periodically runs PurgeBounceTrackers. + nsCOMPtr<nsITimer> mBounceTrackingPurgeTimer; + + // Storage for user agent globals. + RefPtr<BounceTrackingProtectionStorage> mStorage; + + // Clear state for classified bounce trackers. To be called on an interval. + using PurgeBounceTrackersMozPromise = + MozPromise<nsTArray<nsCString>, nsresult, true>; + RefPtr<PurgeBounceTrackersMozPromise> PurgeBounceTrackers(); + + nsresult PurgeBounceTrackersForStateGlobal( + BounceTrackingStateGlobal* aStateGlobal, + const OriginAttributes& aOriginAttributes); + + // Pending clear operations are stored as ClearDataMozPromise, one per host. + using ClearDataMozPromise = MozPromise<nsCString, uint32_t, true>; + nsTArray<RefPtr<ClearDataMozPromise>> mClearPromises; + + // Wraps nsIClearDataCallback in MozPromise. + class ClearDataCallback final : public nsIClearDataCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICLEARDATACALLBACK + + explicit ClearDataCallback(ClearDataMozPromise::Private* aPromise, + const nsACString& aHost) + : mHost(aHost), mPromise(aPromise){}; + + private: + virtual ~ClearDataCallback() { mPromise->Reject(0, __func__); } + + nsCString mHost; + RefPtr<ClearDataMozPromise::Private> mPromise; + }; +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp new file mode 100644 index 0000000000..bdd2c7dc18 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp @@ -0,0 +1,841 @@ +/* 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 "BounceTrackingProtectionStorage.h" +#include <cstdint> + +#include "BounceTrackingState.h" +#include "BounceTrackingStateGlobal.h" +#include "ErrorList.h" +#include "MainThreadUtils.h" +#include "mozIStorageConnection.h" +#include "mozIStorageService.h" +#include "mozIStorageStatement.h" +#include "mozStorageCID.h" +#include "mozilla/Components.h" +#include "mozilla/Monitor.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/Services.h" +#include "mozilla/ShutdownPhase.h" +#include "nsCOMPtr.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIObserverService.h" +#include "nsIPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsStringFwd.h" +#include "nsVariant.h" +#include "nscore.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsCRT.h" + +#define BOUNCE_TRACKING_PROTECTION_DB_FILENAME \ + "bounce-tracking-protection.sqlite"_ns +#define SCHEMA_VERSION 1 + +namespace mozilla { + +NS_IMPL_ISUPPORTS(BounceTrackingProtectionStorage, nsIAsyncShutdownBlocker, + nsIObserver); + +BounceTrackingStateGlobal* +BounceTrackingProtectionStorage::GetOrCreateStateGlobal( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aPrincipal); + return GetOrCreateStateGlobal(aPrincipal->OriginAttributesRef()); +} + +BounceTrackingStateGlobal* +BounceTrackingProtectionStorage::GetOrCreateStateGlobal( + BounceTrackingState* aBounceTrackingState) { + MOZ_ASSERT(aBounceTrackingState); + return GetOrCreateStateGlobal(aBounceTrackingState->OriginAttributesRef()); +} + +BounceTrackingStateGlobal* +BounceTrackingProtectionStorage::GetOrCreateStateGlobal( + const OriginAttributes& aOriginAttributes) { + return mStateGlobal.GetOrInsertNew(aOriginAttributes, this, + aOriginAttributes); +} + +nsresult BounceTrackingProtectionStorage::ClearBySiteHost( + const nsACString& aSiteHost, OriginAttributes* aOriginAttributes) { + NS_ENSURE_TRUE(!aSiteHost.IsEmpty(), NS_ERROR_INVALID_ARG); + + // If OriginAttributes are passed only clear the matching state global. + if (aOriginAttributes) { + RefPtr<BounceTrackingStateGlobal> stateGlobal = + mStateGlobal.Get(*aOriginAttributes); + if (stateGlobal) { + nsresult rv = stateGlobal->ClearSiteHost(aSiteHost, true); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // Otherwise we need to clear the host across all state globals. + for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) { + BounceTrackingStateGlobal* stateGlobal = iter.Data(); + MOZ_ASSERT(stateGlobal); + // Update in memory state. Skip storage so we can batch the writes later. + nsresult rv = stateGlobal->ClearSiteHost(aSiteHost, true); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Update the database. + // Private browsing data is not written to disk. + if (aOriginAttributes && + aOriginAttributes->mPrivateBrowsingId != + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) { + return NS_OK; + } + return DeleteDBEntries(aOriginAttributes, aSiteHost); +} + +nsresult BounceTrackingProtectionStorage::ClearByTimeRange(PRTime aFrom, + PRTime aTo) { + for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) { + BounceTrackingStateGlobal* stateGlobal = iter.Data(); + MOZ_ASSERT(stateGlobal); + // Update in memory state. Skip storage so we can batch the writes later. + nsresult rv = + stateGlobal->ClearByTimeRange(aFrom, Some(aTo), Nothing(), true); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Update the database. + return DeleteDBEntriesInTimeRange(nullptr, aFrom, Some(aTo)); +} + +nsresult BounceTrackingProtectionStorage::ClearByOriginAttributesPattern( + const OriginAttributesPattern& aOriginAttributesPattern) { + // Clear in memory state. + for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) { + if (aOriginAttributesPattern.Matches(iter.Key())) { + iter.Remove(); + } + } + + // Update the database. + // Private browsing data is not written to disk. + if (aOriginAttributesPattern.mPrivateBrowsingId.WasPassed() && + aOriginAttributesPattern.mPrivateBrowsingId.Value() != + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) { + return NS_OK; + } + return DeleteDBEntriesByOriginAttributesPattern(aOriginAttributesPattern); +} + +nsresult BounceTrackingProtectionStorage::UpdateDBEntry( + const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost, + EntryType aEntryType, PRTime aTimeStamp) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aSiteHost.IsEmpty()); + MOZ_ASSERT(aTimeStamp); + + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString originAttributeSuffix; + aOriginAttributes.CreateSuffix(originAttributeSuffix); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: originAttributes: %s, siteHost=%s, entryType=%d, " + "timeStamp=%" PRId64, + __FUNCTION__, originAttributeSuffix.get(), + PromiseFlatCString(aSiteHost).get(), + static_cast<uint8_t>(aEntryType), aTimeStamp)); + } + + IncrementPendingWrites(); + + RefPtr<BounceTrackingProtectionStorage> self = this; + nsCString siteHost(aSiteHost); + + mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::UpdateEntry", + [self, aOriginAttributes, siteHost, aEntryType, aTimeStamp]() { + nsresult rv = + UpsertData(self->mDatabaseConnection, aOriginAttributes, + siteHost, aEntryType, aTimeStamp); + self->DecrementPendingWrites(); + NS_ENSURE_SUCCESS_VOID(rv); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::DeleteDBEntries( + OriginAttributes* aOriginAttributes, const nsACString& aSiteHost) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aSiteHost.IsEmpty()); + MOZ_ASSERT(!aOriginAttributes || + aOriginAttributes->mPrivateBrowsingId == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID, + "Must not write private browsing data to the table."); + + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString originAttributeSuffix("*"); + if (aOriginAttributes) { + aOriginAttributes->CreateSuffix(originAttributeSuffix); + } + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: originAttributes: %s, siteHost=%s", __FUNCTION__, + originAttributeSuffix.get(), PromiseFlatCString(aSiteHost).get())); + } + + RefPtr<BounceTrackingProtectionStorage> self = this; + nsCString siteHost(aSiteHost); + Maybe<OriginAttributes> originAttributes; + if (aOriginAttributes) { + originAttributes.emplace(*aOriginAttributes); + } + + IncrementPendingWrites(); + mBackgroundThread->Dispatch( + NS_NewRunnableFunction("BounceTrackingProtectionStorage::DeleteEntry", + [self, originAttributes, siteHost]() { + nsresult rv = + DeleteData(self->mDatabaseConnection, + originAttributes, siteHost); + self->DecrementPendingWrites(); + NS_ENSURE_SUCCESS_VOID(rv); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::Clear() { + MOZ_ASSERT(NS_IsMainThread()); + // Clear in memory data. + mStateGlobal.Clear(); + + // Clear on disk data. + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + IncrementPendingWrites(); + RefPtr<BounceTrackingProtectionStorage> self = this; + mBackgroundThread->Dispatch( + NS_NewRunnableFunction("BounceTrackingProtectionStorage::Clear", + [self]() { + nsresult rv = + ClearData(self->mDatabaseConnection); + self->DecrementPendingWrites(); + NS_ENSURE_SUCCESS_VOID(rv); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::DeleteDBEntriesInTimeRange( + OriginAttributes* aOriginAttributes, PRTime aFrom, Maybe<PRTime> aTo, + Maybe<BounceTrackingProtectionStorage::EntryType> aEntryType) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG_MIN(aFrom, 0); + NS_ENSURE_TRUE(aTo.isNothing() || aTo.value() > aFrom, NS_ERROR_INVALID_ARG); + + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<BounceTrackingProtectionStorage> self = this; + Maybe<OriginAttributes> originAttributes; + if (aOriginAttributes) { + originAttributes.emplace(*aOriginAttributes); + } + + IncrementPendingWrites(); + mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::DeleteDBEntriesInTimeRange", + [self, originAttributes, aFrom, aTo, aEntryType]() { + nsresult rv = + DeleteDataInTimeRange(self->mDatabaseConnection, + originAttributes, aFrom, aTo, aEntryType); + self->DecrementPendingWrites(); + NS_ENSURE_SUCCESS_VOID(rv); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + return NS_OK; +} + +nsresult +BounceTrackingProtectionStorage::DeleteDBEntriesByOriginAttributesPattern( + const OriginAttributesPattern& aOriginAttributesPattern) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aOriginAttributesPattern.mPrivateBrowsingId.WasPassed() || + aOriginAttributesPattern.mPrivateBrowsingId.Value() == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID, + "Must not clear private browsing data from the table."); + + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + IncrementPendingWrites(); + RefPtr<BounceTrackingProtectionStorage> self = this; + mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::" + "DeleteEntriesByOriginAttributesPattern", + [self, aOriginAttributesPattern]() { + nsresult rv = DeleteDataByOriginAttributesPattern( + self->mDatabaseConnection, aOriginAttributesPattern); + self->DecrementPendingWrites(); + NS_ENSURE_SUCCESS_VOID(rv); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + return NS_OK; +} + +// nsIAsyncShutdownBlocker + +NS_IMETHODIMP BounceTrackingProtectionStorage::BlockShutdown( + nsIAsyncShutdownClient* aClient) { + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + MonitorAutoLock lock(mMonitor); + mShuttingDown.Flip(); + + RefPtr<BounceTrackingProtectionStorage> self = this; + mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::BlockShutdown", + [self]() { + MonitorAutoLock lock(self->mMonitor); + + MOZ_ASSERT(self->mPendingWrites == 0); + + if (self->mDatabaseConnection) { + Unused << self->mDatabaseConnection->Close(); + self->mDatabaseConnection = nullptr; + } + + self->mFinalized.Flip(); + self->mMonitor.NotifyAll(); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::BlockShutdown " + "- mainthread callback", + [self]() { self->Finalize(); })); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::WaitForInitialization() { + MOZ_ASSERT(NS_IsMainThread(), + "Must only wait for initialization in the main thread."); + MonitorAutoLock lock(mMonitor); + while (!mInitialized && !mErrored && !mShuttingDown) { + mMonitor.Wait(); + } + if (mErrored) { + return NS_ERROR_FAILURE; + } + if (mShuttingDown) { + return NS_ERROR_NOT_AVAILABLE; + } + return NS_OK; +} + +void BounceTrackingProtectionStorage::Finalize() { + nsCOMPtr<nsIAsyncShutdownClient> asc = GetAsyncShutdownBarrier(); + MOZ_ASSERT(asc); + DebugOnly<nsresult> rv = asc->RemoveBlocker(this); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +// nsIObserver + +NS_IMETHODIMP +BounceTrackingProtectionStorage::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + AssertIsOnMainThread(); + if (nsCRT::strcmp(aTopic, "last-pb-context-exited") != 0) { + return nsresult::NS_ERROR_FAILURE; + } + + uint32_t removedCount = 0; + // Clear in-memory private browsing entries. + for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) { + BounceTrackingStateGlobal* stateGlobal = iter.Data(); + MOZ_ASSERT(stateGlobal); + if (stateGlobal->IsPrivateBrowsing()) { + iter.Remove(); + removedCount++; + } + } + MOZ_LOG( + gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: last-pb-context-exited: Removed %d private browsing state globals", + __FUNCTION__, removedCount)); + + return NS_OK; +} + +// nsIAsyncShutdownBlocker + +already_AddRefed<nsIAsyncShutdownClient> +BounceTrackingProtectionStorage::GetAsyncShutdownBarrier() const { + nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service(); + MOZ_RELEASE_ASSERT(svc); + + nsCOMPtr<nsIAsyncShutdownClient> client; + nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(client)); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + MOZ_RELEASE_ASSERT(client); + + return client.forget(); +} + +NS_IMETHODIMP BounceTrackingProtectionStorage::GetState(nsIPropertyBag**) { + return NS_OK; +} + +NS_IMETHODIMP BounceTrackingProtectionStorage::GetName(nsAString& aName) { + aName.AssignLiteral("BounceTrackingProtectionStorage: Flushing to disk"); + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::Init() { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s", __FUNCTION__)); + + // Init shouldn't be called if the feature is disabled. + NS_ENSURE_TRUE( + StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup(), + NS_ERROR_FAILURE); + + // Register a shutdown blocker so we can flush pending changes to disk before + // shutdown. + // Init may also be called during shutdown, e.g. because of clearing data + // during shutdown. + nsCOMPtr<nsIAsyncShutdownClient> shutdownBarrier = GetAsyncShutdownBarrier(); + NS_ENSURE_TRUE(shutdownBarrier, NS_ERROR_FAILURE); + + bool closed; + nsresult rv = shutdownBarrier->GetIsClosed(&closed); + if (closed || NS_WARN_IF(NS_FAILED(rv))) { + MonitorAutoLock lock(mMonitor); + mShuttingDown.Flip(); + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + rv = shutdownBarrier->AddBlocker( + this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns); + NS_ENSURE_SUCCESS(rv, rv); + + // Listen for last private browsing context exited message so we can clean up + // in memory state when the PBM session ends. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(observerService, NS_ERROR_FAILURE); + rv = observerService->AddObserver(this, "last-pb-context-exited", false); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the database file. + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mDatabaseFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDatabaseFile->AppendNative(BOUNCE_TRACKING_PROTECTION_DB_FILENAME); + NS_ENSURE_SUCCESS(rv, rv); + + // Init the database and import data. + NS_ENSURE_SUCCESS( + NS_CreateBackgroundTaskQueue("BounceTrackingProtectionStorage", + getter_AddRefs(mBackgroundThread)), + NS_ERROR_FAILURE); + + RefPtr<BounceTrackingProtectionStorage> self = this; + + mBackgroundThread->Dispatch( + NS_NewRunnableFunction("BounceTrackingProtectionStorage::Init", + [self]() { + MonitorAutoLock lock(self->mMonitor); + nsresult rv = self->CreateDatabaseConnection(); + if (NS_WARN_IF(NS_FAILED(rv))) { + self->mErrored.Flip(); + self->mMonitor.Notify(); + return; + } + + rv = self->LoadMemoryStateFromDisk(); + if (NS_WARN_IF(NS_FAILED(rv))) { + self->mErrored.Flip(); + self->mMonitor.Notify(); + return; + } + + self->mInitialized.Flip(); + self->mMonitor.Notify(); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::CreateDatabaseConnection() { + MOZ_ASSERT(!NS_IsMainThread()); + NS_ENSURE_TRUE(mDatabaseFile, NS_ERROR_NULL_POINTER); + + nsCOMPtr<mozIStorageService> storage = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(storage, NS_ERROR_UNEXPECTED); + + nsresult rv = storage->OpenDatabase(mDatabaseFile, + mozIStorageService::CONNECTION_DEFAULT, + getter_AddRefs(mDatabaseConnection)); + if (rv == NS_ERROR_FILE_CORRUPTED) { + rv = mDatabaseFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + rv = storage->OpenDatabase(mDatabaseFile, + mozIStorageService::CONNECTION_DEFAULT, + getter_AddRefs(mDatabaseConnection)); + } + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(mDatabaseConnection, NS_ERROR_UNEXPECTED); + bool ready = false; + mDatabaseConnection->GetConnectionReady(&ready); + NS_ENSURE_TRUE(ready, NS_ERROR_UNEXPECTED); + + return EnsureTable(); +} + +nsresult BounceTrackingProtectionStorage::EnsureTable() { + MOZ_ASSERT(!NS_IsMainThread()); + NS_ENSURE_TRUE(mDatabaseConnection, NS_ERROR_UNEXPECTED); + + nsresult rv = mDatabaseConnection->SetSchemaVersion(SCHEMA_VERSION); + NS_ENSURE_SUCCESS(rv, rv); + + const constexpr auto createTableQuery = + "CREATE TABLE IF NOT EXISTS sites (" + "originAttributeSuffix TEXT NOT NULL," + "siteHost TEXT NOT NULL, " + "entryType INTEGER NOT NULL, " + "timeStamp INTEGER NOT NULL, " + "PRIMARY KEY (originAttributeSuffix, siteHost)" + ");"_ns; + + return mDatabaseConnection->ExecuteSimpleSQL(createTableQuery); +} + +nsresult BounceTrackingProtectionStorage::LoadMemoryStateFromDisk() { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not load the table from disk in the main thread."); + + const constexpr auto selectAllQuery( + "SELECT originAttributeSuffix, siteHost, entryType, timeStamp FROM sites;"_ns); + + nsCOMPtr<mozIStorageStatement> readStmt; + nsresult rv = mDatabaseConnection->CreateStatement(selectAllQuery, + getter_AddRefs(readStmt)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasResult; + // Collect DB entries into an array to hand to the main thread later. + nsTArray<ImportEntry> importEntries; + while (NS_SUCCEEDED(readStmt->ExecuteStep(&hasResult)) && hasResult) { + nsAutoCString originAttributeSuffix, siteHost; + int64_t timeStamp; + int32_t typeInt; + + rv = readStmt->GetUTF8String(0, originAttributeSuffix); + NS_ENSURE_SUCCESS(rv, rv); + rv = readStmt->GetUTF8String(1, siteHost); + NS_ENSURE_SUCCESS(rv, rv); + rv = readStmt->GetInt32(2, &typeInt); + NS_ENSURE_SUCCESS(rv, rv); + rv = readStmt->GetInt64(3, &timeStamp); + NS_ENSURE_SUCCESS(rv, rv); + + // Convert entryType field to enum. + BounceTrackingProtectionStorage::EntryType entryType = + static_cast<BounceTrackingProtectionStorage::EntryType>(typeInt); + // Check that the enum value is valid. + if (NS_WARN_IF( + entryType != + BounceTrackingProtectionStorage::EntryType::BounceTracker && + entryType != + BounceTrackingProtectionStorage::EntryType::UserActivation)) { + continue; + } + + OriginAttributes oa; + bool success = oa.PopulateFromSuffix(originAttributeSuffix); + if (NS_WARN_IF(!success)) { + continue; + } + + // Collect entries to dispatch to main thread later. + importEntries.AppendElement( + ImportEntry{oa, siteHost, entryType, timeStamp}); + } + + // We can only access the state map on the main thread. + RefPtr<BounceTrackingProtectionStorage> self = this; + return NS_DispatchToMainThread(NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::LoadMemoryStateFromDisk", + [self, importEntries = std::move(importEntries)]() { + // For each entry get or create BounceTrackingStateGlobal and insert it + // into global state map. + for (const ImportEntry& entry : importEntries) { + RefPtr<BounceTrackingStateGlobal> stateGlobal = + self->GetOrCreateStateGlobal(entry.mOriginAttributes); + MOZ_ASSERT(stateGlobal); + + nsresult rv; + if (entry.mEntryType == + BounceTrackingProtectionStorage::EntryType::BounceTracker) { + rv = stateGlobal->RecordBounceTracker(entry.mSiteHost, + entry.mTimeStamp, true); + } else { + rv = stateGlobal->RecordUserActivation(entry.mSiteHost, + entry.mTimeStamp, true); + } + if (NS_WARN_IF(NS_FAILED(rv)) && + MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString originAttributeSuffix; + entry.mOriginAttributes.CreateSuffix(originAttributeSuffix); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Failed to load entry from disk: " + "originAttributeSuffix=%s, siteHost=%s, entryType=%d, " + "timeStamp=%" PRId64, + __FUNCTION__, originAttributeSuffix.get(), + PromiseFlatCString(entry.mSiteHost).get(), + static_cast<uint8_t>(entry.mEntryType), entry.mTimeStamp)); + } + } + })); +} + +void BounceTrackingProtectionStorage::IncrementPendingWrites() { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mPendingWrites < std::numeric_limits<uint32_t>::max()); + mPendingWrites++; +} + +void BounceTrackingProtectionStorage::DecrementPendingWrites() { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mPendingWrites > 0); + mPendingWrites--; +} + +// static +nsresult BounceTrackingProtectionStorage::UpsertData( + mozIStorageConnection* aDatabaseConnection, + const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost, + BounceTrackingProtectionStorage::EntryType aEntryType, PRTime aTimeStamp) { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not write to the table from the main thread."); + MOZ_ASSERT(aDatabaseConnection); + MOZ_ASSERT(!aSiteHost.IsEmpty()); + MOZ_ASSERT(aTimeStamp > 0); + MOZ_ASSERT(aOriginAttributes.mPrivateBrowsingId == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID, + "Must not write private browsing data to the table."); + + auto constexpr upsertQuery = + "INSERT INTO sites (originAttributeSuffix, siteHost, entryType, " + "timeStamp)" + "VALUES (:originAttributeSuffix, :siteHost, :entryType, :timeStamp)" + "ON CONFLICT (originAttributeSuffix, siteHost)" + "DO UPDATE SET entryType = :entryType, timeStamp = :timeStamp;"_ns; + + nsCOMPtr<mozIStorageStatement> upsertStmt; + nsresult rv = aDatabaseConnection->CreateStatement( + upsertQuery, getter_AddRefs(upsertStmt)); + NS_ENSURE_SUCCESS(rv, rv); + + // Serialize OriginAttributes. + nsAutoCString originAttributeSuffix; + aOriginAttributes.CreateSuffix(originAttributeSuffix); + + rv = upsertStmt->BindUTF8StringByName("originAttributeSuffix"_ns, + originAttributeSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + rv = upsertStmt->BindUTF8StringByName("siteHost"_ns, aSiteHost); + NS_ENSURE_SUCCESS(rv, rv); + + rv = upsertStmt->BindInt32ByName("entryType"_ns, + static_cast<int32_t>(aEntryType)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = upsertStmt->BindInt64ByName("timeStamp"_ns, aTimeStamp); + NS_ENSURE_SUCCESS(rv, rv); + + return upsertStmt->Execute(); +} + +// static +nsresult BounceTrackingProtectionStorage::DeleteData( + mozIStorageConnection* aDatabaseConnection, + Maybe<OriginAttributes> aOriginAttributes, const nsACString& aSiteHost) { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not write to the table from the main thread."); + MOZ_ASSERT(aDatabaseConnection); + MOZ_ASSERT(!aSiteHost.IsEmpty()); + MOZ_ASSERT(aOriginAttributes.isNothing() || + aOriginAttributes->mPrivateBrowsingId == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID); + + nsAutoCString deleteQuery("DELETE FROM sites WHERE siteHost = :siteHost"); + + if (aOriginAttributes) { + deleteQuery.AppendLiteral( + " AND originAttributeSuffix = :originAttributeSuffix"); + } + + nsCOMPtr<mozIStorageStatement> upsertStmt; + nsresult rv = aDatabaseConnection->CreateStatement( + deleteQuery, getter_AddRefs(upsertStmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = upsertStmt->BindUTF8StringByName("siteHost"_ns, aSiteHost); + NS_ENSURE_SUCCESS(rv, rv); + + if (aOriginAttributes) { + nsAutoCString originAttributeSuffix; + aOriginAttributes->CreateSuffix(originAttributeSuffix); + rv = upsertStmt->BindUTF8StringByName("originAttributeSuffix"_ns, + originAttributeSuffix); + NS_ENSURE_SUCCESS(rv, rv); + } + + return upsertStmt->Execute(); +} + +// static +nsresult BounceTrackingProtectionStorage::DeleteDataInTimeRange( + mozIStorageConnection* aDatabaseConnection, + Maybe<OriginAttributes> aOriginAttributes, PRTime aFrom, Maybe<PRTime> aTo, + Maybe<BounceTrackingProtectionStorage::EntryType> aEntryType) { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not write to the table from the main thread."); + MOZ_ASSERT(aDatabaseConnection); + MOZ_ASSERT(aOriginAttributes.isNothing() || + aOriginAttributes->mPrivateBrowsingId == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID); + MOZ_ASSERT(aFrom >= 0); + MOZ_ASSERT(aTo.isNothing() || aTo.value() > aFrom); + + nsAutoCString deleteQuery( + "DELETE FROM sites " + "WHERE timeStamp >= :aFrom"_ns); + + if (aTo.isSome()) { + deleteQuery.AppendLiteral(" AND timeStamp <= :aTo"); + } + + if (aOriginAttributes) { + deleteQuery.AppendLiteral( + " AND originAttributeSuffix = :originAttributeSuffix"); + } + + if (aEntryType.isSome()) { + deleteQuery.AppendLiteral(" AND entryType = :entryType"); + } + deleteQuery.AppendLiteral(";"); + + nsCOMPtr<mozIStorageStatement> deleteStmt; + nsresult rv = aDatabaseConnection->CreateStatement( + deleteQuery, getter_AddRefs(deleteStmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = deleteStmt->BindInt64ByName("aFrom"_ns, aFrom); + NS_ENSURE_SUCCESS(rv, rv); + + if (aTo.isSome()) { + rv = deleteStmt->BindInt64ByName("aTo"_ns, aTo.value()); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aOriginAttributes) { + nsAutoCString originAttributeSuffix; + aOriginAttributes->CreateSuffix(originAttributeSuffix); + rv = deleteStmt->BindUTF8StringByName("originAttributeSuffix"_ns, + originAttributeSuffix); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aEntryType.isSome()) { + rv = deleteStmt->BindInt32ByName("entryType"_ns, + static_cast<int32_t>(*aEntryType)); + NS_ENSURE_SUCCESS(rv, rv); + } + + return deleteStmt->Execute(); +} + +nsresult BounceTrackingProtectionStorage::DeleteDataByOriginAttributesPattern( + mozIStorageConnection* aDatabaseConnection, + const OriginAttributesPattern& aOriginAttributesPattern) { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not write to the table from the main thread."); + MOZ_ASSERT(aDatabaseConnection); + + nsCOMPtr<mozIStorageFunction> patternMatchFunction( + new OriginAttrsPatternMatchOASuffixSQLFunction(aOriginAttributesPattern)); + + nsresult rv = aDatabaseConnection->CreateFunction( + "ORIGIN_ATTRS_PATTERN_MATCH_OA_SUFFIX"_ns, 1, patternMatchFunction); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aDatabaseConnection->ExecuteSimpleSQL( + "DELETE FROM sites WHERE " + "ORIGIN_ATTRS_PATTERN_MATCH_OA_SUFFIX(originAttributeSuffix);"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + return aDatabaseConnection->RemoveFunction( + "ORIGIN_ATTRS_PATTERN_MATCH_OA_SUFFIX"_ns); +} + +// static +nsresult BounceTrackingProtectionStorage::ClearData( + mozIStorageConnection* aDatabaseConnection) { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not write to the table from the main thread."); + NS_ENSURE_ARG_POINTER(aDatabaseConnection); + return aDatabaseConnection->ExecuteSimpleSQL("DELETE FROM sites;"_ns); +} + +NS_IMPL_ISUPPORTS(OriginAttrsPatternMatchOASuffixSQLFunction, + mozIStorageFunction) + +NS_IMETHODIMP +OriginAttrsPatternMatchOASuffixSQLFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + nsresult rv; + + nsAutoCString originAttributeSuffix; + rv = aFunctionArguments->GetUTF8String(0, originAttributeSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + OriginAttributes originAttributes; + bool parsedSuccessfully = + originAttributes.PopulateFromSuffix(originAttributeSuffix); + NS_ENSURE_TRUE(parsedSuccessfully, NS_ERROR_FAILURE); + + bool result = mPattern.Matches(originAttributes); + + RefPtr<nsVariant> outVar(new nsVariant()); + rv = outVar->SetAsBool(result); + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h new file mode 100644 index 0000000000..8d1d4e0417 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h @@ -0,0 +1,222 @@ +/* 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/. */ +#ifndef mozilla_BounceTrackingProtectionStorage_h__ +#define mozilla_BounceTrackingProtectionStorage_h__ + +#include "mozIStorageFunction.h" +#include "mozilla/Logging.h" +#include "mozilla/Monitor.h" +#include "mozilla/ThreadSafety.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/dom/FlippedOnce.h" +#include "nsIAsyncShutdown.h" +#include "nsIFile.h" +#include "nsIObserver.h" +#include "nsISupports.h" +#include "nsTHashMap.h" +#include "mozIStorageConnection.h" +#include "mozilla/OriginAttributesHashKey.h" + +class nsIPrincipal; +class mozIStorageConnection; + +namespace mozilla { + +class BounceTrackingStateGlobal; +class BounceTrackingState; +class OriginAttributes; + +extern LazyLogModule gBounceTrackingProtectionLog; + +class BounceTrackingProtectionStorage final : public nsIObserver, + public nsIAsyncShutdownBlocker, + public SupportsWeakPtr { + friend class BounceTrackingStateGlobal; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIASYNCSHUTDOWNBLOCKER + + public: + BounceTrackingProtectionStorage() + : mMonitor("mozilla::BounceTrackingProtectionStorage::mMonitor"), + mPendingWrites(0){}; + + // Initialises the storage including the on-disk database. + [[nodiscard]] nsresult Init(); + + // Getters for mStateGlobal. + BounceTrackingStateGlobal* GetOrCreateStateGlobal( + const OriginAttributes& aOriginAttributes); + + BounceTrackingStateGlobal* GetOrCreateStateGlobal(nsIPrincipal* aPrincipal); + + BounceTrackingStateGlobal* GetOrCreateStateGlobal( + BounceTrackingState* aBounceTrackingState); + + using StateGlobalMap = + nsTHashMap<OriginAttributesHashKey, RefPtr<BounceTrackingStateGlobal>>; + // Provides a read-only reference to the state global map. + const StateGlobalMap& StateGlobalMapRef() { return mStateGlobal; } + + // The enum values match the database type field. Updating them requires a DB + // migration. + enum class EntryType : uint8_t { BounceTracker = 0, UserActivation = 1 }; + + // Clear all state for a given site host. If aOriginAttributes is passed, only + // entries for that OA will be deleted. + [[nodiscard]] nsresult ClearBySiteHost(const nsACString& aSiteHost, + OriginAttributes* aOriginAttributes); + + // Clear all state within a given time range. + [[nodiscard]] nsresult ClearByTimeRange(PRTime aFrom, PRTime aTo); + + // Clear all state for a given OriginAttributesPattern. + [[nodiscard]] nsresult ClearByOriginAttributesPattern( + const OriginAttributesPattern& aOriginAttributesPattern); + + // Clear all state. + [[nodiscard]] nsresult Clear(); + + private: + ~BounceTrackingProtectionStorage() = default; + + // Worker thread. This should be a valid thread after Init() returns and be + // destroyed when we finalize + nsCOMPtr<nsISerialEventTarget> mBackgroundThread; // main thread only + + // Database connections. Guaranteed to be non-null and working once + // initialized and not-yet finalized + RefPtr<mozIStorageConnection> mDatabaseConnection; // Worker thread only + + // Wait (non-blocking) until the service is fully initialized. We may be + // waiting for that async work started by Init(). + [[nodiscard]] nsresult WaitForInitialization(); + + // Called to indicate to the async shutdown service that we are all wrapped + // up. This also spins down the worker thread, since it is called after all + // disk database connections are closed. + void Finalize(); + + // Utility function to grab the correct barrier this service needs to shut + // down by + already_AddRefed<nsIAsyncShutdownClient> GetAsyncShutdownBarrier() const; + + // Initialises the DB connection on the worker thread. + [[nodiscard]] nsresult CreateDatabaseConnection(); + + // Creates amd initialises the database table if needed. Worker thread only. + [[nodiscard]] nsresult EnsureTable(); + + // Temporary data structure used to import db data into memory. + struct ImportEntry { + OriginAttributes mOriginAttributes; + nsCString mSiteHost; + EntryType mEntryType; + PRTime mTimeStamp; + }; + + // Imports state from the database on disk into memory. + [[nodiscard]] nsresult LoadMemoryStateFromDisk(); + + // Used to (thread-safely) track how many operations have been launched to the + // worker thread so that we can wait for it to hit zero before close the disk + // database connection + void IncrementPendingWrites(); + void DecrementPendingWrites(); + + // Update or create database entry. Worker thread only. + [[nodiscard]] static nsresult UpsertData( + mozIStorageConnection* aDatabaseConnection, + const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost, + EntryType aEntryType, PRTime aTimeStamp); + + // Delete database entries. Worker thread only. + [[nodiscard]] static nsresult DeleteData( + mozIStorageConnection* aDatabaseConnection, + Maybe<OriginAttributes> aOriginAttributes, const nsACString& aSiteHost); + + // Delete all entries before a given time. Worker thread only. + // If aEntryType is passed only entries of that type will be deleted. + [[nodiscard]] static nsresult DeleteDataInTimeRange( + mozIStorageConnection* aDatabaseConnection, + Maybe<OriginAttributes> aOriginAttributes, PRTime aFrom, + Maybe<PRTime> aTo, + Maybe<BounceTrackingProtectionStorage::EntryType> aEntryType = Nothing{}); + + // Delete all entries matching the given OriginAttributesPattern. Worker + // thread only. + [[nodiscard]] static nsresult DeleteDataByOriginAttributesPattern( + mozIStorageConnection* aDatabaseConnection, + const OriginAttributesPattern& aOriginAttributesPattern); + + // Clear all entries from the database. + [[nodiscard]] static nsresult ClearData( + mozIStorageConnection* aDatabaseConnection); + + // Service state management. We protect these variables with a monitor. This + // monitor is also used to signal the completion of initialization and + // finalization performed in the worker thread. + Monitor mMonitor; + + FlippedOnce<false> mInitialized MOZ_GUARDED_BY(mMonitor); + FlippedOnce<false> mErrored MOZ_GUARDED_BY(mMonitor); + FlippedOnce<false> mShuttingDown MOZ_GUARDED_BY(mMonitor); + FlippedOnce<false> mFinalized MOZ_GUARDED_BY(mMonitor); + uint32_t mPendingWrites MOZ_GUARDED_BY(mMonitor); + + // The database file handle. We can only create this in the main thread and + // need it in the worker to perform blocking disk IO. So we put it on this, + // since we pass this to the worker anyway + nsCOMPtr<nsIFile> mDatabaseFile; + + // Map of origin attributes to global state object. This enables us to track + // bounce tracking state per OA, e.g. to separate private browsing from normal + // browsing. + StateGlobalMap mStateGlobal{}; + + // Helpers used to sync updates to BounceTrackingStateGlobal with the + // database. + + // Updates or inserts a DB entry keyed by OA + site host. + [[nodiscard]] nsresult UpdateDBEntry( + const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost, + EntryType aEntryType, PRTime aTimeStamp); + + // Deletes a DB entry keyed by OA + site host. If only aSiteHost is passed, + // all entries for that host will be deleted across OriginAttributes. + [[nodiscard]] nsresult DeleteDBEntries(OriginAttributes* aOriginAttributes, + const nsACString& aSiteHost); + + // Delete all DB entries before a given time. + // If aEntryType is passed only entries of that type will be deleted. + [[nodiscard]] nsresult DeleteDBEntriesInTimeRange( + OriginAttributes* aOriginAttributes, PRTime aFrom, + Maybe<PRTime> aTo = Nothing{}, Maybe<EntryType> aEntryType = Nothing{}); + + // Deletes all DB entries matching the given OriginAttributesPattern. + [[nodiscard]] nsresult DeleteDBEntriesByOriginAttributesPattern( + const OriginAttributesPattern& aOriginAttributesPattern); +}; + +// A SQL function to match DB entries by OriginAttributesPattern. +class OriginAttrsPatternMatchOASuffixSQLFunction final + : public mozIStorageFunction { + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION + + explicit OriginAttrsPatternMatchOASuffixSQLFunction( + OriginAttributesPattern const& aPattern) + : mPattern(aPattern) {} + OriginAttrsPatternMatchOASuffixSQLFunction() = delete; + + private: + ~OriginAttrsPatternMatchOASuffixSQLFunction() = default; + + OriginAttributesPattern mPattern; +}; + +} // namespace mozilla + +#endif // mozilla_BounceTrackingProtectionStorage_h__ diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp new file mode 100644 index 0000000000..14ee178ae2 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp @@ -0,0 +1,76 @@ +/* -*- 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 "BounceTrackingRecord.h" +#include "mozilla/Logging.h" +#include "nsPrintfCString.h" + +namespace mozilla { + +extern LazyLogModule gBounceTrackingProtectionLog; + +NS_IMPL_CYCLE_COLLECTION(BounceTrackingRecord); + +void BounceTrackingRecord::SetInitialHost(const nsACString& aHost) { + mInitialHost = aHost; +} + +const nsACString& BounceTrackingRecord::GetInitialHost() { + return mInitialHost; +} + +void BounceTrackingRecord::SetFinalHost(const nsACString& aHost) { + mFinalHost = aHost; +} + +const nsACString& BounceTrackingRecord::GetFinalHost() { return mFinalHost; } + +void BounceTrackingRecord::AddBounceHost(const nsACString& aHost) { + mBounceHosts.Insert(aHost); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: %s", __FUNCTION__, Describe().get())); +} + +// static +nsCString BounceTrackingRecord::DescribeSet(const nsTHashSet<nsCString>& set) { + nsAutoCString setStr; + + setStr.AppendLiteral("["); + + if (!set.IsEmpty()) { + for (const nsACString& host : set) { + setStr.Append(host); + setStr.AppendLiteral(","); + } + setStr.Truncate(setStr.Length() - 1); + } + + setStr.AppendLiteral("]"); + + return std::move(setStr); +} + +void BounceTrackingRecord::AddStorageAccessHost(const nsACString& aHost) { + mStorageAccessHosts.Insert(aHost); +} + +const nsTHashSet<nsCString>& BounceTrackingRecord::GetBounceHosts() { + return mBounceHosts; +} + +const nsTHashSet<nsCString>& BounceTrackingRecord::GetStorageAccessHosts() { + return mStorageAccessHosts; +} + +nsCString BounceTrackingRecord::Describe() { + return nsPrintfCString( + "{mInitialHost:%s, mFinalHost:%s, mBounceHosts:%s, " + "mStorageAccessHosts:%s}", + mInitialHost.get(), mFinalHost.get(), DescribeSet(mBounceHosts).get(), + DescribeSet(mStorageAccessHosts).get()); +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h new file mode 100644 index 0000000000..d3e980d00b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h @@ -0,0 +1,72 @@ +/* -*- 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/. */ + +#ifndef mozilla_BounceTrackingRecord_h +#define mozilla_BounceTrackingRecord_h + +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTHashSet.h" + +namespace mozilla { + +namespace dom { +class CanonicalBrowsingContext; +} + +// Stores per-tab data relevant to bounce tracking protection for every extended +// navigation. +class BounceTrackingRecord final { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(BounceTrackingRecord); + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(BounceTrackingRecord); + + void SetInitialHost(const nsACString& aHost); + + const nsACString& GetInitialHost(); + + void SetFinalHost(const nsACString& aHost); + + const nsACString& GetFinalHost(); + + void AddBounceHost(const nsACString& aHost); + + void AddStorageAccessHost(const nsACString& aHost); + + const nsTHashSet<nsCString>& GetBounceHosts(); + + const nsTHashSet<nsCString>& GetStorageAccessHosts(); + + // Create a string that describes this record. Used for logging. + nsCString Describe(); + + private: + ~BounceTrackingRecord() = default; + + // A site's host. The initiator site of the current extended navigation. + nsAutoCString mInitialHost; + + // A site's host or null. The destination of the current extended navigation. + // Updated after every document load. + nsAutoCString mFinalHost; + + // A set of sites' hosts. All server-side and client-side redirects hit during + // this extended navigation. + nsTHashSet<nsCString> mBounceHosts; + + // A set of sites' hosts. All sites which accessed storage during this + // extended navigation. + nsTHashSet<nsCString> mStorageAccessHosts; + + // Create a comma-delimited string that describes a string set. Used for + // logging. + static nsCString DescribeSet(const nsTHashSet<nsCString>& set); +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp new file mode 100644 index 0000000000..c5abb8b8d7 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp @@ -0,0 +1,612 @@ +/* -*- 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 "BounceTrackingProtection.h" +#include "BounceTrackingState.h" +#include "BounceTrackingRecord.h" + +#include "BounceTrackingStorageObserver.h" +#include "ErrorList.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/BrowsingContextWebProgress.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIBrowser.h" +#include "nsIChannel.h" +#include "nsIEffectiveTLDService.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIURI.h" +#include "nsIWebProgressListener.h" +#include "nsIPrincipal.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/ClearOnShutdown.h" +#include "nsTHashMap.h" +#include "mozilla/dom/Element.h" + +namespace mozilla { + +// Global map: browserId -> BounceTrackingState +static StaticAutoPtr<nsTHashMap<uint64_t, RefPtr<BounceTrackingState>>> + sBounceTrackingStates; + +static StaticRefPtr<BounceTrackingStorageObserver> sStorageObserver; + +NS_IMPL_ISUPPORTS(BounceTrackingState, nsIWebProgressListener, + nsISupportsWeakReference); + +BounceTrackingState::BounceTrackingState() { + MOZ_ASSERT(StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()); + mBounceTrackingProtection = BounceTrackingProtection::GetSingleton(); +}; + +BounceTrackingState::~BounceTrackingState() { + if (sBounceTrackingStates) { + sBounceTrackingStates->Remove(mBrowserId); + } +} + +// static +already_AddRefed<BounceTrackingState> BounceTrackingState::GetOrCreate( + dom::BrowsingContextWebProgress* aWebProgress) { + MOZ_ASSERT(aWebProgress); + + if (!ShouldCreateBounceTrackingStateForWebProgress(aWebProgress)) { + return nullptr; + } + + // Create BounceTrackingState instance and populate the global + // BounceTrackingState map. + if (!sBounceTrackingStates) { + sBounceTrackingStates = + new nsTHashMap<nsUint64HashKey, RefPtr<BounceTrackingState>>(); + ClearOnShutdown(&sBounceTrackingStates); + } + + if (!sStorageObserver) { + sStorageObserver = new BounceTrackingStorageObserver(); + ClearOnShutdown(&sStorageObserver); + + DebugOnly<nsresult> rv = sStorageObserver->Init(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to init storage observer"); + } + + dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext(); + if (!browsingContext) { + return nullptr; + } + uint64_t browserId = browsingContext->BrowserId(); + bool createdNew; + RefPtr<BounceTrackingState> bounceTrackingState = + do_AddRef(sBounceTrackingStates->LookupOrInsertWith(browserId, [&] { + createdNew = true; + return do_AddRef(new BounceTrackingState()); + })); + + if (createdNew) { + nsresult rv = bounceTrackingState->Init(aWebProgress); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + } + + return bounceTrackingState.forget(); +}; + +// static +void BounceTrackingState::ResetAll() { Reset(nullptr, nullptr); } + +// static +void BounceTrackingState::ResetAllForOriginAttributes( + const OriginAttributes& aOriginAttributes) { + Reset(&aOriginAttributes, nullptr); +} + +// static +void BounceTrackingState::ResetAllForOriginAttributesPattern( + const OriginAttributesPattern& aPattern) { + Reset(nullptr, &aPattern); +} + +nsresult BounceTrackingState::Init( + dom::BrowsingContextWebProgress* aWebProgress) { + NS_ENSURE_ARG_POINTER(aWebProgress); + NS_ENSURE_TRUE( + StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup(), + NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mBounceTrackingProtection, NS_ERROR_FAILURE); + + // Store the browser ID so we can get the associated BC later without having + // to hold a reference to aWebProgress. + dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext(); + NS_ENSURE_TRUE(browsingContext, NS_ERROR_FAILURE); + mBrowserId = browsingContext->BrowserId(); + // Create a copy of the BC's OriginAttributes so we can use it later without + // having to hold a reference to the BC. + mOriginAttributes = browsingContext->OriginAttributesRef(); + MOZ_ASSERT(mOriginAttributes.mPartitionKey.IsEmpty(), + "Top level BCs mus not have a partition key."); + + // Add a listener for window load. See BounceTrackingState::OnStateChange for + // the listener code. + nsresult rv = aWebProgress->AddProgressListener( + this, nsIWebProgress::NOTIFY_STATE_WINDOW); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void BounceTrackingState::ResetBounceTrackingRecord() { + mBounceTrackingRecord = nullptr; +} + +BounceTrackingRecord* BounceTrackingState::GetBounceTrackingRecord() { + return mBounceTrackingRecord; +} + +nsCString BounceTrackingState::Describe() { + nsAutoCString oaSuffix; + OriginAttributesRef().CreateSuffix(oaSuffix); + + return nsPrintfCString( + "{ mBounceTrackingRecord: %s, mOriginAttributes: %s }", + mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() : "null", + oaSuffix.get()); +} + +// static +void BounceTrackingState::Reset(const OriginAttributes* aOriginAttributes, + const OriginAttributesPattern* aPattern) { + if (aOriginAttributes || aPattern) { + MOZ_ASSERT((aOriginAttributes != nullptr) != (aPattern != nullptr), + "Must not pass both aOriginAttributes and aPattern."); + } + + if (!sBounceTrackingStates) { + return; + } + for (const RefPtr<BounceTrackingState>& bounceTrackingState : + sBounceTrackingStates->Values()) { + if ((aOriginAttributes && + *aOriginAttributes != bounceTrackingState->OriginAttributesRef()) || + (aPattern && + !aPattern->Matches(bounceTrackingState->OriginAttributesRef()))) { + continue; + } + if (bounceTrackingState->mClientBounceDetectionTimeout) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: mClientBounceDetectionTimeout->Cancel()", __FUNCTION__)); + bounceTrackingState->mClientBounceDetectionTimeout->Cancel(); + bounceTrackingState->mClientBounceDetectionTimeout = nullptr; + } + bounceTrackingState->ResetBounceTrackingRecord(); + } +} + +// static +bool BounceTrackingState::ShouldCreateBounceTrackingStateForWebProgress( + dom::BrowsingContextWebProgress* aWebProgress) { + NS_ENSURE_TRUE(aWebProgress, false); + + // Feature is globally disabled. + if (!StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()) { + return false; + } + + // Only keep track of top level content browsing contexts. + dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext(); + if (!browsingContext || !browsingContext->IsTopContent()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("%s: Skip non top-content.", __FUNCTION__)); + return false; + } + + return true; +} + +// static +nsresult BounceTrackingState::HasBounceTrackingStateForSite( + const nsACString& aSiteHost, bool& aResult) { + aResult = false; + NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_FAILURE); + + if (!sBounceTrackingStates) { + return NS_OK; + } + + // Iterate over all browsing contexts which have a bounce tracking state. Use + // the content principal base domain field to determine whether a BC has an + // active site that matches aSiteHost. + for (const RefPtr<BounceTrackingState>& state : + sBounceTrackingStates->Values()) { + RefPtr<dom::BrowsingContext> browsingContext = + state->CurrentBrowsingContext(); + + if (!browsingContext || browsingContext->IsDiscarded() || + browsingContext->IsInBFCache()) { + continue; + } + + RefPtr<dom::Element> embedderElement = + browsingContext->GetEmbedderElement(); + if (!embedderElement) { + continue; + } + + nsCOMPtr<nsIBrowser> browser = embedderElement->AsBrowser(); + if (!browser) { + continue; + } + + nsCOMPtr<nsIPrincipal> contentPrincipal; + nsresult rv = + browser->GetContentPrincipal(getter_AddRefs(contentPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsAutoCString baseDomain; + rv = contentPrincipal->GetBaseDomain(baseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + if (aSiteHost.Equals(baseDomain)) { + aResult = true; + return NS_OK; + } + } + + return NS_OK; +} + +already_AddRefed<dom::BrowsingContext> +BounceTrackingState::CurrentBrowsingContext() { + MOZ_ASSERT(mBrowserId != 0); + return dom::BrowsingContext::GetCurrentTopByBrowserId(mBrowserId); +} + +const OriginAttributes& BounceTrackingState::OriginAttributesRef() { + return mOriginAttributes; +} + +nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) { + NS_ENSURE_ARG_POINTER(aChannel); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s", __FUNCTION__)); + + nsCOMPtr<nsILoadInfo> loadInfo; + nsresult rv = aChannel->GetLoadInfo(getter_AddRefs(loadInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + // Collect uri list including any redirects. + nsTArray<nsCString> siteList; + + for (const nsCOMPtr<nsIRedirectHistoryEntry>& redirectHistoryEntry : + loadInfo->RedirectChain()) { + nsCOMPtr<nsIPrincipal> principal; + rv = redirectHistoryEntry->GetPrincipal(getter_AddRefs(principal)); + NS_ENSURE_SUCCESS(rv, rv); + + // Filter out non-content principals. + if (!principal->GetIsContentPrincipal()) { + continue; + } + + nsAutoCString baseDomain; + rv = principal->GetBaseDomain(baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + siteList.AppendElement(baseDomain); + } + + // Add site via the current URI which is the end of the chain. + nsCOMPtr<nsIURI> channelURI; + rv = aChannel->GetURI(getter_AddRefs(channelURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIEffectiveTLDService> tldService = + do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString siteHost; + rv = tldService->GetSchemelessSite(channelURI, siteHost); + + if (NS_FAILED(rv)) { + NS_WARNING("Failed to retrieve site for final channel URI."); + } + + siteList.AppendElement(siteHost); + + return OnResponseReceived(siteList); +} + +// nsIWebProgressListener + +NS_IMETHODIMP +BounceTrackingState::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags, + nsresult aStatus) { + NS_ENSURE_ARG_POINTER(aWebProgress); + NS_ENSURE_ARG_POINTER(aRequest); + + bool isTopLevel = false; + nsresult rv = aWebProgress->GetIsTopLevel(&isTopLevel); + NS_ENSURE_SUCCESS(rv, rv); + + // Filter for top level loads. + if (!isTopLevel) { + return NS_OK; + } + + // Filter for window loads. + if (!(aStateFlags & nsIWebProgressListener::STATE_STOP) || + !(aStateFlags & nsIWebProgressListener::STATE_IS_WINDOW)) { + return NS_OK; + } + + // Get the document principal via the current window global. + dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext(); + NS_ENSURE_TRUE(browsingContext, NS_ERROR_FAILURE); + + dom::WindowGlobalParent* windowGlobalParent = + browsingContext->Canonical()->GetCurrentWindowGlobal(); + NS_ENSURE_TRUE(windowGlobalParent, NS_ERROR_FAILURE); + + return OnDocumentLoaded(windowGlobalParent->DocumentPrincipal()); +} + +NS_IMETHODIMP +BounceTrackingState::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingState::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* aLocation, + uint32_t aFlags) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingState::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingState::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aState) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingState::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +nsresult BounceTrackingState::OnStartNavigation( + nsIPrincipal* aTriggeringPrincipal, + const bool aHasValidUserGestureActivation) { + NS_ENSURE_ARG_POINTER(aTriggeringPrincipal); + + // Logging + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString origin; + nsresult rv = aTriggeringPrincipal->GetOrigin(origin); + if (NS_FAILED(rv)) { + origin = "err"; + } + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: origin: %s, mBounceTrackingRecord: %s", __FUNCTION__, + origin.get(), + mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() + : "null")); + } + + // Remove any queued global tasks to record stateful bounces for bounce + // tracking from the networking task source. + if (mClientBounceDetectionTimeout) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: mClientBounceDetectionTimeout->Cancel()", __FUNCTION__)); + mClientBounceDetectionTimeout->Cancel(); + mClientBounceDetectionTimeout = nullptr; + } + + // Obtain the (schemeless) site to keep track of bounces. + nsAutoCString siteHost; + + // If origin is an opaque origin, set initialHost to empty host. Strictly + // speaking we only need to check IsNullPrincipal, but we're generally only + // interested in content principals. Other principal types are not considered + // to be trackers. + if (!aTriggeringPrincipal->GetIsContentPrincipal()) { + siteHost = ""; + } + + // obtain site + nsresult rv = aTriggeringPrincipal->GetBaseDomain(siteHost); + if (NS_WARN_IF(NS_FAILED(rv))) { + siteHost = ""; + } + + // If navigable’s bounce tracking record is null: Set navigable’s bounce + // tracking record to a new bounce tracking record with initial host set to + // initialHost. + if (!mBounceTrackingRecord) { + mBounceTrackingRecord = new BounceTrackingRecord(); + mBounceTrackingRecord->SetInitialHost(siteHost); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: new BounceTrackingRecord(): %s", __FUNCTION__, + mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() + : "null")); + + return NS_OK; + } + + // If sourceSnapshotParams’s has transient activation is true: The user + // activation ends the extended navigation. Process the bounce candidates. + // Also treat system principal navigation as having user interaction + bool hasUserActivation = aHasValidUserGestureActivation || + aTriggeringPrincipal->IsSystemPrincipal(); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: site: %s, hasUserActivation? %d", __FUNCTION__, siteHost.get(), + hasUserActivation)); + if (hasUserActivation) { + rv = mBounceTrackingProtection->RecordStatefulBounces(this); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(!mBounceTrackingRecord); + mBounceTrackingRecord = new BounceTrackingRecord(); + mBounceTrackingRecord->SetInitialHost(siteHost); + + return NS_OK; + } + + // There is no transient user activation. Add host as a bounce candidate. + mBounceTrackingRecord->AddBounceHost(siteHost); + + return NS_OK; +} + +// Private + +nsresult BounceTrackingState::OnResponseReceived( + const nsTArray<nsCString>& aSiteList) { + NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE); + + // Logging + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString siteListStr; + + for (const nsACString& site : aSiteList) { + siteListStr.Append(site); + siteListStr.AppendLiteral(", "); + } + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: #%zu siteList: %s", __FUNCTION__, siteListStr.Length(), + siteListStr.get())); + } + + // Check if there is still an active timeout. This shouldn't happen since + // OnStartNavigation already cancels it. + if (NS_WARN_IF(mClientBounceDetectionTimeout)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: mClientBounceDetectionTimeout->Cancel()", __FUNCTION__)); + mClientBounceDetectionTimeout->Cancel(); + mClientBounceDetectionTimeout = nullptr; + } + + // Run steps after a timeout: queue a global task on the networking task + // source with global to record stateful bounces for bounce. + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Scheduling mClientBounceDetectionTimeout", __FUNCTION__)); + + // Use a weak reference to this to avoid keeping the object alive if the tab + // is closed during the timeout. + WeakPtr<BounceTrackingState> thisWeak = this; + nsresult rv = NS_NewTimerWithCallback( + getter_AddRefs(mClientBounceDetectionTimeout), + [thisWeak](auto) { + if (!thisWeak) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: !thisWeak", __FUNCTION__)); + return; + } + MOZ_LOG( + gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Calling RecordStatefulBounces after timeout.", __FUNCTION__)); + + BounceTrackingState* bounceTrackingState = thisWeak; + bounceTrackingState->mBounceTrackingProtection->RecordStatefulBounces( + bounceTrackingState); + + bounceTrackingState->mClientBounceDetectionTimeout = nullptr; + }, + StaticPrefs:: + privacy_bounceTrackingProtection_clientBounceDetectionTimerPeriodMS(), + nsITimer::TYPE_ONE_SHOT, "mClientBounceDetectionTimeout"); + NS_ENSURE_SUCCESS(rv, rv); + + // For each URL in URLs: Insert host to the navigable’s bounce tracking + // record's bounce set. + for (const nsACString& site : aSiteList) { + mBounceTrackingRecord->AddBounceHost(site); + } + + return NS_OK; +} + +nsresult BounceTrackingState::OnDocumentLoaded( + nsIPrincipal* aDocumentPrincipal) { + NS_ENSURE_ARG_POINTER(aDocumentPrincipal); + + // Assert: navigable’s bounce tracking record is not null. + NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE); + + // Logging + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString origin; + nsresult rv = aDocumentPrincipal->GetOrigin(origin); + if (NS_FAILED(rv)) { + origin = "err"; + } + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: origin: %s, mBounceTrackingRecord: %s", __FUNCTION__, + origin.get(), + mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() + : "null")); + } + + nsAutoCString siteHost; + if (!aDocumentPrincipal->GetIsContentPrincipal()) { + siteHost = ""; + } else { + nsresult rv = aDocumentPrincipal->GetBaseDomain(siteHost); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Set the navigable’s bounce tracking record's final host to the host of + // finalSite. + mBounceTrackingRecord->SetFinalHost(siteHost); + + return NS_OK; +} + +nsresult BounceTrackingState::OnCookieWrite(const nsACString& aSiteHost) { + NS_ENSURE_TRUE(!aSiteHost.IsEmpty(), NS_ERROR_FAILURE); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("%s: OnCookieWrite: %s.", __FUNCTION__, + PromiseFlatCString(aSiteHost).get())); + + if (!mBounceTrackingRecord) { + return NS_OK; + } + + mBounceTrackingRecord->AddStorageAccessHost(aSiteHost); + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h new file mode 100644 index 0000000000..70deee5abe --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h @@ -0,0 +1,156 @@ +/* -*- 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/. */ + +#ifndef mozilla_BounceTrackingState_h +#define mozilla_BounceTrackingState_h + +#include "mozilla/WeakPtr.h" +#include "mozilla/OriginAttributes.h" +#include "nsIPrincipal.h" +#include "nsIWeakReferenceUtils.h" +#include "nsStringFwd.h" +#include "nsIWebProgressListener.h" +#include "nsWeakReference.h" + +class nsIChannel; +class nsITimer; +class nsIPrincipal; + +namespace mozilla { + +class BounceTrackingProtection; +class BounceTrackingRecord; + +namespace dom { +class CanonicalBrowsingContext; +class BrowsingContext; +class BrowsingContextWebProgress; +} // namespace dom + +/** + * This class manages the bounce tracking state for a given tab. It is attached + * to top-level CanonicalBrowsingContexts. + */ +class BounceTrackingState : public nsIWebProgressListener, + public nsSupportsWeakReference, + public SupportsWeakPtr { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWEBPROGRESSLISTENER + + // Gets or creates an existing BrowsingContextState keyed by browserId. May + // return nullptr if the given web progress / browsing context is not suitable + // (see ShouldCreateBounceTrackingStateForWebProgress). + static already_AddRefed<BounceTrackingState> GetOrCreate( + dom::BrowsingContextWebProgress* aWebProgress); + + // Reset state for all BounceTrackingState instances this includes resetting + // BounceTrackingRecords and cancelling any running timers. + static void ResetAll(); + static void ResetAllForOriginAttributes( + const OriginAttributes& aOriginAttributes); + static void ResetAllForOriginAttributesPattern( + const OriginAttributesPattern& aPattern); + + BounceTrackingRecord* GetBounceTrackingRecord(); + + void ResetBounceTrackingRecord(); + + // Callback for when we received a response from the server and are about to + // create a document for the response. Calls into + // BounceTrackingState::OnResponseReceived. + nsresult OnDocumentStartRequest(nsIChannel* aChannel); + + // At the start of a navigation, either initialize a new bounce tracking + // record, or append a client-side redirect to the current bounce tracking + // record. + // Should only be called for top level content navigations. + nsresult OnStartNavigation(nsIPrincipal* aTriggeringPrincipal, + const bool aHasValidUserGestureActivation); + + // Record sites which have written cookies in the current extended + // navigation. + nsresult OnCookieWrite(const nsACString& aSiteHost); + + // Whether the given BrowsingContext should hold a BounceTrackingState + // instance to monitor bounce tracking navigations. + static bool ShouldCreateBounceTrackingStateForBC( + dom::CanonicalBrowsingContext* aBrowsingContext); + + // Check if there is a BounceTrackingState which current browsing context is + // associated with aSiteHost. + // This is an approximation for checking if a given site is currently loaded + // in the top level context, e.g. in a tab. See Bug 1842047 for adding a more + // accurate check that calls into the browser implementations. + static nsresult HasBounceTrackingStateForSite(const nsACString& aSiteHost, + bool& aResult); + + // Get the currently associated BrowsingContext. Returns nullptr if it has not + // been attached yet. + already_AddRefed<dom::BrowsingContext> CurrentBrowsingContext(); + + uint64_t GetBrowserId() { return mBrowserId; } + + const OriginAttributes& OriginAttributesRef(); + + // Create a string that describes this object. Used for logging. + nsCString Describe(); + + private: + explicit BounceTrackingState(); + virtual ~BounceTrackingState(); + + uint64_t mBrowserId{}; + + // OriginAttributes associated with the browser this state is attached to. + OriginAttributes mOriginAttributes; + + // Reference to the BounceTrackingProtection singleton. + RefPtr<BounceTrackingProtection> mBounceTrackingProtection; + + // Record to keep track of extended navigation data. Reset on extended + // navigation end. + RefPtr<BounceTrackingRecord> mBounceTrackingRecord; + + // Timer to wait to wait for a client redirect after a navigation ends. + RefPtr<nsITimer> mClientBounceDetectionTimeout; + + // Reset state for all BounceTrackingState instances this includes resetting + // BounceTrackingRecords and cancelling any running timers. + // Optionally filter by OriginAttributes or OriginAttributesPattern. + static void Reset(const OriginAttributes* aOriginAttributes, + const OriginAttributesPattern* aPattern); + + // Whether the given web progress should hold a BounceTrackingState + // instance to monitor bounce tracking navigations. + static bool ShouldCreateBounceTrackingStateForWebProgress( + dom::BrowsingContextWebProgress* aWebProgress); + + // Init to be called after creation, attaches nsIWebProgressListener. + nsresult Init(dom::BrowsingContextWebProgress* aWebProgress); + + // When the response is received at the end of a navigation, fill the + // bounce set. + nsresult OnResponseReceived(const nsTArray<nsCString>& aSiteList); + + // When the document is loaded at the end of a navigation, update the + // final host. + nsresult OnDocumentLoaded(nsIPrincipal* aDocumentPrincipal); + + // TODO: Bug 1839918: Detection of stateful bounces. + + // Record sites which have accessed storage in the current extended + // navigation. + nsresult OnStorageAccess(); + + // Record sites which have activated service workers in the current + // extended navigation. + nsresult OnServiceWorkerActivation(); +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp new file mode 100644 index 0000000000..3481753431 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp @@ -0,0 +1,190 @@ +/* -*- 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 "BounceTrackingStateGlobal.h" +#include "BounceTrackingProtectionStorage.h" +#include "ErrorList.h" +#include "mozilla/Assertions.h" +#include "mozilla/Logging.h" +#include "nsIPrincipal.h" + +namespace mozilla { + +NS_IMPL_CYCLE_COLLECTION(BounceTrackingStateGlobal); + +extern LazyLogModule gBounceTrackingProtectionLog; + +BounceTrackingStateGlobal::BounceTrackingStateGlobal( + BounceTrackingProtectionStorage* aStorage, const OriginAttributes& aAttrs) + : mStorage(aStorage), mOriginAttributes(aAttrs) { + MOZ_ASSERT(aStorage); +} + +bool BounceTrackingStateGlobal::HasUserActivation( + const nsACString& aSiteHost) const { + return mUserActivation.Contains(aSiteHost); +} + +nsresult BounceTrackingStateGlobal::RecordUserActivation( + const nsACString& aSiteHost, PRTime aTime, bool aSkipStorage) { + NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(aTime > 0, NS_ERROR_INVALID_ARG); + + // A site must only be in one of the maps at a time. + bool hasRemoved = mBounceTrackers.Remove(aSiteHost); + + if (hasRemoved) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Removed bounce tracking candidate due to user activation: %s", + __FUNCTION__, PromiseFlatCString(aSiteHost).get())); + } + + mUserActivation.InsertOrUpdate(aSiteHost, aTime); + + if (aSkipStorage || !ShouldPersistToDisk()) { + return NS_OK; + } + + // Write the change to storage. + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + return mStorage->UpdateDBEntry( + mOriginAttributes, aSiteHost, + BounceTrackingProtectionStorage::EntryType::UserActivation, aTime); +} + +nsresult BounceTrackingStateGlobal::TestRemoveUserActivation( + const nsACString& aSiteHost) { + bool hasRemoved = mUserActivation.Remove(aSiteHost); + + // Avoid potentially removing a bounce tracking entry if there is no user + // activation entry. + if (!hasRemoved) { + return NS_OK; + } + + if (!ShouldPersistToDisk()) { + return NS_OK; + } + + // Write the change to storage. + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + return mStorage->DeleteDBEntries(&mOriginAttributes, aSiteHost); +} + +nsresult BounceTrackingStateGlobal::ClearUserActivationBefore(PRTime aTime) { + return ClearByTimeRange( + 0, Some(aTime), + Some(BounceTrackingProtectionStorage::EntryType::UserActivation)); +} + +nsresult BounceTrackingStateGlobal::ClearSiteHost(const nsACString& aSiteHost, + bool aSkipStorage) { + NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_INVALID_ARG); + + bool removedUserActivation = mUserActivation.Remove(aSiteHost); + bool removedBounceTracker = mBounceTrackers.Remove(aSiteHost); + if (removedUserActivation || removedBounceTracker) { + MOZ_ASSERT(removedUserActivation != removedBounceTracker, + "A site must only be in one of the maps at a time."); + } + + if (aSkipStorage || !ShouldPersistToDisk()) { + return NS_OK; + } + + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + return mStorage->DeleteDBEntries(&mOriginAttributes, aSiteHost); +} + +nsresult BounceTrackingStateGlobal::ClearByTimeRange( + PRTime aFrom, Maybe<PRTime> aTo, + Maybe<BounceTrackingProtectionStorage::EntryType> aEntryType, + bool aSkipStorage) { + NS_ENSURE_ARG_MIN(aFrom, 0); + NS_ENSURE_TRUE(!aTo || aTo.value() > aFrom, NS_ERROR_INVALID_ARG); + + // Clear in memory user activation data. + if (aEntryType.isNothing() || + aEntryType.value() == + BounceTrackingProtectionStorage::EntryType::UserActivation) { + for (auto iter = mUserActivation.Iter(); !iter.Done(); iter.Next()) { + if (iter.Data() >= aFrom && + (aTo.isNothing() || iter.Data() <= aTo.value())) { + iter.Remove(); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Remove user activation for %s", __FUNCTION__, + PromiseFlatCString(iter.Key()).get())); + } + } + } + + // Clear in memory bounce tracker data. + if (aEntryType.isNothing() || + aEntryType.value() == + BounceTrackingProtectionStorage::EntryType::BounceTracker) { + for (auto iter = mBounceTrackers.Iter(); !iter.Done(); iter.Next()) { + if (iter.Data() >= aFrom && + (aTo.isNothing() || iter.Data() <= aTo.value())) { + iter.Remove(); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Remove bouncer tracker for %s", __FUNCTION__, + PromiseFlatCString(iter.Key()).get())); + } + } + } + + if (aSkipStorage || !ShouldPersistToDisk()) { + return NS_OK; + } + + // Write the change to storage. + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + return mStorage->DeleteDBEntriesInTimeRange(&mOriginAttributes, aFrom, aTo, + aEntryType); +} + +bool BounceTrackingStateGlobal::HasBounceTracker( + const nsACString& aSiteHost) const { + return mBounceTrackers.Contains(aSiteHost); +} + +nsresult BounceTrackingStateGlobal::RecordBounceTracker( + const nsACString& aSiteHost, PRTime aTime, bool aSkipStorage) { + NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(aTime > 0, NS_ERROR_INVALID_ARG); + + // Can not record a bounce tracker if the site has a user activation. + NS_ENSURE_TRUE(!mUserActivation.Contains(aSiteHost), NS_ERROR_FAILURE); + mBounceTrackers.InsertOrUpdate(aSiteHost, aTime); + + if (aSkipStorage || !ShouldPersistToDisk()) { + return NS_OK; + } + + // Write the change to storage. + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + return mStorage->UpdateDBEntry( + mOriginAttributes, aSiteHost, + BounceTrackingProtectionStorage::EntryType::BounceTracker, aTime); +} + +nsresult BounceTrackingStateGlobal::RemoveBounceTrackers( + const nsTArray<nsCString>& aSiteHosts) { + for (const nsCString& siteHost : aSiteHosts) { + mBounceTrackers.Remove(siteHost); + + // TODO: Create a bulk delete query. + if (ShouldPersistToDisk()) { + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + nsresult rv = mStorage->DeleteDBEntries(&mOriginAttributes, siteHost); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h new file mode 100644 index 0000000000..6680ceae6f --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h @@ -0,0 +1,110 @@ +/* -*- 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/. */ + +#ifndef mozilla_BounceTrackingStateGlobal_h +#define mozilla_BounceTrackingStateGlobal_h + +#include "BounceTrackingProtectionStorage.h" +#include "mozilla/WeakPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTHashMap.h" +#include "nsISupports.h" + +namespace mozilla { + +/** + * This class holds the global state maps which are used to keep track of + * potential bounce trackers and user activations. + * @see BounceTrackingState for the per browser / tab state. + * + * Updates to the state maps are persisted to storage. + */ +class BounceTrackingStateGlobal final { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(BounceTrackingStateGlobal); + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(BounceTrackingStateGlobal); + + BounceTrackingStateGlobal(BounceTrackingProtectionStorage* aStorage, + const OriginAttributes& aAttrs); + + bool IsPrivateBrowsing() const { + return mOriginAttributes.mPrivateBrowsingId != + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID; + } + + bool ShouldPersistToDisk() const { return !IsPrivateBrowsing(); } + + bool HasUserActivation(const nsACString& aSiteHost) const; + + // Store a user interaction flag for the given host. This will remove the + // host from the bounce tracker map if it exists. + [[nodiscard]] nsresult RecordUserActivation(const nsACString& aSiteHost, + PRTime aTime, + bool aSkipStorage = false); + + // Test-only method to clear a user activation flag. + [[nodiscard]] nsresult TestRemoveUserActivation(const nsACString& aSiteHost); + + // Clear any user interactions that happened before aTime. + [[nodiscard]] nsresult ClearUserActivationBefore(PRTime aTime); + + bool HasBounceTracker(const nsACString& aSiteHost) const; + + // Store a bounce tracker flag for the given host. A host which received user + // interaction recently can not be recorded as a bounce tracker. + [[nodiscard]] nsresult RecordBounceTracker(const nsACString& aSiteHost, + PRTime aTime, + bool aSkipStorage = false); + + // Remove one or many bounce trackers identified by site host. + [[nodiscard]] nsresult RemoveBounceTrackers( + const nsTArray<nsCString>& aSiteHosts); + + [[nodiscard]] nsresult ClearSiteHost(const nsACString& aSiteHost, + bool aSkipStorage = false); + + [[nodiscard]] nsresult ClearByTimeRange( + PRTime aFrom, Maybe<PRTime> aTo = Nothing(), + Maybe<BounceTrackingProtectionStorage::EntryType> aEntryType = Nothing(), + bool aSkipStorage = false); + + const nsTHashMap<nsCStringHashKey, PRTime>& UserActivationMapRef() { + return mUserActivation; + } + + const nsTHashMap<nsCStringHashKey, PRTime>& BounceTrackersMapRef() { + return mBounceTrackers; + } + + private: + ~BounceTrackingStateGlobal() = default; + + // The storage which manages this state global. Used to persist changes to + // this state global in storage. + // This needs to be a weak pointer to avoid BounceTrackingProtectionStorage + // and BounceTrackingStateGlobal holding strong references to each other + // leading to memory leaks. + WeakPtr<BounceTrackingProtectionStorage> mStorage; + + // Origin attributes this state global is associated with. e.g. if the state + // was associated with a PBM window this would set privateBrowsingId: 1. + OriginAttributes mOriginAttributes; + + // Map of site hosts to moments. The moments represent the most recent wall + // clock time at which the user activated a top-level document on the + // associated site host. + nsTHashMap<nsCStringHashKey, PRTime> mUserActivation{}; + + // Map of site hosts to moments. The moments represent the first wall clock + // time since the last execution of the bounce tracking timer at which a page + // on the given site host performed an action that could indicate stateful + // bounce tracking took place. + nsTHashMap<nsCStringHashKey, PRTime> mBounceTrackers{}; +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp new file mode 100644 index 0000000000..cc9c3ce971 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp @@ -0,0 +1,107 @@ +/* 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 "BounceTrackingStorageObserver.h" + +#include "BounceTrackingState.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "nsCOMPtr.h" +#include "nsICookieNotification.h" +#include "nsIObserverService.h" +#include "mozilla/dom/BrowsingContext.h" +#include "nsICookie.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(BounceTrackingStorageObserver, nsIObserver); + +nsresult BounceTrackingStorageObserver::Init() { + MOZ_ASSERT(XRE_IsParentProcess()); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s", __FUNCTION__)); + + // Add observers to listen for cookie changes. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(observerService, NS_ERROR_FAILURE); + + nsresult rv = observerService->AddObserver(this, "cookie-changed", false); + NS_ENSURE_SUCCESS(rv, rv); + return observerService->AddObserver(this, "private-cookie-changed", false); +} + +// nsIObserver +NS_IMETHODIMP +BounceTrackingStorageObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("Observe topic %s", aTopic)); + + NS_ENSURE_TRUE(aSubject, NS_ERROR_FAILURE); + + nsresult rv = NS_OK; + nsCOMPtr<nsICookieNotification> notification = + do_QueryInterface(aSubject, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsICookieNotification::Action action = notification->GetAction(); + // Filter for cookies added, changed or deleted. We don't care about other + // actions such as clearing the entire cookie store. + if (action != nsICookieNotification::COOKIE_ADDED && + action != nsICookieNotification::COOKIE_CHANGED && + action != nsICookieNotification::COOKIE_DELETED) { + return NS_OK; + } + + // Ensure the notification is associated with a BrowsingContext. It's only set + // for cases where a website updated a cookie. + RefPtr<dom::BrowsingContext> browsingContext; + rv = notification->GetBrowsingContext(getter_AddRefs(browsingContext)); + NS_ENSURE_SUCCESS(rv, rv); + if (!browsingContext) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("Could not get BC for CookieNotification.")); + return NS_OK; + } + + // Check if the cookie is partitioned. Partitioned cookies can not be used for + // bounce tracking. + nsCOMPtr<nsICookie> cookie; + rv = notification->GetCookie(getter_AddRefs(cookie)); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(cookie); + + if (!cookie->OriginAttributesNative().mPartitionKey.IsEmpty()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("Skipping partitioned cookie.")); + return NS_OK; + } + + dom::BrowsingContext* topBC = browsingContext->Top(); + dom::BrowsingContextWebProgress* webProgress = + topBC->Canonical()->GetWebProgress(); + if (!webProgress) { + return NS_OK; + } + + RefPtr<BounceTrackingState> bounceTrackingState = + webProgress->GetBounceTrackingState(); + if (!bounceTrackingState) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("BC does not have BounceTrackingState.")); + return NS_OK; + } + + nsAutoCString baseDomain; + rv = notification->GetBaseDomain(baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + return bounceTrackingState->OnCookieWrite(baseDomain); +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h new file mode 100644 index 0000000000..1e76c85a3c --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h @@ -0,0 +1,28 @@ +/* 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/. */ +#ifndef mozilla_BounceTrackingStorageObserver_h__ +#define mozilla_BounceTrackingStorageObserver_h__ + +#include "mozilla/Logging.h" +#include "nsIObserver.h" + +namespace mozilla { + +extern LazyLogModule gBounceTrackingProtectionLog; + +class BounceTrackingStorageObserver final : public nsIObserver { + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + public: + BounceTrackingStorageObserver() = default; + nsresult Init(); + + private: + ~BounceTrackingStorageObserver() = default; +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/components.conf b/toolkit/components/antitracking/bouncetrackingprotection/components.conf new file mode 100644 index 0000000000..c0b202bb18 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/components.conf @@ -0,0 +1,19 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'name': 'BounceTrackingProtection', + 'cid': '{4866F748-29DA-4C10-8EAA-ED2F7851E6B1}', + 'interfaces': ['nsIBounceTrackingProtection'], + 'contract_ids': ['@mozilla.org/bounce-tracking-protection;1'], + 'type': 'mozilla::BounceTrackingProtection', + 'headers': ['/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h'], + 'singleton': True, + 'constructor': 'mozilla::BounceTrackingProtection::GetSingleton', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, +] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/moz.build b/toolkit/components/antitracking/bouncetrackingprotection/moz.build new file mode 100644 index 0000000000..09107bb782 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/moz.build @@ -0,0 +1,51 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking") + +XPIDL_SOURCES += [ + "nsIBounceTrackingProtection.idl", +] + +XPIDL_MODULE = "toolkit_antitracking" + + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXPORTS.mozilla += [ + "BounceTrackingProtection.h", + "BounceTrackingProtectionStorage.h", + "BounceTrackingRecord.h", + "BounceTrackingState.h", + "BounceTrackingStateGlobal.h", + "BounceTrackingStorageObserver.h", +] + +UNIFIED_SOURCES += [ + "BounceTrackingProtection.cpp", + "BounceTrackingProtectionStorage.cpp", + "BounceTrackingRecord.cpp", + "BounceTrackingState.cpp", + "BounceTrackingStateGlobal.cpp", + "BounceTrackingStorageObserver.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/dom/base", +] + +FINAL_LIBRARY = "xul" + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.toml", +] + +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl new file mode 100644 index 0000000000..9ade9cb0ea --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl @@ -0,0 +1,46 @@ +/* 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 "nsISupports.idl" + +[scriptable, uuid(4866F748-29DA-4C10-8EAA-ED2F7851E6B1)] +interface nsIBounceTrackingProtection : nsISupports { + // Reset the global bounce tracking state, including the maps for tracking + // bounce tracker candidates and user activation. + void clearAll(); + + // Clear bounce tracking state for a specific site host and OriginAttributes pair. + [implicit_jscontext] + void clearBySiteHostAndOA(in ACString aSiteHost, in jsval originAttributes); + + // Clear bounce tracking state for a specific site host for all OriginAttributes. + void clearBySiteHost(in ACString aSiteHost); + + // Clear bounce tracking state for a specific time range. + void clearByTimeRange(in PRTime aFrom, in PRTime aTo); + + // Clear bounce tracking state for the given origin attributes. + void clearByOriginAttributesPattern(in AString aPattern); + + // Trigger the bounce tracking timer algorithm that clears state for + // classified bounce trackers. + [implicit_jscontext] + Promise testRunPurgeBounceTrackers(); + + // Getters and setters for user activation and bounce tracker state. + // These are used for testing purposes only. + // State is keyed by OriginAttributes. + + [implicit_jscontext] + Array<ACString> testGetBounceTrackerCandidateHosts(in jsval originAttributes); + + [implicit_jscontext] + Array<ACString> testGetUserActivationHosts(in jsval originAttributes); + + [implicit_jscontext] + void testAddBounceTrackerCandidate(in jsval originAttributes, in ACString aSiteHost, in PRTime aBounceTime); + + [implicit_jscontext] + void testAddUserActivation(in jsval originAttributes, in ACString aSiteHost, in PRTime aActivationTime); +}; diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml new file mode 100644 index 0000000000..1c44d7804e --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml @@ -0,0 +1,20 @@ +[DEFAULT] +head = "head.js" +prefs = [ + "privacy.bounceTrackingProtection.enabled=true", + "privacy.bounceTrackingProtection.enableTestMode=true", + "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0", +] +support-files = [ + "file_start.html", + "file_bounce.sjs", + "file_bounce.html", +] + +["browser_bouncetracking_oa_isolation.js"] + +["browser_bouncetracking_purge.js"] + +["browser_bouncetracking_simple.js"] + +["browser_bouncetracking_stateful.js"] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js new file mode 100644 index 0000000000..12c2c943dd --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); +}); + +// Tests that bounces in PBM don't affect state in normal browsing. +add_task(async function test_pbm_data_isolated() { + await runTestBounce({ + bounceType: "client", + setState: "cookie-client", + originAttributes: { privateBrowsingId: 1 }, + postBounceCallback: () => { + // After the PBM bounce assert that we haven't recorded any data for normal browsing. + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker candidates for normal browsing." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activations for normal browsing." + ); + }, + }); +}); + +// Tests that bounces in PBM don't affect state in normal browsing. +add_task(async function test_containers_isolated() { + await runTestBounce({ + bounceType: "server", + setState: "cookie-server", + originAttributes: { userContextId: 2 }, + postBounceCallback: () => { + // After the bounce in the container tab assert that we haven't recorded any data for normal browsing. + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker candidates for normal browsing." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activations for normal browsing." + ); + + // Or in another container tab. + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ + userContextId: 1, + }).length, + 0, + "No bounce tracker candidates for container tab 1." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({ + userContextId: 1, + }).length, + 0, + "No user activations for container tab 1." + ); + }, + }); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js new file mode 100644 index 0000000000..a8e98b80f0 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BOUNCE_TRACKING_GRACE_PERIOD_SEC = 30; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", + BOUNCE_TRACKING_GRACE_PERIOD_SEC, + ], + ["privacy.bounceTrackingProtection.requireStatefulBounces", false], + ], + }); +}); + +/** + * The following tests ensure that sites that have open tabs are exempt from purging. + */ + +function initBounceTrackerState() { + bounceTrackingProtection.clearAll(); + + // Bounce time of 1 is out of the grace period which means we should purge. + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.com", 1); + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.net", 1); + + // Should not purge because within grace period. + let timestampWithinGracePeriod = + Date.now() - (BOUNCE_TRACKING_GRACE_PERIOD_SEC * 1000) / 2; + bounceTrackingProtection.testAddBounceTrackerCandidate( + {}, + "example.org", + timestampWithinGracePeriod * 1000 + ); +} + +add_task(async function test_purging_skip_open_foreground_tab() { + initBounceTrackerState(); + + // Foreground tab + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com" + ); + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + ["example.net"], + "Should only purge example.net. example.org is within the grace period, example.com has an open tab." + ); + + info("Close the tab for example.com and test that it gets purged now."); + initBounceTrackerState(); + + BrowserTestUtils.removeTab(tab); + Assert.deepEqual( + (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(), + ["example.net", "example.com"].sort(), + "example.com should have been purged now that it no longer has an open tab." + ); + + bounceTrackingProtection.clearAll(); +}); + +add_task(async function test_purging_skip_open_background_tab() { + initBounceTrackerState(); + + // Background tab + let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + ["example.net"], + "Should only purge example.net. example.org is within the grace period, example.com has an open tab." + ); + + info("Close the tab for example.com and test that it gets purged now."); + initBounceTrackerState(); + + BrowserTestUtils.removeTab(tab); + Assert.deepEqual( + (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(), + ["example.net", "example.com"].sort(), + "example.com should have been purged now that it no longer has an open tab." + ); + + bounceTrackingProtection.clearAll(); +}); + +add_task(async function test_purging_skip_open_tab_extra_window() { + initBounceTrackerState(); + + // Foreground tab in new window. + let win = await BrowserTestUtils.openNewBrowserWindow({}); + await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "https://example.com" + ); + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + ["example.net"], + "Should only purge example.net. example.org is within the grace period, example.com has an open tab." + ); + + info( + "Close the window with the tab for example.com and test that it gets purged now." + ); + initBounceTrackerState(); + + await BrowserTestUtils.closeWindow(win); + Assert.deepEqual( + (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(), + ["example.net", "example.com"].sort(), + "example.com should have been purged now that it no longer has an open tab." + ); + + bounceTrackingProtection.clearAll(); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js new file mode 100644 index 0000000000..dfbd4d0fc0 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", false], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); +}); + +// Tests a stateless bounce via client redirect. +add_task(async function test_client_bounce_simple() { + await runTestBounce({ bounceType: "client" }); +}); + +// Tests a stateless bounce via server redirect. +add_task(async function test_server_bounce_simple() { + await runTestBounce({ bounceType: "server" }); +}); + +// Tests a chained redirect consisting of a server and a client redirect. +add_task(async function test_bounce_chain() { + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker hosts initially." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activation hosts initially." + ); + + await BrowserTestUtils.withNewTab( + getBaseUrl(ORIGIN_A) + "file_start.html", + async browser => { + let promiseRecordBounces = waitForRecordBounces(browser); + + // The final destination after the bounces. + let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"); + + // Construct last hop. + let bounceChainUrlEnd = getBounceURL({ bounceType: "server", targetURL }); + // Construct first hop, nesting last hop. + let bounceChainUrlFull = getBounceURL({ + bounceType: "client", + redirectDelayMS: 100, + bounceOrigin: ORIGIN_TRACKER_B, + targetURL: bounceChainUrlEnd, + }); + + info("bounceChainUrl: " + bounceChainUrlFull.href); + + // Navigate through the bounce chain. + await navigateLinkClick(browser, bounceChainUrlFull); + + // Wait for the final site to be loaded which complete the BounceTrackingRecord. + await BrowserTestUtils.browserLoaded(browser, false, targetURL); + + // Navigate again with user gesture which triggers + // BounceTrackingProtection::RecordStatefulBounces. We could rely on the + // timeout (mClientBounceDetectionTimeout) here but that can cause races + // in debug where the load is quite slow. + await navigateLinkClick( + browser, + new URL(getBaseUrl(ORIGIN_C) + "file_start.html") + ); + + await promiseRecordBounces; + + Assert.deepEqual( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(), + [SITE_TRACKER_B, SITE_TRACKER].sort(), + `Identified all bounce trackers in the redirect chain.` + ); + Assert.deepEqual( + bounceTrackingProtection.testGetUserActivationHosts({}).sort(), + [SITE_A, SITE_B].sort(), + "Should only have user activation for sites where we clicked links." + ); + + bounceTrackingProtection.clearAll(); + } + ); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js new file mode 100644 index 0000000000..e7fb4521a7 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); +}); + +// Cookie tests. + +add_task(async function test_bounce_stateful_cookies_client() { + info("Test client bounce with cookie."); + await runTestBounce({ + bounceType: "client", + setState: "cookie-client", + }); + info("Test client bounce without cookie."); + await runTestBounce({ + bounceType: "client", + setState: null, + expectCandidate: false, + expectPurge: false, + }); +}); + +add_task(async function test_bounce_stateful_cookies_server() { + info("Test server bounce with cookie."); + await runTestBounce({ + bounceType: "server", + setState: "cookie-server", + }); + info("Test server bounce without cookie."); + await runTestBounce({ + bounceType: "server", + setState: null, + expectCandidate: false, + expectPurge: false, + }); +}); + +// Storage tests. + +// TODO: Bug 1848406: Implement stateful bounce detection for localStorage. +add_task(async function test_bounce_stateful_localStorage() { + info("TODO: client bounce with localStorage."); + await runTestBounce({ + bounceType: "client", + setState: "localStorage", + expectCandidate: false, + expectPurge: false, + }); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html new file mode 100644 index 0000000000..2756555fa5 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <title>Bounce!</title> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + </head> + <body> + <p>Nothing to see here...</p> + <script> + // Wrap the entire block so we can run async code. + (async () => { + let url = new URL(location.href); + + let redirectDelay = url.searchParams.get("redirectDelay"); + if(redirectDelay != null) { + redirectDelay = Number.parseInt(redirectDelay); + } else { + redirectDelay = 50; + } + + let setState = url.searchParams.get("setState"); + if (setState) { + let id = Math.random().toString(); + + if (setState == "cookie-client") { + let cookie = document.cookie; + + if (cookie) { + console.info("Received cookie", cookie); + } else { + let newCookie = `id=${id}`; + console.info("Setting new cookie", newCookie); + document.cookie = newCookie; + } + } else if (setState == "localStorage") { + let entry = localStorage.getItem("id"); + + if (entry) { + console.info("Found localStorage entry. id", entry); + } else { + console.info("Setting new localStorage entry. id", id); + localStorage.setItem(id, id); + } + } + } + + let target = url.searchParams.get("target"); + if (target) { + console.info("redirecting to", target); + setTimeout(() => { + location.href = target; + }, redirectDelay); + } + })(); + </script> + </body> +</html> diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs new file mode 100644 index 0000000000..5e948a899b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs @@ -0,0 +1,19 @@ +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + let query = new URLSearchParams(request.queryString); + + let setState = query.get("setState"); + if (setState == "cookie-server") { + response.setHeader("Set-Cookie", "foo=bar"); + } + + let statusCode = 302; + let statusCodeQuery = query.get("statusCode"); + if (statusCodeQuery) { + statusCode = Number.parseInt(statusCodeQuery); + } + + response.setStatusLine("1.1", statusCode, "Found"); + response.setHeader("Location", query.get("target"), false); +} diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html new file mode 100644 index 0000000000..ded691023b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <title>Blank</title> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + </head> + <body> + </body> +</html> diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js new file mode 100644 index 0000000000..f5857b6919 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js @@ -0,0 +1,275 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SITE_A = "example.com"; +const ORIGIN_A = `https://${SITE_A}`; + +const SITE_B = "example.org"; +const ORIGIN_B = `https://${SITE_B}`; + +const SITE_C = "example.net"; +const ORIGIN_C = `https://${SITE_C}`; + +const SITE_TRACKER = "itisatracker.org"; +const ORIGIN_TRACKER = `https://${SITE_TRACKER}`; + +const SITE_TRACKER_B = "trackertest.org"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const ORIGIN_TRACKER_B = `http://${SITE_TRACKER_B}`; + +// Test message used for observing when the record-bounces method in +// BounceTrackingProtection.cpp has finished. +const OBSERVER_MSG_RECORD_BOUNCES_FINISHED = "test-record-bounces-finished"; + +const ROOT_DIR = getRootDirectory(gTestPath); + +XPCOMUtils.defineLazyServiceGetter( + this, + "bounceTrackingProtection", + "@mozilla.org/bounce-tracking-protection;1", + "nsIBounceTrackingProtection" +); + +/** + * Get the base url for the current test directory using the given origin. + * @param {string} origin - Origin to use in URL. + * @returns {string} - Generated URL as a string. + */ +function getBaseUrl(origin) { + return ROOT_DIR.replace("chrome://mochitests/content", origin); +} + +/** + * Constructs a url for an intermediate "bounce" hop which represents a tracker. + * @param {*} options - URL generation options. + * @param {('server'|'client')} options.bounceType - Redirect type to use for + * the bounce. + * @param {string} [options.bounceOrigin] - The origin of the bounce URL. + * @param {string} [options.targetURL] - URL to redirect to after the bounce. + * @param {("cookie"|null)} [options.setState] - What type of state should be set during + * the bounce. No state by default. + * @param {number} [options.statusCode] - HTTP status code to use for server + * side redirect. Only applies to bounceType == "server". + * @param {number} [options.redirectDelayMS] - How long to wait before + * redirecting. Only applies to bounceType == "client". + * @returns {URL} Generated URL which points to an endpoint performing the + * redirect. + */ +function getBounceURL({ + bounceType, + bounceOrigin = ORIGIN_TRACKER, + targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"), + setState = null, + statusCode = 302, + redirectDelayMS = 50, +}) { + if (!["server", "client"].includes(bounceType)) { + throw new Error("Invalid bounceType"); + } + + let bounceFile = + bounceType == "client" ? "file_bounce.html" : "file_bounce.sjs"; + + let bounceUrl = new URL(getBaseUrl(bounceOrigin) + bounceFile); + + let { searchParams } = bounceUrl; + searchParams.set("target", targetURL.href); + if (setState) { + searchParams.set("setState", setState); + } + + if (bounceType == "server") { + searchParams.set("statusCode", statusCode); + } else if (bounceType == "client") { + searchParams.set("redirectDelay", redirectDelayMS); + } + + return bounceUrl; +} + +/** + * Insert an <a href/> element with the given target and perform a synthesized + * click on it. + * @param {MozBrowser} browser - Browser to insert the link in. + * @param {URL} targetURL - Destination for navigation. + * @returns {Promise} Resolves once the click is done. Does not wait for + * navigation or load. + */ +async function navigateLinkClick(browser, targetURL) { + await SpecialPowers.spawn(browser, [targetURL.href], targetURL => { + let link = content.document.createElement("a"); + + link.href = targetURL; + link.textContent = targetURL; + // The link needs display: block, otherwise synthesizeMouseAtCenter doesn't + // hit it. + link.style.display = "block"; + + content.document.body.appendChild(link); + }); + + await BrowserTestUtils.synthesizeMouseAtCenter("a[href]", {}, browser); +} + +/** + * Wait for the record-bounces method to run for the given tab / browser. + * @param {browser} browser - Browser element which represents the tab we want + * to observe. + * @returns {Promise} Promise which resolves once the record-bounces method has + * run for the given browser. + */ +async function waitForRecordBounces(browser) { + return TestUtils.topicObserved( + OBSERVER_MSG_RECORD_BOUNCES_FINISHED, + subject => { + // Ensure the message was dispatched for the browser we're interested in. + let propBag = subject.QueryInterface(Ci.nsIPropertyBag2); + let browserId = propBag.getProperty("browserId"); + return browser.browsingContext.browserId == browserId; + } + ); +} + +/** + * Test helper which loads an initial blank page, then navigates to a url which + * performs a bounce. Checks that the bounce hosts are properly identified as + * trackers. + * @param {object} options - Test Options. + * @param {('server'|'client')} options.bounceType - Whether to perform a client + * or server side redirect. + * @param {('cookie-server'|'cookie-client'|'localStorage')} [options.setState] + * Type of state to set during the redirect. Defaults to non stateful redirect. + * @param {boolean} [options.expectCandidate=true] - Expect the redirecting site to be + * identified as a bounce tracker (candidate). + * @param {boolean} [options.expectPurge=true] - Expect the redirecting site to have + * its storage purged. + * @param {OriginAttributes} [options.originAttributes={}] - Origin attributes + * to use for the test. This determines whether the test is run in normal + * browsing, a private window or a container tab. By default the test is run + * in normal browsing. + * @param {function} [options.postBounceCallback] - Optional function to run after the + * bounce has completed. + */ +async function runTestBounce(options = {}) { + let { + bounceType, + setState = null, + expectCandidate = true, + expectPurge = true, + originAttributes = {}, + postBounceCallback = () => {}, + } = options; + info(`runTestBounce ${JSON.stringify(options)}`); + + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts( + originAttributes + ).length, + 0, + "No bounce tracker hosts initially." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts(originAttributes) + .length, + 0, + "No user activation hosts initially." + ); + + let win = window; + let { privateBrowsingId, userContextId } = originAttributes; + let usePrivateWindow = + privateBrowsingId != null && + privateBrowsingId != + Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID; + if (userContextId != null && userContextId > 0 && usePrivateWindow) { + throw new Error("userContextId is not supported in private windows"); + } + + if (usePrivateWindow) { + win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + } + + let tab = win.gBrowser.addTab(getBaseUrl(ORIGIN_A) + "file_start.html", { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + userContextId, + }); + win.gBrowser.selectedTab = tab; + + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + let promiseRecordBounces = waitForRecordBounces(browser); + + // The final destination after the bounce. + let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"); + + // Navigate through the bounce chain. + await navigateLinkClick( + browser, + getBounceURL({ bounceType, targetURL, setState }) + ); + + // Wait for the final site to be loaded which complete the BounceTrackingRecord. + await BrowserTestUtils.browserLoaded(browser, false, targetURL); + + // Navigate again with user gesture which triggers + // BounceTrackingProtection::RecordStatefulBounces. We could rely on the + // timeout (mClientBounceDetectionTimeout) here but that can cause races + // in debug where the load is quite slow. + await navigateLinkClick( + browser, + new URL(getBaseUrl(ORIGIN_C) + "file_start.html") + ); + + await promiseRecordBounces; + + Assert.deepEqual( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts( + originAttributes + ), + expectCandidate ? [SITE_TRACKER] : [], + `Should ${ + expectCandidate ? "" : "not " + }have identified ${SITE_TRACKER} as a bounce tracker.` + ); + Assert.deepEqual( + bounceTrackingProtection + .testGetUserActivationHosts(originAttributes) + .sort(), + [SITE_A, SITE_B].sort(), + "Should only have user activation for sites where we clicked links." + ); + + // If the caller specified a function to run after the bounce, run it now. + await postBounceCallback(); + + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + expectPurge ? [SITE_TRACKER] : [], + `Should ${expectPurge ? "" : "not "}purge state for ${SITE_TRACKER}.` + ); + + // Clean up + BrowserTestUtils.removeTab(tab); + if (usePrivateWindow) { + await BrowserTestUtils.closeWindow(win); + + info( + "Closing the last PBM window should trigger a purge of all PBM state." + ); + Assert.ok( + !bounceTrackingProtection.testGetBounceTrackerCandidateHosts( + originAttributes + ).length, + "No bounce tracker hosts after closing private window." + ); + Assert.ok( + !bounceTrackingProtection.testGetUserActivationHosts(originAttributes) + .length, + "No user activation hosts after closing private window." + ); + } + bounceTrackingProtection.clearAll(); +} diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml new file mode 100644 index 0000000000..7caad6eb15 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml @@ -0,0 +1,7 @@ +[DEFAULT] +prefs = [ + "privacy.bounceTrackingProtection.enabled=true", + "privacy.bounceTrackingProtection.enableTestMode=true", +] + +["test_bouncetracking_storage_persistence.py"] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py new file mode 100644 index 0000000000..afc3239839 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py @@ -0,0 +1,133 @@ +# 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/. + +from marionette_harness import MarionetteTestCase + +# Tests the persistence of the bounce tracking protection storage across +# restarts. + + +class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase): + def setUp(self): + super(BounceTrackingStoragePersistenceTestCase, self).setUp() + self.marionette.enforce_gecko_prefs( + { + "privacy.bounceTrackingProtection.enabled": True, + "privacy.bounceTrackingProtection.enableTestMode": True, + } + ) + + self.marionette.set_context("chrome") + self.populate_state() + + def populate_state(self): + # Add some data to test persistence. + self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "bouncetracker.net", Date.now() * 10000); + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "bouncetracker.org", Date.now() * 10000); + bounceTrackingProtection.testAddBounceTrackerCandidate({ userContextId: 3 }, "tracker.com", Date.now() * 10000); + // A private browsing entry which must not be persisted across restarts. + bounceTrackingProtection.testAddBounceTrackerCandidate({ privateBrowsingId: 1 }, "tracker.net", Date.now() * 10000); + + bounceTrackingProtection.testAddUserActivation({}, "example.com", (Date.now() + 5000) * 10000); + // A private browsing entry which must not be persisted across restarts. + bounceTrackingProtection.testAddUserActivation({ privateBrowsingId: 1 }, "example.org", (Date.now() + 2000) * 10000); + """ + ) + + def test_state_after_restart(self): + self.marionette.restart(clean=False, in_app=True) + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(); + """, + ) + self.assertEqual( + len(bounceTrackerCandidates), + 2, + msg="There should be two entries for default OA", + ) + self.assertEqual(bounceTrackerCandidates[0], "bouncetracker.net") + self.assertEqual(bounceTrackerCandidates[1], "bouncetracker.org") + + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).sort(); + """, + ) + self.assertEqual( + len(bounceTrackerCandidates), + 1, + msg="There should be only one entry for user context 3", + ) + self.assertEqual(bounceTrackerCandidates[0], "tracker.com") + + # Unrelated user context should not have any entries. + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 4 }).length; + """, + ) + self.assertEqual( + bounceTrackerCandidates, + 0, + msg="There should be no entries for user context 4", + ) + + # Private browsing entries should not be persisted across restarts. + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ privateBrowsingId: 1 }).length; + """, + ) + self.assertEqual( + bounceTrackerCandidates, + 0, + msg="There should be no entries for private browsing", + ) + + userActivations = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetUserActivationHosts({}).sort(); + """, + ) + self.assertEqual( + len(userActivations), + 1, + msg="There should be only one entry for user activation", + ) + self.assertEqual(userActivations[0], "example.com") + + # Private browsing entries should not be persisted across restarts. + userActivations = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetUserActivationHosts({ privateBrowsingId: 1 }).length; + """, + ) + self.assertEqual( + userActivations, 0, msg="There should be no entries for private browsing" + ) diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js new file mode 100644 index 0000000000..5ede57a08b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js @@ -0,0 +1,307 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +let btp; +let bounceTrackingGracePeriodSec; +let bounceTrackingActivationLifetimeSec; + +/** + * Adds brackets to a host if it's an IPv6 address. + * @param {string} host - Host which may be an IPv6. + * @returns {string} bracketed IPv6 or host if host is not an IPv6. + */ +function maybeFixupIpv6(host) { + if (!host.includes(":")) { + return host; + } + return `[${host}]`; +} + +/** + * Adds cookies and indexedDB test data for the given host. + * @param {string} host + */ +async function addStateForHost(host) { + info(`adding state for host ${host}`); + SiteDataTestUtils.addToCookies({ host }); + await SiteDataTestUtils.addToIndexedDB(`https://${maybeFixupIpv6(host)}`); +} + +/** + * Checks if the given host as cookies or indexedDB data. + * @param {string} host + * @returns {boolean} + */ +async function hasStateForHost(host) { + let origin = `https://${maybeFixupIpv6(host)}`; + if (SiteDataTestUtils.hasCookies(origin)) { + return true; + } + return SiteDataTestUtils.hasIndexedDB(origin); +} + +/** + * Assert that there are no bounce tracker candidates or user activations + * recorded. + */ +function assertEmpty() { + Assert.equal( + btp.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No tracker candidates." + ); + Assert.equal( + btp.testGetUserActivationHosts({}).length, + 0, + "No user activation hosts." + ); +} + +add_setup(function () { + // Need a profile to data clearing calls. + do_get_profile(); + + btp = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + + // Reset global bounce tracking state. + btp.clearAll(); + + bounceTrackingGracePeriodSec = Services.prefs.getIntPref( + "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec" + ); + bounceTrackingActivationLifetimeSec = Services.prefs.getIntPref( + "privacy.bounceTrackingProtection.bounceTrackingActivationLifetimeSec" + ); +}); + +/** + * When both maps are empty running PurgeBounceTrackers should be a no-op. + */ +add_task(async function test_empty() { + assertEmpty(); + + info("Run PurgeBounceTrackers"); + await btp.testRunPurgeBounceTrackers(); + + assertEmpty(); +}); + +/** + * Tests that the PurgeBounceTrackers behaves as expected by adding site state + * and adding simulated bounce state and user activations. + */ +add_task(async function test_purge() { + let now = Date.now(); + + // Epoch in MS. + let timestampWithinGracePeriod = + now - (bounceTrackingGracePeriodSec * 1000) / 2; + let timestampWithinGracePeriod2 = + now - (bounceTrackingGracePeriodSec * 1000) / 4; + let timestampOutsideGracePeriodFiveSeconds = + now - (bounceTrackingGracePeriodSec + 5) * 1000; + let timestampOutsideGracePeriodThreeDays = + now - (bounceTrackingGracePeriodSec + 60 * 60 * 24 * 3) * 1000; + let timestampFuture = now + bounceTrackingGracePeriodSec * 1000 * 2; + + let timestampValidUserActivation = + now - (bounceTrackingActivationLifetimeSec * 1000) / 2; + let timestampExpiredUserActivationFourSeconds = + now - (bounceTrackingActivationLifetimeSec + 4) * 1000; + let timestampExpiredUserActivationTenDays = + now - (bounceTrackingActivationLifetimeSec + 60 * 60 * 24 * 10) * 1000; + + const TEST_TRACKERS = { + "example.com": { + bounceTime: timestampWithinGracePeriod, + userActivationTime: null, + message: "Should not purge within grace period.", + shouldPurge: bounceTrackingGracePeriodSec == 0, + }, + "example2.com": { + bounceTime: timestampWithinGracePeriod2, + userActivationTime: null, + message: "Should not purge within grace period (2).", + shouldPurge: bounceTrackingGracePeriodSec == 0, + }, + "example.net": { + bounceTime: timestampOutsideGracePeriodFiveSeconds, + userActivationTime: null, + message: "Should purge after grace period.", + shouldPurge: true, + }, + // Also ensure that clear data calls with IP sites succeed. + "1.2.3.4": { + bounceTime: timestampOutsideGracePeriodThreeDays, + userActivationTime: null, + message: "Should purge after grace period (2).", + shouldPurge: true, + }, + "2606:4700:4700::1111": { + bounceTime: timestampOutsideGracePeriodThreeDays, + userActivationTime: null, + message: "Should purge after grace period (3).", + shouldPurge: true, + }, + "example.org": { + bounceTime: timestampWithinGracePeriod, + userActivationTime: null, + message: "Should not purge within grace period.", + shouldPurge: false, + }, + "example2.org": { + bounceTime: timestampFuture, + userActivationTime: null, + message: "Should not purge for future bounce time (within grace period).", + shouldPurge: false, + }, + "1.1.1.1": { + bounceTime: null, + userActivationTime: timestampValidUserActivation, + message: "Should not purge without bounce (valid user activation).", + shouldPurge: false, + }, + // Also testing domains with trailing ".". + "mozilla.org.": { + bounceTime: null, + userActivationTime: timestampExpiredUserActivationFourSeconds, + message: "Should not purge without bounce (expired user activation).", + shouldPurge: false, + }, + "firefox.com": { + bounceTime: null, + userActivationTime: timestampExpiredUserActivationTenDays, + message: "Should not purge without bounce (expired user activation) (2).", + shouldPurge: false, + }, + }; + + info("Assert empty initially."); + assertEmpty(); + + info("Populate bounce and user activation sets."); + + let expectedBounceTrackerHosts = []; + let expectedUserActivationHosts = []; + + let expiredUserActivationHosts = []; + let expectedPurgedHosts = []; + + // This would normally happen over time while browsing. + let initPromises = Object.entries(TEST_TRACKERS).map( + async ([siteHost, { bounceTime, userActivationTime, shouldPurge }]) => { + // Add site state so we can later assert it has been purged. + await addStateForHost(siteHost); + + if (bounceTime != null) { + if (userActivationTime != null) { + throw new Error( + "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint." + ); + } + + expectedBounceTrackerHosts.push(siteHost); + + // Convert bounceTime timestamp to nanoseconds (PRTime). + info( + `Adding bounce. siteHost: ${siteHost}, bounceTime: ${bounceTime} ms` + ); + btp.testAddBounceTrackerCandidate({}, siteHost, bounceTime * 1000); + } + + if (userActivationTime != null) { + if (bounceTime != null) { + throw new Error( + "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint." + ); + } + + expectedUserActivationHosts.push(siteHost); + if ( + userActivationTime + bounceTrackingActivationLifetimeSec * 1000 > + now + ) { + expiredUserActivationHosts.push(siteHost); + } + + // Convert userActivationTime timestamp to nanoseconds (PRTime). + info( + `Adding user interaction. siteHost: ${siteHost}, userActivationTime: ${userActivationTime} ms` + ); + btp.testAddUserActivation({}, siteHost, userActivationTime * 1000); + } + + if (shouldPurge) { + expectedPurgedHosts.push(siteHost); + } + } + ); + await Promise.all(initPromises); + + info( + "Check that bounce and user activation data has been correctly recorded." + ); + Assert.deepEqual( + btp.testGetBounceTrackerCandidateHosts({}).sort(), + expectedBounceTrackerHosts.sort(), + "Has added bounce tracker hosts." + ); + Assert.deepEqual( + btp.testGetUserActivationHosts({}).sort(), + expectedUserActivationHosts.sort(), + "Has added user activation hosts." + ); + + info("Run PurgeBounceTrackers"); + let actualPurgedHosts = await btp.testRunPurgeBounceTrackers(); + + Assert.deepEqual( + actualPurgedHosts.sort(), + expectedPurgedHosts.sort(), + "Should have purged all expected hosts." + ); + + let expectedBounceTrackerHostsAfterPurge = expectedBounceTrackerHosts + .filter(host => !expectedPurgedHosts.includes(host)) + .sort(); + Assert.deepEqual( + btp.testGetBounceTrackerCandidateHosts({}).sort(), + expectedBounceTrackerHostsAfterPurge.sort(), + "After purge the bounce tracker candidate host set should be updated correctly." + ); + + Assert.deepEqual( + btp.testGetUserActivationHosts({}).sort(), + expiredUserActivationHosts.sort(), + "After purge any expired user activation records should have been removed" + ); + + info("Test that we actually purged the correct sites."); + for (let siteHost of expectedPurgedHosts) { + Assert.ok( + !(await hasStateForHost(siteHost)), + `Site ${siteHost} should no longer have state.` + ); + } + for (let siteHost of expectedBounceTrackerHostsAfterPurge) { + Assert.ok( + await hasStateForHost(siteHost), + `Site ${siteHost} should still have state.` + ); + } + + info("Reset bounce tracking state."); + btp.clearAll(); + assertEmpty(); + + info("Clean up site data."); + await SiteDataTestUtils.clear(); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..16e270b85c --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml @@ -0,0 +1,8 @@ +[DEFAULT] +prefs = [ + "privacy.bounceTrackingProtection.enabled=true", + "privacy.bounceTrackingProtection.enableTestMode=true", + "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0", +] + +["test_bouncetracking_purge.js"] diff --git a/toolkit/components/antitracking/components.conf b/toolkit/components/antitracking/components.conf new file mode 100644 index 0000000000..6a584b493f --- /dev/null +++ b/toolkit/components/antitracking/components.conf @@ -0,0 +1,64 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{3c9c43b6-09eb-4ed2-9b87-e29f4221eef0}', + 'contract_ids': ['@mozilla.org/tracking-db-service;1'], + 'esModule': 'resource://gre/modules/TrackingDBService.sys.mjs', + 'constructor': 'TrackingDBService', + }, + { + 'cid': '{5874af6d-5719-4e1b-b155-ef4eae7fcb32}', + 'contract_ids': ['@mozilla.org/tracking-url-decoration-service;1'], + 'esModule': 'resource://gre/modules/URLDecorationAnnotationsService.sys.mjs', + 'constructor': 'URLDecorationAnnotationsService', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{90d1fd17-2018-4e16-b73c-a04a26fa6dd4}', + 'contract_ids': ['@mozilla.org/purge-tracker-service;1'], + 'esModule': 'resource://gre/modules/PurgeTrackerService.sys.mjs', + 'constructor': 'PurgeTrackerService', + 'categories': {'profile-after-change': 'PurgeTrackerService'}, + }, + { + 'cid': '{ab94809d-33f0-4f28-af38-01efbd3baf22}', + 'contract_ids': ['@mozilla.org/partitioning/exception-list-service;1'], + 'esModule': 'resource://gre/modules/PartitioningExceptionListService.sys.mjs', + 'constructor': 'PartitioningExceptionListService', + }, + { + 'cid': '{42906796-d16a-44a1-b518-0f108ab38eba}', + 'contract_ids': ['@mozilla.org/content-blocking-telemetry-service;1'], + 'singleton': True, + 'type': 'mozilla::ContentBlockingTelemetryService', + 'headers': ['mozilla/ContentBlockingTelemetryService.h'], + 'constructor': 'mozilla::ContentBlockingTelemetryService::GetSingleton', + 'categories': {'idle-daily': 'ContentBlockingTelemetryService'}, + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{afff16f0-3fd2-4153-9ccd-c6d9abd879e4}', + 'contract_ids': ['@mozilla.org/query-stripping-list-service;1'], + 'singleton': True, + 'esModule': 'resource://gre/modules/URLQueryStrippingListService.sys.mjs', + 'constructor': 'URLQueryStrippingListService', + }, + { + 'name': 'URLQueryStringStripper', + 'cid': '{6b42a890-2624-4560-99c4-b25380e8cd77}', + 'interfaces': ['nsIURLQueryStringStripper'], + 'contract_ids': ['@mozilla.org/url-query-string-stripper;1'], + 'type': 'mozilla::URLQueryStringStripper', + 'headers': ['mozilla/URLQueryStringStripper.h'], + 'singleton': True, + 'constructor': 'mozilla::URLQueryStringStripper::GetSingleton', + 'categories': { + 'profile-after-change': 'URLQueryStringStripper', + } + }, +] diff --git a/toolkit/components/antitracking/data/StripOnShare.json b/toolkit/components/antitracking/data/StripOnShare.json new file mode 100644 index 0000000000..394723fa00 --- /dev/null +++ b/toolkit/components/antitracking/data/StripOnShare.json @@ -0,0 +1,94 @@ +{ + "global": { + "queryParams": [ + "utm_ad", + "utm_affiliate", + "utm_brand", + "utm_campaign", + "utm_campaignid", + "utm_channel", + "utm_cid", + "utm_content", + "utm_creative", + "utm_emcid", + "utm_emmid", + "utm_id", + "utm_id_", + "utm_keyword", + "utm_medium", + "utm_name", + "utm_place", + "utm_product", + "utm_pubreferrer", + "utm_reader", + "utm_referrer", + "utm_serial", + "utm_session", + "utm_siteid", + "utm_social", + "utm_social-type", + "utm_source", + "utm_supplier", + "utm_swu", + "utm_term", + "utm_umguk", + "utm_userid", + "utm_viz_id", + "vero_conv", + "ymid", + "var", + "s_cid", + "hsa_grp", + "hsa_cam", + "hsa_src", + "hsa_ad", + "hsa_acc", + "hsa_kw", + "hsa_tgt", + "hsa_ver", + "hsa_la", + "hsa_ol", + "hsa_net", + "hsa_mt" + ], + "topLevelSites": ["*"] + }, + "twitter": { + "queryParams": ["ref_src", "ref_url"], + "topLevelSites": ["www.twitter.com", "twitter.com"] + }, + "instagram": { + "queryParams": ["igshid", "ig_rid"], + "topLevelSites": ["www.instagram.com"] + }, + "amazon": { + "queryParams": [ + "keywords", + "pd_rd_r", + "pd_rd_w", + "pd_rd_wg", + "pf_rd_r", + "pf_rd_p", + "sr", + "content-id" + ], + "topLevelSites": [ + "www.amazon.com", + "www.amazon.de", + "www.amazon.nl", + "www.amazon.fr", + "www.amazon.co.jp", + "www.amazon.in", + "www.amazon.es", + "www.amazon.ac", + "www.amazon.cn", + "www.amazon.eg", + "www.amazon.in", + "www.amazon.co.uk", + "www.amazon.it", + "www.amazon.pl", + "www.amazon.sg", + "www.amazon.ca" + ] + } +} diff --git a/toolkit/components/antitracking/docs/cookie-purging/index.md b/toolkit/components/antitracking/docs/cookie-purging/index.md new file mode 100644 index 0000000000..7b8c76cd39 --- /dev/null +++ b/toolkit/components/antitracking/docs/cookie-purging/index.md @@ -0,0 +1,217 @@ +# Cookie Purging + +“Cookie Purging” describes a technique that will periodically clear +cookies and site data of known tracking domains without user interaction +to protect against [bounce +tracking](https://privacycg.github.io/nav-tracking-mitigations/#bounce-tracking). + +## Protection Background + +### What similar protections do other browsers have? + +**Safari** classifies sites as redirect trackers which directly or +shortly after navigation redirect the user to other sites. Sites which +receive user interaction are exempt from this. To detect bigger redirect +networks, sites may also inherit redirect tracker +[classification](https://privacycg.github.io/nav-tracking-mitigations/#mitigations-safari). +If a site is classified as a redirect tracker, any site pointing to it +will inherit this classification. Safari does not use tracker lists. + +When the source site is classified as a tracker, Safari will purge all +storage, excluding cookies. Sites which receive user interaction within +seven days of browser use are exempt. If the destination site's URL +includes query parameters or URL fragments, Safari caps the lifetime of +client-side set cookies of the destination site to 24 hours. + +**Brave** uses lists to classify redirect trackers. Recently, they have +rolled out a new protection, [Unlinkable Bouncing](https://brave.com/privacy-updates/16-unlinkable-bouncing/), +which limits first party storage lifetime. The underlying mechanism is +called “first-party ephemeral storage”. If a user visits a known +bounce-tracker which doesn’t have any pre-existing storage, the browser +will create a temporary first-party storage bucket for the destination +site. This temporary storage is cleared 30 seconds after the user closes +the last tab of the site. + +**Chrome** and **Edge** currently do not implement any navigational +tracking protections. + +### Is it standardized? + +At this time there are no standardized navigational tracking +protections. The PrivacyCG has a [work item for Navigation-based Tracking Mitigations](https://privacycg.github.io/nav-tracking-mitigations/). +Also see Apple’s proposal +[here](https://github.com/privacycg/proposals/issues/6). + +### How does it fit into our vision of “Zero Privacy Leaks?” + +Existing tracking protections mechanisms focus mostly on third-party +trackers. Redirect tracking can circumvent these mechanisms and utilize +first-party storage for tracking. Cookie purging contributes to the +“Zero Privacy Leaks” vision by mitigating this cross-site tracking +vector. + +## Firefox Status + +Metabug: [Bug 1594226 - \[Meta\] Purging Tracking Cookies](https://bugzilla.mozilla.org/show_bug.cgi?id=1594226) + +### What is the ship state of this protection in Firefox? + +Shipped to Release in standard ETP mode + +### Is there outstanding work? + +The mechanism of storing user interaction as a permission via +nsIPermissionManager has shown to be brittle and has led to users +getting logged out of sites in the past. The concept of a permission +doesn’t fully match that of a user interaction flag. Permissions may be +separated between normal browsing and PBM (Bug +[1692567](https://bugzilla.mozilla.org/show_bug.cgi?id=1692567)). +They may also get purged when clearing site data (Bug +[1675018](https://bugzilla.mozilla.org/show_bug.cgi?id=1675018)). + +A proposed solution to this is to create a dedicated data store for +keeping track of user interaction. This could also enable tracking user +interaction relative to browser usage time, rather than absolute time +([Bug 1637146](https://bugzilla.mozilla.org/show_bug.cgi?id=1637146)). + +Important outstanding bugs: +- [Bug 1637146 - Use use-time rather than absolute time when computing whether to purge cookies](https://bugzilla.mozilla.org/show_bug.cgi?id=1637146) + +### Existing Documentation + +- [https://developer.mozilla.org/en-US/docs/Web/Privacy/Redirect\_tracking\_protection](https://developer.mozilla.org/en-US/docs/Web/Privacy/Redirect_tracking_protection) + +- [PrivacyCG: Navigational-Tracking Mitigations](https://privacycg.github.io/nav-tracking-mitigations/) + + +## Technical Information + +### Feature Prefs + +Cookie purging can be enabled or disabled by flipping the +`privacy.purge_trackers.enabled` preference. Further, it will only run if +the `network.cookie.cookieBehavior` pref is set to `4` or `5` ([bug 1643045](https://bugzilla.mozilla.org/show_bug.cgi?id=1643045) adds +support for behaviors `1` and `3`). + +Different log levels can be set via the pref +`privacy.purge_trackers.logging.level`. + +The time until user interaction permissions expire can be set to a lower +amount of time using the `privacy.userInteraction.expiration` pref. Note +that you will have to set this pref before visiting the sites you want +to test on, it will not apply retroactively. + +### How does it work? + +Cookie purging periodically clears first-party storage of known +trackers, which the user has not interacted with recently. It is +implemented in the +[PurgeTrackerService](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/antitracking/PurgeTrackerService.jsm), +which implements the +[nsIPurgeTrackerService](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/antitracking/nsIPurgeTrackerService.idl) +IDL interface. + +#### What origins are cleared? + +An origin will be cleared if it fulfills the following conditions: + +1. It has stored cookies or accessed other site storage (e.g. + [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API), + [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), + or the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage)) + within the last 72 hours. Since cookies are per-host, we will + clear both the http and https origin variants of a cookie host. + +2. The origin is [classified as a tracker](https://developer.mozilla.org/en-US/docs/Web/Privacy/Storage_Access_Policy#tracking_protection_explained) + in our Tracking Protection list. + +3. No origin with the same base domain (eTLD+1) has a user-interaction + permission. + + - This permission is granted to an origin for 45 days once a user + interacts with a top-level document from that origin. + "Interacting" includes scrolling. + + - Although this permission is stored on a per-origin level, we + will check whether any origin with the same base domain has + it, to avoid breaking sites with subdomains and a + corresponding cookie setup. + +#### What data is cleared? + +Firefox will clear the [following data](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/antitracking/PurgeTrackerService.jsm#205-213): + +- Network cache and image cache + +- Cookies + +- DOM Quota Storage (localStorage, IndexedDB, ServiceWorkers, DOM + Cache, etc.) + +- DOM Push notifications + +- Reporting API Reports + +- Security Settings (i.e. HSTS) + +- EME Media Plugin Data + +- Plugin Data (e.g. Flash) + +- Media Devices + +- Storage Access permissions granted to the origin + +- HTTP Authentication Tokens + +- HTTP Authentication Cache + +**Note:** Even though we're clearing all of this data, we currently only +flag origins for clearing when they use cookies or other site storage. + +Storage clearing ignores origin attributes. This means that storage will +be cleared across +[containers](https://wiki.mozilla.org/Security/Contextual_Identity_Project/Containers) +and isolated storage (i.e. from [First-Party Isolation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies#first-party_isolation)). + +#### How frequently is data cleared? + +Firefox clears storage based on the firing of an internal event called +[idle-daily](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/antitracking/PurgeTrackerService.jsm#60,62,65), +which is defined by the following conditions: + +- It will, at the earliest, fire 24h after the last idle-daily event + fired. + +- It will only fire if the user has been idle for at least 3min (for + 24-48h after the last idle-daily) or 1 min (for >48h after the + last idle-daily). + +This means that there are at least 24 hours between each storage +clearance, and storage will only be cleared when the browser is idle. +When clearing cookies, we sort cookies by creation date and batch them +into sets of 100 (controlled by the pref +`privacy.purge_trackers.max_purge_count`) for performance reasons. + +#### Debugging + +For debugging purposes, it's easiest to trigger storage clearing by +triggering the service directly via the [Browser Console command line](/devtools-user/browser_console/index.rst#browser_console_command_line). +Note that this is different from the normal [Web Console](/devtools-user/web_console/index.rst) +you might use to debug a website, and requires the +`devtools.chrome.enabled` pref to be set to true to use it interactively. +Once you've enabled the Browser Console you can trigger storage clearing +by running the following command: + +``` javascript +await Components.classes["@mozilla.org/purge-tracker-service;1"] +.getService(Components.interfaces.nsIPurgeTrackerService) +.purgeTrackingCookieJars() +``` + +<!--- +TODO: consider integrating +[https://developer.mozilla.org/en-US/docs/Web/Privacy/Redirect\_tracking\_protection](https://developer.mozilla.org/en-US/docs/Web/Privacy/Redirect_tracking_protection) +into firefox source docs. The article doesn’t really belong into MDN, +because it’s very specific to Firefox. +--> diff --git a/toolkit/components/antitracking/docs/data-sanitization/index.md b/toolkit/components/antitracking/docs/data-sanitization/index.md new file mode 100644 index 0000000000..b04222d190 --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/index.md @@ -0,0 +1,230 @@ +# Data Sanitization +<!-- TODO: This doesn't strictly talk only about toolkit code. Consider splitting the article up and moving to relevant components --> + +Firefox has several Data Sanitization features. They allow users to clear preferences and website data. Clearing data is an essential feature for user privacy. There are two major privacy issues data clearing helps mitigate: + +1. Websites tracking the user via web-exposed APIs and storages. This can be traditional storages, e.g. localStorage, or cookies. However, sites can also use Supercookies, e.g. caches, to persist storage in the browser. + +2. Attackers who have control over a computer can exfiltrate data from Firefox, such as history, passwords, etc. + +## Protection Background + +### What similar protections do other browsers have? + +All major browsers implement data clearing features ([Chrome](https://support.google.com/chrome/answer/2392709?hl=en&co=GENIE.Platform%3DDesktop&oco=0#zippy=), [Edge](https://support.microsoft.com/en-us/microsoft-edge/view-and-delete-browser-history-in-microsoft-edge-00cf7943-a9e1-975a-a33d-ac10ce454ca4), [Safari](https://support.apple.com/guide/safari/clear-your-browsing-history-sfri47acf5d6/mac), [Brave](https://support.brave.com/hc/en-us/articles/360054509991-How-do-I-clear-Cookies-and-Site-data-in-Brave-on-Android-)). They usually include a way for users to clear site data within a configurable time-span along with a list of data categories to be cleared. + +Chrome, Edge and Brave all share Chromium’s data clearing dialog with smaller adjustments. Notably, Brave extends it with a clear-on-shutdown mechanism similar to Firefox, while Chrome only supports clearing specifically site data on shutdown. + +Safari’s history clearing feature only allows users to specify a time span. It does not allow filtering by categories, but clears all website related data. + +All browsers allow fine grained control over website cookies and storages via the developer tools. + +### Is it standardized? + +This is a browser UX feature and is therefore not standardized. It is not part of the web platform. + +There is a standardized HTTP header that sites can send to clear associated browser cache, cookies and storage: [Clear-Site-Data](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data). However, Firefox no longer allows sites to clear caches via the header since [Bug 1671182](https://bugzilla.mozilla.org/show_bug.cgi?id=1671182). + +### How does it fit into our vision of “Zero Privacy Leaks?” + +Clearing site data protects users against various tracking techniques that rely on browser state to (re-)identify users. While Total Cookie Protection covers many cross-site tracking scenarios, clearing site data can additionally protect against first-party tracking and other tracking methods that bypass TCP such as [navigational tracking](https://privacycg.github.io/nav-tracking-mitigations/#intro). + +## Firefox Status + +### What is the ship state of this protection in Firefox? + +This long standing set of features is shipped in Release in default ETP mode. In Firefox 91 we introduced [Enhanced Cookie Clearing](https://blog.mozilla.org/security/2021/08/10/firefox-91-introduces-enhanced-cookie-clearing/) which makes use of TCP’s cookie jars. This feature only benefits users who have TCP enabled - which is not enabled default for users. + +### Is there outstanding work? + +Since [Bug 1422365](https://bugzilla.mozilla.org/show_bug.cgi?id=1422365) the ClearDataService provides a common interface to clear data of various storage implementations. However, we don’t have full coverage of all browser state yet. There are several smaller blind spots, most of which are listed in this [meta bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1102808). There is also a long backlog of data sanitization bugs [here](https://bugzilla.mozilla.org/show_bug.cgi?id=1550317). + +The clear history UI has a intuitive and modern UI, which offers users an easy way to clear their data, while feeling that their privacy is secured in Firefox. The UI changes were undertaken in this [meta bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1853996). + +Data clearing can take a long time on bigger Firefox profiles. Since these operations mostly run on the main thread, this can lock up the UI making the browser unresponsive until the operation has completed. + +Generally it would be worth revisiting cleaner implementations in the ClearDataService and beyond to see where we can improve clearing performance. + +Slow data clearing is especially problematic on shutdown. If the sanitize-on-shutdown feature takes too long to clear storage, the parent process will be terminated, resulting in a shutdown crash. [Bug 1756724](https://bugzilla.mozilla.org/show_bug.cgi?id=1756724) proposes a solution to this: We could show a progress dialog when clearing data. This way we can allow a longer shutdown phase, since the user is aware that we’re clearing data. + +Important outstanding bugs: + +- [Bug 1550317 - \[meta\] Broken data sanitization](https://bugzilla.mozilla.org/show_bug.cgi?id=1550317) + +- [Bug 1102808 - \[meta\] Clear Recent History / Forget button blind spots](https://bugzilla.mozilla.org/show_bug.cgi?id=1102808) +- [Bug 1756724 - Show a data clearing progress dialog when sanitizing data at shutdown due to "delete cookies and site data when Firefox is closed"](https://bugzilla.mozilla.org/show_bug.cgi?id=1756724) + +### Existing Documentation + +<!-- TODO: link existing documentation, if any --> + +\- + +## Technical Information + +### Feature Prefs + +| Pref | Description | +| ---- | ----------- | +| places.forgetThisSite.clearByBaseDomain | Switches “Forget about this site” to clear for the whole base domain rather than just the host. | +| privacy.sanitize.sanitizeOnShutdown | Whether to clear data on Firefox shutdown. | +| privacy.clearOnShutdown.* | Categories of data to be cleared on shutdown. True = clear category. Data is only cleared if privacy.sanitize.sanitizeOnShutdown is enabled.| +| privacy.clearHistory.* | Categories of data to be cleared in the clear history or browser context. True = clear category.| +| privacy.clearSiteData.* | Categories of data to be cleared in the clear site data context. True = clear category.| + +### How does it work? + +The following section lists user facing data sanitization features in Firefox, along with a brief description and a diagram how they tie into the main clearing logic in `nsIClearDataService`. + +The recent revamp of them clear history dialog led to a combination of the various entry points, to use the same dialog +#### Clear browsing data and cookies +- Accessible via + - `about:preferences#privacy` => Clear Site Data + - `about:preferences#privacy` => Clear History + - Hamburger menu => History => Clear Recent History + - Clears a configurable list of categories as defined in [Sanitizer.sys.mjs](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/modules/Sanitizer.sys.mjs#455) +- Can clear everything or a specific time range +- Shows data sizes based on time range for cookies and site data using the method [getQuotaUsageForTimeRanges](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/modules/SiteDataManager.sys.mjs#392) +- Currently, the cache and some site settings are over cleared (clears everything, regardless of time range selection). Outstanding bugs: + - [Bug 1860069 - Clearing cache by range clears the full cache](https://bugzilla.mozilla.org/show_bug.cgi?id=1860069) + - [Bug 1878902 - Investigate clearing site preferences by range](https://bugzilla.mozilla.org/show_bug.cgi?id=1878902) +- Source + - [sanitize_v2.xhtml](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/base/content/sanitize_v2.xhtml) + - [sanitizeDialog.js](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/base/content/sanitizeDialog.js) + +![newClearHistoryDialog](media/newClearHistoryDialog.png) + +#### Sanitize on Shutdown + +- Can be enabled via `about:preferences#privacy` => History: Firefox will: Use custom settings for history => Check “Clear history when Firefox closes” + - After [Bug 1681493](https://bugzilla.mozilla.org/show_bug.cgi?id=1681493) it can also be controlled via the checkbox “Delete cookies and site data when Firefox is closed” +- On shutdown of Firefox, will clear all data for the selected categories. The list of categories is defined in [Sanitizer.sys.mjs](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/modules/Sanitizer.sys.mjs#455) +- Categories are the same as for the “Clear recent history” dialog +- Exceptions + - Sites which have a “cookie” permission, set to [ACCESS\_SESSION](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/netwerk/cookie/nsICookiePermission.idl#28) always get cleared, even if sanitize-on-shutdown is disabled + - Sites which have a “cookie” permission set to [ACCESS\_ALLOW](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/netwerk/cookie/nsICookiePermission.idl#19) are exempt from data clearing + - Caveat: When “site settings” is selected in the categories to be cleared, the Sanitizer will remove exception permissions too. This results in the above exceptions being cleared. +- Uses PrincipalsCollector to obtain a list of principals which have site data associated with them +- [getAllPrincipals](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/toolkit/components/cleardata/PrincipalsCollector.sys.mjs#83) queries the QuotaManager, the cookie service and the service worker manager for principals +- The list of principals obtained is checked for permission exceptions. Principals which set a cookie [ACCESS\_ALLOW](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/netwerk/cookie/nsICookiePermission.idl#19) permission are removed from the list. +- Sanitizer.sys.mjs [calls the ClearDataService](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/modules/Sanitizer.sys.mjs#1299,1304-1309) to clear data for every principal from the filtered list +- Source + - Most of the sanitize-on-shutdown logic is implemented in [Sanitizer.sys.mjs](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/modules/Sanitizer.sys.mjs) + - The main entry point is [sanitizeOnShutdown](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/modules/Sanitizer.sys.mjs#1050) + + - [Parts of sanitize-on-shutdown](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/modules/Sanitizer.sys.mjs#1145-1152) always have to run, even if the rest of the feature is disabled, to support clearing storage of sites which have “cookie” set to [ACCESS\_SESSION](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/netwerk/cookie/nsICookiePermission.idl#28) (see exceptions above) + +```{mermaid} +flowchart TD + A[Clear History] & B[Clear Site Data] & C[Clear On Shutdown]-->|init| D[sanitizeDialog.js] + D --> |Clear|E[Sanitizer.sys.mjs] + E --> F[ClearDataService.sys.mjs] +``` + +#### Forget About this Site + +- Accessible via hamburger menu => History => Contextmenu of an item => Forget About This Site +- Clears all data associated with the base domain of the selected site +- \[With TCP\] Also clears data of any third-party sites embedded under the top level base domain +- The goal is to remove all traces of the associated site from Firefox +- Clears [[flags](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/toolkit/components/cleardata/nsIClearDataService.idl#302-307)\] + - History, session history, download history + - All caches + - Site data (cookies, dom storages) + - Encrypted Media Extensions (EME) + - Passwords (See [Bug 702925](https://bugzilla.mozilla.org/show_bug.cgi?id=702925)) + - Permissions + - Content preferences (e.g. page zoom level) + - Predictor network data + - Reports (Reporting API) + - Client-Auth-Remember flag, Certificate exceptions + - Does **not** clear bookmarks +- Source + - [ForgetAboutSite.sys.mjs](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/toolkit/components/forgetaboutsite/ForgetAboutSite.sys.mjs) + - [nsIClearDataService flags used](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/toolkit/components/cleardata/nsIClearDataService.idl#302-307) + +```{mermaid} +flowchart TD + A[Places controller.js] --> B[removeDataFromBaseDomain] + B --> C[ForgetAboutSite.sys.mjs] + C --> D[ClearDataService.sys.mjs] +``` + +![image2](media/image2.png) + + +#### Manage Cookies and Site Data + +- Accessible via `about:preferences#privacy` => Cookies and Site Data => Manage Data +- Clears \[[flags](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/modules/SiteDataManager.sys.mjs#537,549-554)\] + - Cookies + - DOM storages + - EME + - Caches: CSS, Preflight, HSTS +- Lists site cookies and storage grouped by base domain. +- Clearing data on a more granular (host or origin) level is not possible. This is a deliberate decision to make this UI more thorough in cleaning and easier to understand. If users need very granular data management capabilities, they can install an addon or use the devtools. +- Allows users to clear storage for specific sites, or all sites +- \[With TCP\] Also clears data of any third-party sites embedded under the top level base domain +- Collects list of sites via [SiteDataManager.getSites](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/modules/SiteDataManager.sys.mjs#437) +- Before removal, prompts via SiteDataManger.promptSiteDataRemoval +- On removal calls SiteDataManager.removeAll() if all sites have been selected or SiteDataManager.remove() passing a list of sites to be removed. +- Source + - [siteDataSettings.xhtml](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/components/preferences/dialogs/siteDataSettings.xhtml) + - [siteDataSettings.js](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/components/preferences/dialogs/siteDataSettings.js) + +#### Clear Cookies and Site Data + +- Accessible via the identity panel (click on lock icon in the URL bar) +- Clears \[[flags](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/browser/modules/SiteDataManager.sys.mjs#537,549-554)\] + - Cookies + - DOM storages + - EME + - Caches: CSS, Preflight, HSTS +- Button handler method: [clearSiteData](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/base/content/browser-siteIdentity.js#364-385) +- Calls SiteDataManager.remove() with the base domain of the currently selected tab +- The button is only shown if a site has any cookies or quota storage. This is checked [here](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/browser/base/content/browser-siteIdentity.js#923). +- Source + - [identityPanel.inc.xhtml](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/browser/components/controlcenter/content/identityPanel.inc.xhtml#97) + - [browser-siteIdentity.js](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/browser/base/content/browser-siteIdentity.js#364) + +![image7](media/image7.png) + + + +![image5](media/image5.png) + + +A broad overview of the different data clearing features accessible via about:preferences#privacy. + +The user can clear data on demand or choose to clear data on shutdown. For the latter the user may make exceptions for specific origins not to be cleared or to be always cleared on shutdown. + +#### ClearDataService + +This service serves as a unified module to hold all data clearing logic in Firefox / Gecko. Callers can use the [nsIClearDataService](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/cleardata/nsIClearDataService.idl) interface to clear data. From JS the service is accessible via Services.clearData. + +To specify which state to clear pass a combination of [flags](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/cleardata/nsIClearDataService.idl#161-308) into aFlags. + +Every category of browser state should have its own cleaner implementation which exposes the following methods to the ClearDataService: + - **deleteAll**: Deletes all data owned by the cleaner + - **deleteByPrincipal**: Deletes data associated with a specific principal. + - **deleteByBaseDomain**: Deletes all entries which are associated with the given base domain. This includes data partitioned by Total Cookie Protection. + - **deleteByHost**: Clears data associated with a host. Does not clear partitioned data. + - **deleteByRange**: Clear data which matches a given time-range. + - **deleteByLocalFiles**: Delete data held for local files and other hostless origins. + - **deleteByOriginAttributes**: Clear entries which match an [OriginAttributesPattern](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/caps/OriginAttributes.h#153). + + Some of these methods are optional. See [comment here](https://searchfox.org/mozilla-central/rev/6b8a3f804789fb865f42af54e9d2fef9dd3ec74d/toolkit/components/cleardata/ClearDataService.sys.mjs#85-110). + +If a cleaner does not support a specific method, we will usually try to fall back to deleteAll. For privacy reasons we try to over-clear storage rather than under-clear it or not clear it at all because we can’t target individual entries. + +![image8](media/image8.png) + + +Overview of the most important cleaning methods of the ClearDataService called by other Firefox / Gecko components. deleteDataFromPrincipal is called programmatically, while user exposed data clearing features clear by base domain, host or all data. +<!-- + +TODO: For firefox-source-docs, import JSdoc for relevant modules + +[like so](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/toolkit/components/prompts/docs/nsIPromptService-reference.rst#9) + +--> diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image1.png b/toolkit/components/antitracking/docs/data-sanitization/media/image1.png Binary files differnew file mode 100644 index 0000000000..c9029753a3 --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/media/image1.png diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image2.png b/toolkit/components/antitracking/docs/data-sanitization/media/image2.png Binary files differnew file mode 100644 index 0000000000..451b297b7e --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/media/image2.png diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image3.png b/toolkit/components/antitracking/docs/data-sanitization/media/image3.png Binary files differnew file mode 100644 index 0000000000..8b37d5f160 --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/media/image3.png diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image4.png b/toolkit/components/antitracking/docs/data-sanitization/media/image4.png Binary files differnew file mode 100644 index 0000000000..25b0811441 --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/media/image4.png diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image5.png b/toolkit/components/antitracking/docs/data-sanitization/media/image5.png Binary files differnew file mode 100644 index 0000000000..21f34894fa --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/media/image5.png diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image6.png b/toolkit/components/antitracking/docs/data-sanitization/media/image6.png Binary files differnew file mode 100644 index 0000000000..719227d2ca --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/media/image6.png diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image7.png b/toolkit/components/antitracking/docs/data-sanitization/media/image7.png Binary files differnew file mode 100644 index 0000000000..9259380188 --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/media/image7.png diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image8.png b/toolkit/components/antitracking/docs/data-sanitization/media/image8.png Binary files differnew file mode 100644 index 0000000000..469aa398cd --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/media/image8.png diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/newClearHistoryDialog.png b/toolkit/components/antitracking/docs/data-sanitization/media/newClearHistoryDialog.png Binary files differnew file mode 100644 index 0000000000..49f99cc6a5 --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/media/newClearHistoryDialog.png diff --git a/toolkit/components/antitracking/docs/index.rst b/toolkit/components/antitracking/docs/index.rst new file mode 100644 index 0000000000..45d888b989 --- /dev/null +++ b/toolkit/components/antitracking/docs/index.rst @@ -0,0 +1,12 @@ +================================= +Anti-Tracking +================================= + +This page is an overview of various anti-tracking components. + +.. toctree:: + :maxdepth: 1 + + Cookie Purging <cookie-purging/index.md> + Data Sanitization <data-sanitization/index.md> + Query Stripping <query-stripping/index.md> diff --git a/toolkit/components/antitracking/docs/query-stripping/index.md b/toolkit/components/antitracking/docs/query-stripping/index.md new file mode 100644 index 0000000000..e49d8513ba --- /dev/null +++ b/toolkit/components/antitracking/docs/query-stripping/index.md @@ -0,0 +1,153 @@ +# Query Parameter Stripping + +To combat [Navigational +Tracking](https://privacycg.github.io/nav-tracking-mitigations/#navigational-tracking) +through [link +decoration](https://privacycg.github.io/nav-tracking-mitigations/#link-decoration), +Firefox can strip known tracking query parameters from URLs before the +user navigates to them. + +## Protection Background + +### What similar protections do other browsers have? + +Brave also has a list-based query parameter stripping mechanism. A list +of query parameters stripped can be found +[here](https://github.com/brave/brave-core/blob/5fcad3e35bac6fea795941fd8189a59d79d488bc/browser/net/brave_site_hacks_network_delegate_helper.cc#L29-L67). +Brave also has a strip-on-copy feature which allows users to copy a +stripped version of the current URL. + +### Is it standardized? + +At this time there are no standardized navigational tracking +protections. The PrivacyCG has a [work item for Navigation-based +Tracking +Mitigations](https://privacycg.github.io/nav-tracking-mitigations/). +Also see Apple’s proposal +[here](https://github.com/privacycg/proposals/issues/6). + +### How does it fit into our vision of “Zero Privacy Leaks?” + +Existing tracking protections mechanisms in Firefox, such as ETP and TCP +focus mostly on third-party trackers. Redirect tracking can circumvent +these mechanisms by passing identifiers through link decoration and +first-party storage. Query parameter stripping contributes to the “Zero +Privacy Leaks” vision by mitigating this cross-site tracking vector. + +## Firefox Status + +Metabug: [Bug 1706602 - \[meta\] Implement URL query string stripping +prototype](https://bugzilla.mozilla.org/show_bug.cgi?id=1706602) + +### What is the ship state of this protection in Firefox? + +Query stripping is enabled in release in ETP strict with an initial list +of query params: + +- mc\_eid + +- oly\_anon\_id + +- oly\_enc\_id + +- \_\_s + +- vero\_id + +- \_hsenc + +- mkt\_tok + +- fbclid + +It is enabled in Nightly by default in all modes with an extended +strip-list. You can find the current list of parameters that are +stripped +[here](https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/query-stripping/records). +Note that some records have a *filter\_expression* that limits where +they apply. + +### Is there outstanding work? + +After our initial release on ETP strict, we are considering to ship the +feature to Private Browsing Mode and possibly also to enable it by default +in release in the future. + +Other possible improvements: + +- Extend the list of query parameters stripped, in accordance with our policy. + +- Extend the protection to cover different kinds of link decoration, beyond just query parameters. + +- Ability to identify and strip hashed link decoration fields + +- Strip query params for urls shared / copied out from the browser + +Outstanding bugs: + +- See dependencies of [Bug 1706602 - \[meta\] Implement URL query + string stripping + prototype](https://bugzilla.mozilla.org/show_bug.cgi?id=1706602) + +### Existing Documentation + +- [Anti-Tracking Policy: Navigational cross-site + tracking](https://wiki.mozilla.org/Security/Anti_tracking_policy#2._Navigational_cross-site_tracking) + +## Technical Information + +### Feature Prefs + +| Pref | Description | +| ---- | ----------- | +| privacy.query_stripping.enabled | Enable / disable the feature in normal browsing. | +| privacy.query_stripping.enabled.pbmode | Enable / disable the feature in private browsing. | +| privacy.query_stripping.allow_list | Comma separated list of sites (without scheme) which should not have their query parameters stripped. | +| privacy.query_stripping.redirect | Whether to perform stripping for redirects. | +| privacy.query_stripping.strip_list | List of space delimited query parameters to be stripped. | + +### How does it work? + +![Architecture](overview.png "Overview") + +[**UrlQueryStrippingListService**](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStrippingListService.jsm) + +- Collects list of query parameters to be stripped and allow-list from + the *privacy.query\_stripping.strip\_list/allow\_list* preference + and the *query-stripping* Remote Settings collection + +- Lists from the two sources are + [concatenated](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStrippingListService.jsm#150-151) + +- Lists are distributed via [observer + notification](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStrippingListService.jsm#158-161) + via the + [nsIUrlQueryStrippingListService](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl#25). + [onQueryStrippingListUpdate](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl#25) + is called initially on registration and whenever the preferences + or the Remote Settings collection updates. + +[**URLQueryStringStripper**](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStringStripper.h) + +- Only subscriber of the + [UrlQueryStrippingListService](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStrippingListService.jsm) + +- Holds [hash set + representations](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStringStripper.h#56-57) + of the strip- and allow-list. + +- [URLQueryStringStripper::Strip](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStringStripper.cpp#45): + takes a nsIURI as input and strips any query parameters that are + on the strip-list. If the given URI matches a site on the + allow-list no query parameters are stripped. + +**Consumers** + +- [nsDocShell::DoURILoad](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/docshell/base/nsDocShell.cpp#10569): + Strips in the content, before creating the channel. + +- [BrowsingContext::LoadURI](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/docshell/base/BrowsingContext.cpp#2019): + Strips before loading the URI in the parent. + +- [nsHttpChannel::AsyncProcessRedirection](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/netwerk/protocol/http/nsHttpChannel.cpp#5154): + Strips query parameters for HTTP redirects (e.g. 301). diff --git a/toolkit/components/antitracking/docs/query-stripping/overview.png b/toolkit/components/antitracking/docs/query-stripping/overview.png Binary files differnew file mode 100644 index 0000000000..63cf495202 --- /dev/null +++ b/toolkit/components/antitracking/docs/query-stripping/overview.png diff --git a/toolkit/components/antitracking/jar.mn b/toolkit/components/antitracking/jar.mn new file mode 100644 index 0000000000..e45b37c5b6 --- /dev/null +++ b/toolkit/components/antitracking/jar.mn @@ -0,0 +1,12 @@ +# 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/. + +toolkit.jar: + # This is the strip list that contains the query parameters stripped by the strip-on-share feature + # (In addition to this list, the strip-on-share feature also strips the query params from the qps list). + # The strip list has the format: + # domain1: {queryParams: [param1, param2, ..], topLevelSites: [www.site.de, www.site.com,...]}, domain2: {...} + # This list will be consumed from the nsIQueryStrippingListService and + # later be dispatched to the nsIURLQueryStringStripper in a further processed form. + content/global/antitracking/StripOnShare.json (data/StripOnShare.json) diff --git a/toolkit/components/antitracking/moz.build b/toolkit/components/antitracking/moz.build new file mode 100644 index 0000000000..6e9ab6f129 --- /dev/null +++ b/toolkit/components/antitracking/moz.build @@ -0,0 +1,101 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking") + +JAR_MANIFESTS += ["jar.mn"] + +DIRS += [ + "bouncetrackingprotection", +] + +XPIDL_SOURCES += [ + "nsIContentBlockingAllowList.idl", + "nsIPartitioningExceptionListService.idl", + "nsIPurgeTrackerService.idl", + "nsITrackingDBService.idl", + "nsIURLDecorationAnnotationsService.idl", + "nsIURLQueryStringStripper.idl", + "nsIURLQueryStrippingListService.idl", +] + +XPIDL_MODULE = "toolkit_antitracking" + +EXTRA_COMPONENTS += [ + "antitracking.manifest", +] + +EXTRA_JS_MODULES += [ + "ContentBlockingAllowList.sys.mjs", + "PartitioningExceptionListService.sys.mjs", + "PurgeTrackerService.sys.mjs", + "TrackingDBService.sys.mjs", + "URLDecorationAnnotationsService.sys.mjs", + "URLQueryStrippingListService.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXPORTS.mozilla = [ + "AntiTrackingIPCUtils.h", + "AntiTrackingRedirectHeuristic.h", + "AntiTrackingUtils.h", + "ContentBlockingAllowList.h", + "ContentBlockingLog.h", + "ContentBlockingNotifier.h", + "ContentBlockingTelemetryService.h", + "ContentBlockingUserInteraction.h", + "DynamicFpiRedirectHeuristic.h", + "PartitioningExceptionList.h", + "StorageAccess.h", + "StorageAccessAPIHelper.h", + "StoragePrincipalHelper.h", + "URLDecorationStripper.h", + "URLQueryStringStripper.h", +] + +UNIFIED_SOURCES += [ + "AntiTrackingRedirectHeuristic.cpp", + "AntiTrackingUtils.cpp", + "ContentBlockingAllowList.cpp", + "ContentBlockingLog.cpp", + "ContentBlockingNotifier.cpp", + "ContentBlockingTelemetryService.cpp", + "ContentBlockingUserInteraction.cpp", + "DynamicFpiRedirectHeuristic.cpp", + "PartitioningExceptionList.cpp", + "SettingsChangeObserver.cpp", + "StorageAccess.cpp", + "StorageAccessAPIHelper.cpp", + "StoragePrincipalHelper.cpp", + "TemporaryAccessGrantObserver.cpp", + "URLDecorationStripper.cpp", + "URLQueryStringStripper.cpp", +] + +LOCAL_INCLUDES += [ + "/netwerk/base", + "/netwerk/protocol/http", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +if CONFIG["MOZ_BUILD_APP"] != "mobile/android": + BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser-blocking.toml", + "test/browser/browser.toml", + ] + +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"] + +TEST_DIRS += ["test/gtest"] + +SPHINX_TREES["anti-tracking"] = "docs" diff --git a/toolkit/components/antitracking/nsIContentBlockingAllowList.idl b/toolkit/components/antitracking/nsIContentBlockingAllowList.idl new file mode 100644 index 0000000000..90c2c60492 --- /dev/null +++ b/toolkit/components/antitracking/nsIContentBlockingAllowList.idl @@ -0,0 +1,20 @@ +/* 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 "nsISupports.idl" +#include "nsIPrincipal.idl" +/** + * This file contains an interface to the ContentBlockingAllowList. + */ +[scriptable, uuid(00ed5d73-9de5-42cf-868c-e739a94f6b37)] +interface nsIContentBlockingAllowList : nsISupports { + + /** + * Computes a contentBlockingAllowList principal for a given content principal. + * + * @param aPrincipal the content principal for which the contentBlockingAllowList principal is computed. + * @return a contentBlockingAllowList principal. + */ + nsIPrincipal computeContentBlockingAllowListPrincipal(in nsIPrincipal aPrincipal); +}; diff --git a/toolkit/components/antitracking/nsIPartitioningExceptionListService.idl b/toolkit/components/antitracking/nsIPartitioningExceptionListService.idl new file mode 100644 index 0000000000..6a6f97a9c9 --- /dev/null +++ b/toolkit/components/antitracking/nsIPartitioningExceptionListService.idl @@ -0,0 +1,51 @@ +/* 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 "nsISupports.idl" + +/** + * Observer for exception list updates. + */ +[scriptable, function, uuid(d8db1086-7b59-44d3-9f88-f31a7e642637)] +interface nsIPartitioningExceptionListObserver : nsISupports +{ + /** + * Called by nsIPartitioningExceptionListService when the exception list + * changes and when the observer is first registered. + * + * @param aList + * A semicolon-separated list of comma-separated url pairs. + */ + void onExceptionListUpdate(in ACString aList); +}; + +/** + * A service that monitors updates to the exception list of partitioning + * from sources such as a local pref and remote settings updates. + */ +[scriptable, uuid(cf83a9af-dd3f-43a2-88bb-489a22bca124)] +interface nsIPartitioningExceptionListService : nsISupports +{ + /** + * Register a new observer to exception list updates. When the observer is + * registered it is called immediately once. Afterwards it will be called + * whenever the specified pref changes or when remote settings for + * partitioning updates. + * + * @param aObserver + * An nsIPartitioningExceptionListObserver object or function that + * will receive updates to the exception list as a comma-separated + * string. Will be called immediately with the current exception + * list value. + */ + void registerAndRunExceptionListObserver(in nsIPartitioningExceptionListObserver aObserver); + + /** + * Unregister an observer. + * + * @param aObserver + * The nsIPartitioningExceptionListObserver object to unregister. + */ + void unregisterExceptionListObserver(in nsIPartitioningExceptionListObserver aObserver); +}; diff --git a/toolkit/components/antitracking/nsIPurgeTrackerService.idl b/toolkit/components/antitracking/nsIPurgeTrackerService.idl new file mode 100644 index 0000000000..01955da874 --- /dev/null +++ b/toolkit/components/antitracking/nsIPurgeTrackerService.idl @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +[scriptable, uuid(cd68d61e-9a44-402d-9671-838ac0872176)] +interface nsIPurgeTrackerService : nsISupports +{ + /** + * Purge cookies and associated data of sites which no longer have the user interaction permission. + */ + Promise purgeTrackingCookieJars(); +}; diff --git a/toolkit/components/antitracking/nsITrackingDBService.idl b/toolkit/components/antitracking/nsITrackingDBService.idl new file mode 100644 index 0000000000..12faad0ba4 --- /dev/null +++ b/toolkit/components/antitracking/nsITrackingDBService.idl @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +interface nsIPrincipal; +interface nsIAsyncInputStream; + +[scriptable, uuid(650934db-1939-4424-be26-6ffb0375424d)] +interface nsITrackingDBService : nsISupports +{ + /** + * Record entries from a content blocking log in the tracking database. + * This function is typically called at the end of the document lifecycle, + * since calling it multiple times results in multiple new entries. + * + * @param data a json string containing the content blocking log. + */ + void recordContentBlockingLog(in ACString data); + + /** + * Save new events in the content blocking database + * @param data a json string containing the content blocking log. + */ + Promise saveEvents(in AString data); + + /** + * Clear all content blocking database entries. + */ + Promise clearAll(); + + /** + * Clear all content blocking database entries added since the specified time. + * @param since a unix timestamp representing the number of milliseconds from + * Jan 1, 1970 00:00:00 UTC. + */ + Promise clearSince(in int64_t since); + + /** + * Fetch events from the content blocking database + * @param dateFrom a unix timestamp. + * @param dateTo a unix timestamp. + */ + Promise getEventsByDateRange(in int64_t dateFrom, in int64_t dateTo); + + /** + * Return a count of all tracking events. + */ + Promise sumAllEvents(); + + /** + * Return the earliest recorded date. + */ + Promise getEarliestRecordedDate(); + + const unsigned long OTHER_COOKIES_BLOCKED_ID = 0; + const unsigned long TRACKERS_ID = 1; + const unsigned long TRACKING_COOKIES_ID = 2; + const unsigned long CRYPTOMINERS_ID = 3; + const unsigned long FINGERPRINTERS_ID = 4; + const unsigned long SOCIAL_ID = 5; + const unsigned long SUSPICIOUS_FINGERPRINTERS_ID = 6; +}; diff --git a/toolkit/components/antitracking/nsIURLDecorationAnnotationsService.idl b/toolkit/components/antitracking/nsIURLDecorationAnnotationsService.idl new file mode 100644 index 0000000000..357b8baaa2 --- /dev/null +++ b/toolkit/components/antitracking/nsIURLDecorationAnnotationsService.idl @@ -0,0 +1,27 @@ +/* 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 "nsISupports.idl" + +/** + * A service that monitors updates to the anti-tracking URL decoration + * annotations from remote settings. + */ +[scriptable, uuid(937d0c66-6821-4e3f-9e04-50dbc2b2b476)] +interface nsIURLDecorationAnnotationsService : nsISupports +{ + /** + * Ensures that the list is updated and resolves the returned promise when + * the update is finished. + * + * The new list will be written to a space-separated list of tokens inside + * the following string preference: + * privacy.restrict3rdpartystorage.url_decorations + * + * This preference will be kept up to date with future list updates from + * the remote settings server. This preference cannot be modified by any + * external component and is managed by this service. + */ + Promise ensureUpdated(); +}; diff --git a/toolkit/components/antitracking/nsIURLQueryStringStripper.idl b/toolkit/components/antitracking/nsIURLQueryStringStripper.idl new file mode 100644 index 0000000000..372cc0f94a --- /dev/null +++ b/toolkit/components/antitracking/nsIURLQueryStringStripper.idl @@ -0,0 +1,35 @@ +/* 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 "nsISupports.idl" +#include "nsIURI.idl" + +/** + * nsIURLQueryStringStripper is responsible for stripping certain part of the + * query string of the given URI to address the bounce(redirect) tracking + * issues. It will strip every query parameter which matches the strip list + * defined in the pref 'privacy.query_stripping.strip_list'. Note that It's + * different from URLDecorationStripper which strips the entire query string + * from the referrer if there is a tracking query parameter present in the URI. + * + * TODO: Given that nsIURLQueryStringStripper and URLDecorationStripper are + * doing similar things. We could somehow combine these two modules into + * one. We will improve this in the future. + */ +[scriptable, uuid(6b42a890-2624-4560-99c4-b25380e8cd77)] +interface nsIURLQueryStringStripper : nsISupports { + + // Strip the query parameters that are in the strip list. Return the amount of + // query parameters that have been stripped. Returns 0 if no query parameters + // have been stripped or the feature is disabled. + uint32_t strip(in nsIURI aURI, in bool aIsPBM, out nsIURI aOutput); + + // Strip the query parameters that are in the stripForCopy/Share strip list. + // Returns ether the stripped URI or null if no query parameters have been stripped + // Thorws NS_ERROR_NOT_AVAILABLE if the feature is disabled. + [must_use] nsIURI stripForCopyOrShare(in nsIURI aURI); + + // Test-only method to get the current strip list. + ACString testGetStripList(); +}; diff --git a/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl b/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl new file mode 100644 index 0000000000..f746126c13 --- /dev/null +++ b/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl @@ -0,0 +1,128 @@ +/* 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 "nsISupports.idl" + +/** + * Observer for query stripping list updates. + */ +[scriptable, uuid(ef56ae12-b1bb-43e6-b1d8-16459cb98dfd)] +interface nsIURLQueryStrippingListObserver : nsISupports +{ + /** + * Called by nsIQueryStrippingListService when the list of query stripping + * changes and when the observer is first registered. Note that the lists + * could have duplicate entries because we would combine the lists from the + * pref and remote settings. + * + * @param aStripList + * A space-separated list of query parameters that will be stripped. + * @param aAllowList + * A comma-separated list of hosts (eTLD+1) that are exempt from query + * stripping. + */ + void onQueryStrippingListUpdate(in AString aStripList, in ACString aAllowList); + + /** + * Called by nsIQueryStrippingListService when the list of query stripping + * parameters for strip-on-share feature is updated and when the observer is first registered. + * + * @param aStripRules + * An Array of stringified strip rules. + * A stringified rule has the form of: + * "'queryParams': ['param1', 'param2', ...], 'topLevelSites': ['www.site.com', 'www.site.de', ...]" + */ + [implicit_jscontext] + void onStripOnShareUpdate(in Array<AString> aStripRules); +}; + +/** + * A service that monitors updates to the query stripping list from sources such + * as a local pref and remote settings updates. + */ +[scriptable, uuid(afff16f0-3fd2-4153-9ccd-c6d9abd879e4)] +interface nsIURLQueryStrippingListService : nsISupports +{ + /** + * Register a new observer to query stripping list updates. When the observer + * is registered it is called immediately once. Afterwards it will be called + * whenever the specified pref changes or when remote settings for + * partitioning updates. + * + * @param aObserver + * An nsIURLQueryStrippingListObserver object or function that + * will receive updates to the strip list and the allow list. Will be + * called immediately with the current list value. + */ + void registerAndRunObserver(in nsIURLQueryStrippingListObserver aObserver); + + + /** + * Register a new observer to strip-on-share stripping list updates + * (this is the strip-on-share list combined with the QPS list). + * When the observer is registered it is called immediately once. Afterwards it will be called + * when there is an remote settings update to the QPS strip list. + * + * @param aObserver + * An nsIURLQueryStrippingListObserver object or function that + * will receive updates to the strip list and the allow list. Will be + * called immediately with the current list value. + */ + void registerAndRunObserverStripOnShare(in nsIURLQueryStrippingListObserver aObserver); + + /** + * Unregister an observer. + * + * @param aObserver + * The nsIURLQueryStrippingListObserver object to unregister. + */ + void unregisterObserver(in nsIURLQueryStrippingListObserver aObserver); + + /** + * Unregister an observer for strip-on-share. + * + * @param aObserver + * The nsIURLQueryStrippingListObserver object to unregister. + */ + void unregisterStripOnShareObserver(in nsIURLQueryStrippingListObserver aObserver); + + /** + * Clear all Lists. + * + * Note that this is for testing purpose. + */ + void clearLists(); + + /** + * Test-only method used to wait for the list service to initialize fully. + * Resolves once the service has reached a fully disabled (false) or fully + * enabled state (true). + * May also be called when the service is already fully initialized or + * disabled, in this case it will resolve immediately. + */ + Promise testWaitForInit(); + + + /** + * Add new lists with different params + * + * Note that this is for testing purpose. + */ + Promise testSetList(in jsval testFile); + + + /** + * Check if Strip on Share observers are unregistered + * + * Note that this is for testing purpose. + */ + boolean testHasStripOnShareObservers(); + + /** + * Check if QPS observers are unregistered + * + * Note that this is for testing purpose. + */ + boolean testHasQPSObservers(); +}; diff --git a/toolkit/components/antitracking/test/browser/.eslintrc.js b/toolkit/components/antitracking/test/browser/.eslintrc.js new file mode 100644 index 0000000000..e57058ecb1 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + env: { + webextensions: true, + }, +}; diff --git a/toolkit/components/antitracking/test/browser/3rdParty.html b/toolkit/components/antitracking/test/browser/3rdParty.html new file mode 100644 index 0000000000..796f89d86f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdParty.html @@ -0,0 +1,51 @@ +<html> +<head> + <title>3rd party content!</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Here the 3rd party content!</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = function(e) { + let data = e.data; + if (data.includes("!!!")) { + // The data argument may be packed with information about whether we are on + // the allow list. In that case, extract that information and prepare it + // for our callbacks to access it. + let parts = data.split("!!!"); + // Only consider ourselves allow-listed when the cookie policy is set to + // 'block third-party trackers or 'block third-party trackers and partition + // third-party cookies', since otherwise we won't obtain storage access by + // default, which is what this data is used for in tests. + let cookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref("network.cookie.cookieBehavior.pbmode") + : SpecialPowers.Services.prefs.getIntPref("network.cookie.cookieBehavior"); + window.allowListed = + parts[0] === "true" && + (cookieBehavior == SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER || + cookieBehavior == SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); + data = parts[1]; + } + let runnableStr = `(() => {return (${data});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + runnable.call(this, /* Phase */ 1).then(_ => { + parent.postMessage({ type: "finish" }, "*"); + }); +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyOpen.html b/toolkit/components/antitracking/test/browser/3rdPartyOpen.html new file mode 100644 index 0000000000..1110834c0e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyOpen.html @@ -0,0 +1,16 @@ +<html> +<head> + <title>A popup!</title> +</head> +<body> +<h1>hi!</h1> +<script> + +if (opener) { + opener.postMessage("hello!", "*"); +} +window.close(); + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html b/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html new file mode 100644 index 0000000000..5afbb5d89b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html @@ -0,0 +1,17 @@ +<html> +<head> + <title>A popup!</title> +</head> +<body> +<h1>hi!</h1> +<script> + +SpecialPowers.wrap(document).userInteractionForTesting(); +if (window.location.search == "?messageme") { + window.opener.postMessage("done", "*"); +} +window.close(); + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyPartitioned.html b/toolkit/components/antitracking/test/browser/3rdPartyPartitioned.html new file mode 100644 index 0000000000..f97ed4791f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyPartitioned.html @@ -0,0 +1,29 @@ +<html> +<head> + <title>3rd party content!</title> +</head> +<body> +<h1>Here the 3rd party content!</h1> +<script> + +onmessage = async function(e) { + let cb = e.data.cb; + let runnableStr = `(() => {return (${cb});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + let variant = (new URL(location.href)).searchParams.get("variant"); + let win = this; + if (variant == "initial-aboutblank") { + let i = win.document.createElement("iframe"); + i.src = "about:blank"; + win.document.body.appendChild(i); + // override win to make it point to the initial about:blank window + win = i.contentWindow; + } + + let result = await runnable.call(this, win, e.data.value); + parent.postMessage(result, "*"); +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyRelay.html b/toolkit/components/antitracking/test/browser/3rdPartyRelay.html new file mode 100644 index 0000000000..64e713913b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyRelay.html @@ -0,0 +1,41 @@ +<html> +<head> + <title>Tracker</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Relay</h1> +<iframe></iframe> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = function(e) { + switch (e.data.type || "") { + case "finish": + case "ok": + case "info": + parent.postMessage(e.data, "*"); + break; + default: + let iframe = document.querySelector("iframe"); + iframe.contentWindow.postMessage(e.data, "*"); + break; + } +}; + +document.querySelector("iframe").src = location.search.substr(1); + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartySVG.html b/toolkit/components/antitracking/test/browser/3rdPartySVG.html new file mode 100644 index 0000000000..df791f355f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartySVG.html @@ -0,0 +1,20 @@ +<html> +<head> + <title>3rd party content!</title> + <style> + body { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="context-fill" d="M28.5,8.1c0-1.1-1-1.9-2.1-2.4V3.7c-0.2-0.2-0.3-0.3-0.6-0.3c-0.6,0-1.1,0.8-1.3,2.1c-0.2,0-0.3,0-0.5,0l0,0c0-0.2,0-0.3-0.2-0.5c-0.3-1.1-0.8-1.9-1.3-2.6C22,2.6,21.7,3.2,21.7,4L22,6.3c-0.3,0.2-0.6,0.3-1,0.6l-3.5,3.7l0,0c0,0-6.3-0.8-10.9,0.2c-0.6,0-1,0.2-1.1,0.3c-0.5,0.2-0.8,0.3-1.1,0.6c-1.1-0.8-2.2-2.1-3.2-4c0-0.3-0.5-0.5-0.8-0.5s-0.5,0.6-0.3,1c0.8,2.1,2.1,3.5,3.4,4.5c-0.5,0.5-0.8,1-1,1.6c0,0-0.3,2.2-0.3,5.5l1.4,8c0,1,0.8,1.8,1.9,1.8c1,0,1.9-0.8,1.9-1.8V23l0.5-1.3h8.8l0.8,1.3v4.7c0,1,0.8,1.8,1.9,1.8c1,0,1.6-0.6,1.8-1.4l0,0l1.9-9l0,0l2.1-6.4h3c3.4,0,3.7-2.9,3.7-2.9L28.5,8.1z"/></svg>'); + } + </style> +</head> +<body> +<h1>3rd party content with an SVG image background</h1> +<script> + +onload = function(e) { + parent.postMessage({ type: "finish" }, "*"); +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyStorage.html b/toolkit/components/antitracking/test/browser/3rdPartyStorage.html new file mode 100644 index 0000000000..749ead7c20 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyStorage.html @@ -0,0 +1,44 @@ +<html> +<head> + <title>3rd party content!</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Here the 3rd party content!</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = function(e) { + let data = e.data; + let runnableStr = `(() => {return (${data});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + + let win = window.open("3rdPartyStorageWO.html"); + win.onload = async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await runnable.call(this, this, win, false /* allowed */); + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + await runnable.call(this, this, win, true /* allowed */); + + win.close(); + parent.postMessage({ type: "finish" }, "*"); + }; +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyStorageWO.html b/toolkit/components/antitracking/test/browser/3rdPartyStorageWO.html new file mode 100644 index 0000000000..b04916103d --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyStorageWO.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>1st party content!</title> +</head> +<body> +<h1>Here the 1st party content!</h1> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyUI.html b/toolkit/components/antitracking/test/browser/3rdPartyUI.html new file mode 100644 index 0000000000..57693fbcbd --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyUI.html @@ -0,0 +1,32 @@ +<html> +<head> + <title>Tracker</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Tracker</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = function(e) { + let runnableStr = `(() => {return (${e.data.callback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + runnable.call(this, e.data.arg || /* Phase */ 3).then(_ => { + parent.postMessage({ type: "finish" }, "*"); + }); +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyWO.html b/toolkit/components/antitracking/test/browser/3rdPartyWO.html new file mode 100644 index 0000000000..7986b31063 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyWO.html @@ -0,0 +1,80 @@ +<html> +<head> + <title>Interact with me!</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Interact with me!</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = function(e) { + let runnableStr = `(() => {return (${e.data.blockingCallback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + runnable.call(this, /* Phase */ 2).then(_ => { + info("Let's do a window.open()"); + return new Promise(resolve => { + if (location.search == "?noopener") { + let features = "noopener"; + + window.open("3rdPartyOpen.html", undefined, features); + setTimeout(resolve, 1000); + } else { + onmessage = resolve; + + window.open("3rdPartyOpen.html"); + } + }); + }).then(_ => { + info("The popup has been dismissed!"); + // First time storage access should not be granted because the tracker has + // not had user interaction yet. + let runnableStr = `(() => {return (${e.data.blockingCallback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + return runnable.call(this, /* Phase */ 2); + }).then(_ => { + info("Let's interact with the tracker"); + return new Promise(resolve => { + onmessage = resolve; + + window.open("3rdPartyOpenUI.html?messageme"); + }); + }).then(_ => { + info("Let's do another window.open()"); + return new Promise(resolve => { + if (location.search == "?noopener") { + let features = "noopener"; + + window.open("3rdPartyOpen.html", undefined, features); + setTimeout(resolve, 1000); + } else { + onmessage = resolve; + + window.open("3rdPartyOpen.html"); + } + }); + }).then(_ => { + // This time the tracker must have been able to obtain first-party storage + // access because it has had user interaction before. + let runnableStr = `(() => {return (${e.data.nonBlockingCallback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + return runnable.call(this, /* Phase */ 2); + }).then(_ => { + parent.postMessage({ type: "finish" }, "*"); + }); +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyWorker.html b/toolkit/components/antitracking/test/browser/3rdPartyWorker.html new file mode 100644 index 0000000000..e79a992660 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyWorker.html @@ -0,0 +1,55 @@ +<html> +<head> + <title>Tracker</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Tracker</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function workerCode() { + onmessage = e => { + try { + indexedDB.open("test", "1"); + postMessage(true); + } catch (e) { + postMessage(false); + } + }; +} + +var worker; +function createWorker() { + let blob = new Blob([workerCode.toString() + "; workerCode();"]); + let blobURL = URL.createObjectURL(blob); + info("Blob created"); + + worker = new Worker(blobURL); + info("Worker created"); +} + +onmessage = function(e) { + let runnableStr = `(() => {return (${e.data.callback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + runnable.call(this, e.data.arg || /* Phase */ 3).then(_ => { + parent.postMessage({ type: "finish" }, "*"); + }); +}; + +createWorker(); + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/antitracking_head.js b/toolkit/components/antitracking/test/browser/antitracking_head.js new file mode 100644 index 0000000000..52871b60c2 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/antitracking_head.js @@ -0,0 +1,1392 @@ +/* vim: set ts=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/. */ + +/* import-globals-from head.js */ + +"use strict"; + +var gFeatures = undefined; +var gTestTrackersCleanedUp = false; +var gTestTrackersCleanupRegistered = false; + +/** + * Force garbage collection. + */ +function forceGC() { + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); +} + +this.AntiTracking = { + runTestInNormalAndPrivateMode( + name, + callbackTracking, + callbackNonTracking, + cleanupFunction, + extraPrefs, + windowOpenTest = true, + userInteractionTest = true, + expectedBlockingNotifications = Ci.nsIWebProgressListener + .STATE_COOKIES_BLOCKED_TRACKER, + iframeSandbox = null, + accessRemoval = null, + callbackAfterRemoval = null, + iframeAllow = null + ) { + // Normal mode + this.runTest( + name, + callbackTracking, + callbackNonTracking, + cleanupFunction, + extraPrefs, + windowOpenTest, + userInteractionTest, + expectedBlockingNotifications, + false, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + iframeAllow + ); + + // Private mode + this.runTest( + name, + callbackTracking, + callbackNonTracking, + cleanupFunction, + extraPrefs, + windowOpenTest, + userInteractionTest, + expectedBlockingNotifications, + true, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + iframeAllow + ); + }, + + runTest( + name, + callbackTracking, + callbackNonTracking, + cleanupFunction, + extraPrefs, + windowOpenTest = true, + userInteractionTest = true, + expectedBlockingNotifications = Ci.nsIWebProgressListener + .STATE_COOKIES_BLOCKED_TRACKER, + runInPrivateWindow = false, + iframeSandbox = null, + accessRemoval = null, + callbackAfterRemoval = null, + iframeAllow = null + ) { + let runExtraTests = true; + let options = {}; + if (typeof callbackNonTracking == "object" && !!callbackNonTracking) { + options.callback = callbackNonTracking.callback; + runExtraTests = callbackNonTracking.runExtraTests; + if ("cookieBehavior" in callbackNonTracking) { + options.cookieBehavior = callbackNonTracking.cookieBehavior; + } else { + options.cookieBehavior = BEHAVIOR_ACCEPT; + } + if ("expectedBlockingNotifications" in callbackNonTracking) { + options.expectedBlockingNotifications = + callbackNonTracking.expectedBlockingNotifications; + } else { + options.expectedBlockingNotifications = 0; + } + if ("blockingByAllowList" in callbackNonTracking) { + options.blockingByAllowList = callbackNonTracking.blockingByAllowList; + if (options.blockingByAllowList) { + // If we're on the allow list, there won't be any blocking! + options.expectedBlockingNotifications = 0; + } + } else { + options.blockingByAllowList = false; + } + callbackNonTracking = options.callback; + options.accessRemoval = null; + options.callbackAfterRemoval = null; + } + + // Here we want to test that a 3rd party context is simply blocked. + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: callbackTracking, + extraPrefs, + expectedBlockingNotifications, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + iframeAllow, + }); + this._createCleanupTask(cleanupFunction); + + if (callbackNonTracking) { + // Phase 1: Here we want to test that a 3rd party context is not blocked if pref is off. + if (runExtraTests) { + // There are five ways in which the third-party context may not be blocked: + // * If the cookieBehavior pref causes it to not be blocked. + // * If the contentBlocking pref causes it to not be blocked. + // * If both of these prefs cause it to not be blocked. + // * If the top-level page is on the content blocking allow list. + // * If the contentBlocking third-party cookies UI pref is off, the allow list will be ignored. + // All of these cases are tested here. + this._createTask({ + name, + cookieBehavior: BEHAVIOR_ACCEPT, + allowList: false, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + iframeAllow, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_ACCEPT, + allowList: true, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + iframeAllow, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT, + allowList: false, + callback: callbackTracking, + extraPrefs, + expectedBlockingNotifications: expectedBlockingNotifications + ? Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL + : 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + iframeAllow, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_LIMIT_FOREIGN, + allowList: true, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + iframeAllow, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name: name + " reject foreign with exception", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: true, + callback: callbackNonTracking, + extraPrefs: [...(extraPrefs || [])], + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + iframeAllow, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: false, + callback: callbackTracking, + extraPrefs: [...(extraPrefs || [])], + expectedBlockingNotifications: expectedBlockingNotifications + ? Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN + : 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + iframeAllow, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: true, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + iframeAllow, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: false, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + iframeAllow, + }); + this._createCleanupTask(cleanupFunction); + } else { + // This is only used for imageCacheWorker.js tests + this._createTask({ + name, + cookieBehavior: options.cookieBehavior, + allowList: options.blockingByAllowList, + callback: options.callback, + extraPrefs, + expectedBlockingNotifications: options.expectedBlockingNotifications, + runInPrivateWindow, + iframeSandbox, + accessRemoval: options.accessRemoval, + callbackAfterRemoval: options.callbackAfterRemoval, + iframeAllow, + }); + this._createCleanupTask(cleanupFunction); + } + + // Phase 2: Here we want to test that a third-party context doesn't + // get blocked with when the same origin is opened through window.open(). + if (windowOpenTest) { + this._createWindowOpenTask( + name, + BEHAVIOR_REJECT_TRACKER, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + false, + extraPrefs, + iframeAllow + ); + this._createCleanupTask(cleanupFunction); + + // Now, check if it works for nested iframes. + this._createWindowOpenTask( + name, + BEHAVIOR_REJECT_TRACKER, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + true, + extraPrefs, + iframeAllow + ); + this._createCleanupTask(cleanupFunction); + } + + // Phase 3: Here we want to test that a third-party context doesn't + // get blocked with user interaction present + if (userInteractionTest) { + this._createUserInteractionTask( + name, + BEHAVIOR_REJECT_TRACKER, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + false, + extraPrefs, + iframeAllow + ); + this._createCleanupTask(cleanupFunction); + + // Now, check if it works for nested iframes. + this._createUserInteractionTask( + name, + BEHAVIOR_REJECT_TRACKER, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + true, + extraPrefs, + iframeAllow + ); + this._createCleanupTask(cleanupFunction); + } + } + }, + + _waitObserver(targetTopic, expectedCount) { + let cnt = 0; + + return new Promise(resolve => { + Services.obs.addObserver(function observer(subject, topic, data) { + if (topic != targetTopic) { + return; + } + cnt++; + + if (cnt != expectedCount) { + return; + } + + Services.obs.removeObserver(observer, targetTopic); + resolve(); + }, targetTopic); + }); + }, + + _waitUserInteractionPerm() { + return this._waitObserver( + "antitracking-test-user-interaction-perm-added", + 1 + ); + }, + + _waitStorageAccessPerm(expectedCount) { + return this._waitObserver( + "antitracking-test-storage-access-perm-added", + expectedCount + ); + }, + + async interactWithTracker() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: TEST_3RD_PARTY_PAGE }, + async function (browser) { + info("Let's interact with the tracker"); + + await SpecialPowers.spawn(browser, [], async function () { + SpecialPowers.wrap(content.document).userInteractionForTesting(); + }); + } + ); + await BrowserTestUtils.closeWindow(win); + }, + + async _setupTest(win, cookieBehavior, runInPrivateWindow, extraPrefs) { + await SpecialPowers.flushPrefEnv(); + + await setCookieBehaviorPref(cookieBehavior, runInPrivateWindow); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["dom.security.https_first_pbm", false], + [ + "privacy.trackingprotection.annotate_channels", + cookieBehavior != BEHAVIOR_ACCEPT, + ], + ["privacy.restrict3rdpartystorage.console.lazy", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ["privacy.antitracking.testing", true], + ], + }); + + if (extraPrefs && Array.isArray(extraPrefs) && extraPrefs.length) { + await SpecialPowers.pushPrefEnv({ set: extraPrefs }); + + let enableWebcompat = Services.prefs.getBoolPref( + "privacy.antitracking.enableWebcompat" + ); + + // If the skip list is disabled by pref, it will always return an empty + // list. + if (enableWebcompat) { + for (let item of extraPrefs) { + // When setting up exception URLs, we need to wait to ensure our prefs + // actually take effect. In order to do this, we set up a exception + // list observer and wait until it calls us back. + if (item[0] == "urlclassifier.trackingAnnotationSkipURLs") { + info("Waiting for the exception list service to initialize..."); + let classifier = Cc[ + "@mozilla.org/url-classifier/dbservice;1" + ].getService(Ci.nsIURIClassifier); + let feature = classifier.getFeatureByName("tracking-annotation"); + await TestUtils.waitForCondition(() => { + for (let x of item[1].toLowerCase().split(",")) { + if (feature.exceptionHostList.split(",").includes(x)) { + return true; + } + } + return false; + }, "Exception list service initialized"); + break; + } + } + } + } + + await UrlClassifierTestUtils.addTestTrackers(); + if (!gTestTrackersCleanupRegistered) { + registerCleanupFunction(_ => { + if (gTestTrackersCleanedUp) { + return; + } + UrlClassifierTestUtils.cleanupTestTrackers(); + gTestTrackersCleanedUp = true; + }); + gTestTrackersCleanupRegistered = true; + } + }, + + _createTask(options) { + add_task(async function () { + info( + "Starting " + + (options.cookieBehavior != BEHAVIOR_ACCEPT + ? "blocking" + : "non-blocking") + + " cookieBehavior (" + + options.cookieBehavior + + ") with" + + (options.allowList ? "" : "out") + + " allow list test " + + options.name + + " running in a " + + (options.runInPrivateWindow ? "private" : "normal") + + " window " + + " with iframe sandbox set to " + + options.iframeSandbox + + " and iframe allow set to " + + options.iframeAllow + + " and access removal set to " + + options.accessRemoval + + (typeof options.thirdPartyPage == "string" + ? " and third party page set to " + options.thirdPartyPage + : "") + + (typeof options.topPage == "string" + ? " and top page set to " + options.topPage + : "") + ); + + is( + !!options.callbackAfterRemoval, + !!options.accessRemoval, + "callbackAfterRemoval must be passed when accessRemoval is non-null" + ); + + let win = window; + if (options.runInPrivateWindow) { + win = OpenBrowserWindow({ private: true }); + await TestUtils.topicObserved("browser-delayed-startup-finished"); + } + + await AntiTracking._setupTest( + win, + options.cookieBehavior, + options.runInPrivateWindow, + options.extraPrefs + ); + + let topPage; + if (typeof options.topPage == "string") { + topPage = options.topPage; + } else { + topPage = TEST_TOP_PAGE; + } + + let thirdPartyPage, thirdPartyDomainURI; + if (typeof options.thirdPartyPage == "string") { + thirdPartyPage = options.thirdPartyPage; + let url = new URL(thirdPartyPage); + thirdPartyDomainURI = Services.io.newURI(url.origin); + } else { + thirdPartyPage = TEST_3RD_PARTY_PAGE; + thirdPartyDomainURI = Services.io.newURI(TEST_3RD_PARTY_DOMAIN); + } + + // It's possible that the third-party domain has been exceptionlisted + // through extraPrefs, so let's try annotating it here and adjust our + // blocking expectations as necessary. + if ( + options.expectedBlockingNotifications == + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER + ) { + if ( + !(await AntiTracking._isThirdPartyPageClassifiedAsTracker( + topPage, + thirdPartyDomainURI + )) + ) { + options.expectedBlockingNotifications = 0; + } + } + + let cookieBlocked = 0; + let { expectedBlockingNotifications } = options; + if (!Array.isArray(expectedBlockingNotifications)) { + expectedBlockingNotifications = [expectedBlockingNotifications]; + } + let listener = { + onContentBlockingEvent(webProgress, request, event) { + for (const notification of expectedBlockingNotifications) { + if (event & notification) { + ++cookieBlocked; + } + } + }, + }; + function prepareTestEnvironmentOnPage() { + win.gBrowser.addProgressListener(listener); + + Services.console.reset(); + } + + if (!options.allowList) { + prepareTestEnvironmentOnPage(); + } + + let consoleWarningPromise; + + if (options.expectedBlockingNotifications) { + consoleWarningPromise = new Promise(resolve => { + let consoleListener = { + observe(msg) { + if ( + msg + .QueryInterface(Ci.nsIScriptError) + .category.startsWith("cookieBlocked") + ) { + Services.console.unregisterListener(consoleListener); + resolve(); + } + }, + }; + + Services.console.registerListener(consoleListener); + }); + } else { + consoleWarningPromise = Promise.resolve(); + } + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(win.gBrowser, topPage); + win.gBrowser.selectedTab = tab; + + let browser = win.gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Check the cookieJarSettings of the browser object"); + ok( + browser.cookieJarSettings, + "The browser object has the cookieJarSettings." + ); + is( + browser.cookieJarSettings.cookieBehavior, + options.cookieBehavior, + "The cookieJarSettings has the correct cookieBehavior" + ); + + if (options.allowList) { + info("Disabling content blocking for this page"); + win.gProtectionsHandler.disableForCurrentPage(); + + prepareTestEnvironmentOnPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + } + + info("Creating a 3rd party content"); + let doAccessRemovalChecks = + typeof options.accessRemoval == "string" && + options.cookieBehavior == BEHAVIOR_REJECT_TRACKER && + !options.allowList; + await SpecialPowers.spawn( + browser, + [ + { + page: thirdPartyPage, + nextPage: TEST_4TH_PARTY_PAGE, + callback: options.callback.toString(), + callbackAfterRemoval: options.callbackAfterRemoval + ? options.callbackAfterRemoval.toString() + : null, + accessRemoval: options.accessRemoval, + iframeSandbox: options.iframeSandbox, + iframeAllow: options.iframeAllow, + allowList: options.allowList, + doAccessRemovalChecks, + }, + ], + async function (obj) { + let id = "id" + Math.random(); + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.id = id; + ifr.onload = function () { + info("Sending code to the 3rd party content"); + let callback = obj.allowList + "!!!" + obj.callback; + ifr.contentWindow.postMessage(callback, "*"); + }; + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + if (typeof obj.iframeAllow == "string") { + ifr.setAttribute("allow", obj.iframeAllow); + } + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + + if (obj.doAccessRemovalChecks) { + info(`Running after removal checks (${obj.accessRemoval})`); + switch (obj.accessRemoval) { + case "navigate-subframe": + await new content.Promise(resolve => { + let ifr = content.document.getElementById(id); + let oldWindow = ifr.contentWindow; + ifr.onload = function () { + info("Sending code to the old 3rd party content"); + oldWindow.postMessage(obj.callbackAfterRemoval, "*"); + }; + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + if (typeof obj.iframeAllow == "string") { + ifr.setAttribute("allow", obj.iframeAllow); + } + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + ifr.src = obj.nextPage; + }); + break; + case "navigate-topframe": + // pass-through + break; + default: + ok( + false, + "Unexpected accessRemoval code passed: " + obj.accessRemoval + ); + break; + } + } + } + ); + + if ( + doAccessRemovalChecks && + options.accessRemoval == "navigate-topframe" + ) { + BrowserTestUtils.startLoadingURIString(browser, TEST_4TH_PARTY_PAGE); + await BrowserTestUtils.browserLoaded(browser); + + let pageshow = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + gBrowser.goBack(); + await pageshow; + + await SpecialPowers.spawn( + browser, + [ + { + page: thirdPartyPage, + callbackAfterRemoval: options.callbackAfterRemoval + ? options.callbackAfterRemoval.toString() + : null, + iframeSandbox: options.iframeSandbox, + iframeAllow: options.iframeAllow, + }, + ], + async function (obj) { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info( + "Sending code to the 3rd party content to verify accessRemoval" + ); + ifr.contentWindow.postMessage(obj.callbackAfterRemoval, "*"); + }; + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + if (typeof obj.iframeAllow == "string") { + ifr.setAttribute("allow", obj.iframeAllow); + } + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + } + ); + } + + // Wait until the message appears on the console. + await consoleWarningPromise; + + let allMessages = Services.console.getMessageArray().filter(msg => { + try { + // Select all messages that the anti-tracking backend could generate. + return msg + .QueryInterface(Ci.nsIScriptError) + .category.startsWith("cookieBlocked"); + } catch (e) { + return false; + } + }); + // When changing this list, please make sure to update the corresponding + // code in ReportBlockingToConsole(). + let expectedCategories = []; + let rawExpectedCategories = options.expectedBlockingNotifications; + if (!Array.isArray(rawExpectedCategories)) { + // if given a single value to match, expect each message to match it + rawExpectedCategories = Array(allMessages.length).fill( + rawExpectedCategories + ); + } + for (let category of rawExpectedCategories) { + switch (category) { + case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION: + expectedCategories.push("cookieBlockedPermission"); + break; + case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER: + expectedCategories.push("cookieBlockedTracker"); + break; + case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL: + expectedCategories.push("cookieBlockedAll"); + break; + case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN: + expectedCategories.push("cookieBlockedForeign"); + break; + } + } + + if (!expectedCategories.length) { + is(allMessages.length, 0, "No console messages should be generated"); + } else { + ok(!!allMessages.length, "Some console message should be generated"); + if (options.errorMessageDomains) { + is( + allMessages.length, + options.errorMessageDomains.length, + "Enough items provided in errorMessageDomains" + ); + } + } + let index = 0; + for (let msg of allMessages) { + is( + msg.category, + expectedCategories[index], + `Message ${index} should be of expected category` + ); + + if (options.errorMessageDomains) { + ok( + msg.errorMessage.includes(options.errorMessageDomains[index]), + `Error message domain ${options.errorMessageDomains[index]} (${index}) found in "${msg.errorMessage}"` + ); + index++; + } + } + + if (options.allowList) { + info("Enabling content blocking for this page"); + win.gProtectionsHandler.enableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + } + + win.gBrowser.removeProgressListener(listener); + + if (!!cookieBlocked != !!options.expectedBlockingNotifications) { + ok(false, JSON.stringify(cookieBlocked)); + ok(false, JSON.stringify(options.expectedBlockingNotifications)); + } + is( + !!cookieBlocked, + !!options.expectedBlockingNotifications, + "Checking cookie blocking notifications" + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (options.runInPrivateWindow) { + win.close(); + } + }); + }, + + _createCleanupTask(cleanupFunction) { + add_task(async function () { + info("Cleaning up."); + if (cleanupFunction) { + await cleanupFunction(); + } + + // While running these tests we typically do not have enough idle time to do + // GC reliably, so force it here. + forceGC(); + }); + }, + + _createWindowOpenTask( + name, + cookieBehavior, + blockingCallback, + nonBlockingCallback, + runInPrivateWindow, + iframeSandbox, + testInSubIFrame, + extraPrefs, + iframeAllow + ) { + add_task(async function () { + info( + `Starting window-open${ + testInSubIFrame ? " sub iframe" : "" + } test ${name}` + ); + + let win = window; + if (runInPrivateWindow) { + win = OpenBrowserWindow({ private: true }); + await TestUtils.topicObserved("browser-delayed-startup-finished"); + } + + await AntiTracking._setupTest( + win, + cookieBehavior, + runInPrivateWindow, + extraPrefs + ); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE); + win.gBrowser.selectedTab = tab; + + let browser = win.gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Create a first-level iframe to test sub iframes."); + if (testInSubIFrame) { + let iframeBrowsingContext = await SpecialPowers.spawn( + browser, + [{ page: TEST_IFRAME_PAGE }], + async function (obj) { + // Add an iframe. + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + return ifr.browsingContext; + } + ); + + browser = iframeBrowsingContext; + } + + let pageURL = TEST_3RD_PARTY_PAGE_WO; + if (gFeatures == "noopener") { + pageURL += "?noopener"; + } + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: pageURL, + blockingCallback: blockingCallback.toString(), + nonBlockingCallback: nonBlockingCallback.toString(), + iframeSandbox, + iframeAllow, + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj, "*"); + }; + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + if (typeof obj.iframeAllow == "string") { + ifr.setAttribute("allow", obj.iframeAllow); + } + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (runInPrivateWindow) { + win.close(); + } + }); + }, + + _createUserInteractionTask( + name, + cookieBehavior, + blockingCallback, + nonBlockingCallback, + runInPrivateWindow, + iframeSandbox, + testInSubIFrame, + extraPrefs, + iframeAllow + ) { + add_task(async function () { + info( + `Starting user-interaction${ + testInSubIFrame ? " sub iframe" : "" + } test ${name}` + ); + + let win = window; + if (runInPrivateWindow) { + win = OpenBrowserWindow({ private: true }); + await TestUtils.topicObserved("browser-delayed-startup-finished"); + } + + await AntiTracking._setupTest( + win, + cookieBehavior, + runInPrivateWindow, + extraPrefs + ); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE); + win.gBrowser.selectedTab = tab; + + let browser = win.gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + if (testInSubIFrame) { + info("Create a first-level iframe to test sub iframes."); + let iframeBrowsingContext = await SpecialPowers.spawn( + browser, + [{ page: TEST_IFRAME_PAGE }], + async function (obj) { + // Add an iframe. + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + return ifr.browsingContext; + } + ); + + browser = iframeBrowsingContext; + } + + // The following test will open an popup which interacts with the tracker + // page. So there will be an user-interaction permission added. We wait + // it explicitly. + let promiseUIPerm = AntiTracking._waitUserInteractionPerm(); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + blockingCallback: blockingCallback.toString(), + iframeSandbox, + iframeAllow, + }, + ], + async function (obj) { + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + if (typeof obj.iframeAllow == "string") { + ifr.setAttribute("allow", obj.iframeAllow); + } + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info( + "The 3rd party content should not have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage( + { callback: obj.blockingCallback }, + "*" + ); + }); + + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + if (aTopic == "domwindowclosed") { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + info("Opening a window from the iframe."); + SpecialPowers.spawn( + ifr, + [{ popup: obj.popup }], + async function (obj) { + content.open(obj.popup); + } + ); + + info("Let's wait for the window to be closed"); + await windowClosed; + + info( + "First time, the 3rd party content should not have access to first party storage " + + "because the tracker did not have user interaction" + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage( + { callback: obj.blockingCallback }, + "*" + ); + }); + } + ); + + // We wait until the user-interaction permission is added. + await promiseUIPerm; + + // We also need to wait the user-interaction permission here. + promiseUIPerm = AntiTracking._waitUserInteractionPerm(); + await AntiTracking.interactWithTracker(); + await promiseUIPerm; + + // Following test will also open an popup to interact with the page. We + // need to explicitly wait it. Without waiting it, it could be added after + // we clear up the test and interfere the next test. + promiseUIPerm = AntiTracking._waitUserInteractionPerm(); + + // We have to wait until the storage access permission is added. This has + // the same reason as above user-interaction permission. Note that there + // will be two storage access permission added due to the way how we + // trigger the heuristic. The first permission is added due to 'Opener' + // heuristic and the second one is due to 'Opener after user interaction'. + // The page we use to trigger the heuristic will trigger both heuristic, + // so we have to wait 2 permissions. + let promiseStorageAccessPerm = AntiTracking._waitStorageAccessPerm(2); + + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + nonBlockingCallback: nonBlockingCallback.toString(), + iframeSandbox, + iframeAllow, + }, + ], + async function (obj) { + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + if (typeof obj.iframeAllow == "string") { + ifr.setAttribute("allow", obj.iframeAllow); + } + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + if (aTopic == "domwindowclosed") { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + info("Opening a window from the iframe."); + SpecialPowers.spawn( + ifr, + [{ popup: obj.popup }], + async function (obj) { + content.open(obj.popup); + } + ); + + info("Let's wait for the window to be closed"); + await windowClosed; + + info( + "The 3rd party content should now have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage( + { callback: obj.nonBlockingCallback }, + "*" + ); + }); + } + ); + + // Explicitly wait the user-interaction and storage access permission + // before we do the cleanup. + await promiseUIPerm; + await promiseStorageAccessPerm; + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (runInPrivateWindow) { + win.close(); + } + }); + }, + + async _isThirdPartyPageClassifiedAsTracker(topPage, thirdPartyDomainURI) { + let channel; + await new Promise((resolve, reject) => { + channel = NetUtil.newChannel({ + uri: thirdPartyDomainURI, + loadingPrincipal: Services.scriptSecurityManager.createContentPrincipal( + thirdPartyDomainURI, + {} + ), + securityFlags: + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + channel + .QueryInterface(Ci.nsIHttpChannelInternal) + .setTopWindowURIIfUnknown(Services.io.newURI(topPage)); + + function Listener() {} + Listener.prototype = { + onStartRequest(request) {}, + onDataAvailable(request, stream, off, cnt) { + // Consume the data to prevent hitting the assertion. + NetUtil.readInputStreamToString(stream, cnt); + }, + onStopRequest(request, st) { + let status = request.QueryInterface(Ci.nsIHttpChannel).responseStatus; + if (status == 200) { + resolve(); + } else { + reject(); + } + }, + }; + let listener = new Listener(); + channel.asyncOpen(listener); + }); + + return !!( + channel.QueryInterface(Ci.nsIClassifiedChannel).classificationFlags & + Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING + ); + }, +}; diff --git a/toolkit/components/antitracking/test/browser/blobPartitionPage.html b/toolkit/components/antitracking/test/browser/blobPartitionPage.html new file mode 100644 index 0000000000..d0dd156bc5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/blobPartitionPage.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> + <head> + <title>Blob URL Partitioning Test</title> + </head> + <body> + <script> + onmessage = e => { + + fetch(e.data) + .then(response => { + if (!response.ok) { + throw new Error(); + } + return response.text(); + }) + .then(text => { + parent.postMessage(text, "*"); + }) + .catch(error => { + parent.postMessage("error", "*"); + }); + }; + </script> + </body> +</html> diff --git a/toolkit/components/antitracking/test/browser/browser-blocking.toml b/toolkit/components/antitracking/test/browser/browser-blocking.toml new file mode 100644 index 0000000000..4a0aa8f44d --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser-blocking.toml @@ -0,0 +1,89 @@ +[DEFAULT] +skip-if = ["os == 'linux' && (asan || tsan)"] # bug 1662229 - task exception +prefs = [ + # Disable the Storage Access API prompts for all of the tests in this directory + "dom.storage_access.prompt.testing=true", + "dom.storage_access.prompt.testing.allow=true", + "dom.testing.sync-content-blocking-notifications=true", + # Enable the window.open() heuristics globally in this directory + "privacy.restrict3rdpartystorage.heuristic.window_open=true", + "privacy.restrict3rdpartystorage.heuristic.opened_window_after_interaction=true", + # Disable https-first because of explicit http/https testing + "dom.security.https_first=false", +] + +support-files = [ + "head.js", + "antitracking_head.js", + "iframe.html", + "image.sjs", + "page.html", + "3rdParty.html", + "3rdPartyRelay.html", + "3rdPartySVG.html", + "3rdPartyUI.html", + "3rdPartyWO.html", + "3rdPartyWorker.html", + "3rdPartyOpen.html", + "3rdPartyOpenUI.html", + "empty.js", + "popup.html", + "server.sjs", + "storageAccessAPIHelpers.js", + "3rdPartyStorage.html", + "3rdPartyStorageWO.html", + "3rdPartyPartitioned.html", + "localStorage.html", + "!/browser/modules/test/browser/head.js", + "!/browser/base/content/test/general/head.js", + "!/browser/base/content/test/protectionsUI/cookieServer.sjs", + "!/browser/base/content/test/protectionsUI/trackingPage.html", + "!/browser/base/content/test/protectionsUI/trackingAPI.js", +] + +["browser_blockingCookies.js"] +skip-if = ["socketprocess_networking"] + +["browser_blockingDOMCache.js"] + +["browser_blockingDOMCacheAlwaysPartition.js"] +skip-if = ["socketprocess_networking"] + +["browser_blockingDOMCacheAlwaysPartitionSAA.js"] +skip-if = ["socketprocess_networking"] + +["browser_blockingDOMCacheSAA.js"] +skip-if = ["socketprocess_networking"] + +["browser_blockingIndexedDb.js"] +skip-if = ["os == 'linux' && socketprocess_networking"] + +["browser_blockingIndexedDbInWorkers.js"] +skip-if = ["os == 'linux' && socketprocess_networking"] + +["browser_blockingIndexedDbInWorkers2.js"] + +["browser_blockingLocalStorage.js"] +skip-if = ["os == 'linux' && socketprocess_networking"] + +["browser_blockingMessaging.js"] +skip-if = ["os == 'linux' && debug"] #bug 1627094 + +["browser_blockingNoOpener.js"] + +["browser_blockingServiceWorkers.js"] + +["browser_blockingServiceWorkersStorageAccessAPI.js"] + +["browser_blockingSessionStorage.js"] + +["browser_blockingSharedWorkers.js"] +skip-if = ["os == 'linux' && socketprocess_networking"] + +["browser_contentBlockingAllowListPrincipal.js"] +support-files = [ + "sandboxed.html", + "sandboxed.html^headers^", +] + +["browser_contentBlockingTelemetry.js"] diff --git a/toolkit/components/antitracking/test/browser/browser.toml b/toolkit/components/antitracking/test/browser/browser.toml new file mode 100644 index 0000000000..9d0874ed3a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser.toml @@ -0,0 +1,339 @@ +[DEFAULT] +skip-if = ["os == 'linux' && (asan || tsan)"] # bug 1662229 - task exception +prefs = [ + "dom.storage_access.prompt.testing=true", # Disable the Storage Access API prompts for all of the tests in this directory + "dom.storage_access.prompt.testing.allow=true", + "dom.testing.sync-content-blocking-notifications=true", + "dom.storage_access.frame_only=true", # Enable frame-only Storage Access API for all of the tests in this directory + "privacy.restrict3rdpartystorage.heuristic.window_open=true", # Enable the window.open() heuristics globally in this directory + "privacy.restrict3rdpartystorage.heuristic.opened_window_after_interaction=true", + "network.cookie.sameSite.laxByDefault=false", # Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + "dom.security.https_first=false", # Disable https-first because of explicit http/https testing + "dom.storage_access.dont_grant_insecure_contexts=true", # Enable constraining storage access api to secure contexts +] +support-files = [ + "container.html", + "container2.html", + "embedder.html", + "embedder2.html", + "head.js", + "antitracking_head.js", + "dynamicfpi_head.js", + "partitionedstorage_head.js", + "storage_access_head.js", + "cookiesCORS.sjs", + "iframe.html", + "image.sjs", + "imageCacheWorker.js", + "page.html", + "3rdParty.html", + "3rdPartyRelay.html", + "3rdPartySVG.html", + "3rdPartyUI.html", + "3rdPartyWO.html", + "3rdPartyWorker.html", + "3rdPartyOpen.html", + "3rdPartyOpenUI.html", + "empty.js", + "empty-altsvc.js", + "empty-altsvc.js^headers^", + "empty.html", + "file_iframe_document_open.html", + "file_localStorage.html", + "popup.html", + "redirect.sjs", + "server.sjs", + "storageAccessAPIHelpers.js", + "3rdPartyStorage.html", + "3rdPartyStorageWO.html", + "3rdPartyPartitioned.html", + "localStorage.html", + "blobPartitionPage.html", + "raptor.jpg", + "!/browser/modules/test/browser/head.js", + "!/browser/base/content/test/general/head.js", + "!/browser/base/content/test/protectionsUI/cookieServer.sjs", + "!/browser/base/content/test/protectionsUI/trackingPage.html", + "!/browser/base/content/test/protectionsUI/trackingAPI.js", + "!/toolkit/content/tests/browser/common/mockTransfer.js", +] + +["browser_AntiTrackingETPHeuristic.js"] + +["browser_PBMCookieBehavior.js"] + +["browser_aboutblank.js"] + +["browser_addonHostPermissionIgnoredInTP.js"] + +["browser_allowListNotifications.js"] + +["browser_allowListNotifications_alwaysPartition.js"] +support-files = ["subResources.sjs"] + +["browser_allowListSeparationInPrivateAndNormalWindows.js"] +skip-if = ["os == 'mac' && !debug"] # Bug 1503778, 1577362 + +["browser_allowPermissionForTracker.js"] + +["browser_backgroundImageAssertion.js"] + +["browser_cookieBetweenTabs.js"] + +["browser_denyPermissionForTracker.js"] + +["browser_doublyNestedTracker.js"] + +["browser_emailtracking.js"] + +["browser_existingCookiesForSubresources.js"] + +["browser_fileUrl.js"] + +["browser_firstPartyCookieRejectionHonoursAllowList.js"] + +["browser_hasStorageAccess.js"] + +["browser_hasStorageAccess_alwaysPartition.js"] + +["browser_iframe_document_open.js"] + +["browser_imageCache4.js"] + +["browser_imageCache8.js"] + +["browser_localStorageEvents.js"] + +["browser_onBeforeRequestNotificationForTrackingResources.js"] + +["browser_onModifyRequestNotificationForTrackingResources.js"] + +["browser_partitionedClearSiteDataHeader.js"] +support-files = ["clearSiteData.sjs"] + +["browser_partitionedConsoleMessage.js"] + +["browser_partitionedCookies.js"] +support-files = ["cookies.sjs"] + +["browser_partitionedDOMCache.js"] + +["browser_partitionedIndexedDB.js"] + +["browser_partitionedLocalStorage.js"] + +["browser_partitionedLocalStorage_events.js"] +support-files = ["localStorageEvents.html"] + +["browser_partitionedLockManager.js"] + +["browser_partitionedMessaging.js"] +skip-if = ["true"] #Bug 1588241 + +["browser_partitionedServiceWorkers.js"] +support-files = [ + "dedicatedWorker.js", + "matchAll.js", + "serviceWorker.js", +] + +["browser_partitionedSharedWorkers.js"] +support-files = [ + "sharedWorker.js", + "partitionedSharedWorker.js", +] + +["browser_partitionkey_bloburl.js"] + +["browser_permissionInNormalWindows.js"] + +["browser_permissionInNormalWindows_alwaysPartition.js"] + +["browser_permissionInPrivateWindows.js"] + +["browser_permissionInPrivateWindows_alwaysPartition.js"] + +["browser_permissionPropagation.js"] +skip-if = [ + "os == 'linux' && bits == 64", # Bug 1645505 + "os == 'win' && debug", # Bug 1645505 +] + +["browser_referrerDefaultPolicy.js"] +support-files = ["referrer.sjs"] + +["browser_script.js"] +support-files = ["tracker.js"] + +["browser_serviceWorkersWithStorageAccessGranted.js"] + +["browser_siteSpecificWorkArounds.js"] + +["browser_socialtracking.js"] + +["browser_socialtracking_save_image.js"] + +["browser_staticPartition_CORS_preflight.js"] +support-files = ["browser_staticPartition_CORS_preflight.sjs"] + +["browser_staticPartition_HSTS.js"] +support-files = ["browser_staticPartition_HSTS.sjs"] + +["browser_staticPartition_cache.js"] +support-files = [ + "!/browser/components/originattributes/test/browser/file_cache.html", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.audio.ogg", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.embed.png", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.fetch.html", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.iframe.html", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.img.png", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.favicon.png", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.import.js", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.link.css", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.object.png", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.request.html", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.script.js", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.sharedworker.js", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.fetch.html", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.js", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.request.html", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.xhr.html", + "!/browser/components/originattributes/test/browser/file_thirdPartyChild.xhr.html", +] + +["browser_staticPartition_network.js"] + +["browser_staticPartition_saveAs.js"] +skip-if = ["os == 'linux' && bits == 64"] # Bug 1775746 +support-files = [ + "file_saveAsImage.sjs", + "file_saveAsVideo.sjs", + "file_saveAsPageInfo.html", + "file_video.ogv", +] + +["browser_staticPartition_tls_session.js"] + +["browser_staticPartition_websocket.js"] +skip-if = ["os == 'mac' && verify"] # Bug 1721210 +support-files = ["file_ws_handshake_delay_wsh.py"] + +["browser_storageAccessAutograntRequiresUserInteraction.js"] + +["browser_storageAccessDeniedGivesNoUserInteraction.js"] + +["browser_storageAccessDoorHanger.js"] + +["browser_storageAccessFrameInteractionGrantsUserInteraction.js"] + +["browser_storageAccessGrantedGivesUserInteraction.js"] + +["browser_storageAccessPrivilegeAPI.js"] + +["browser_storageAccessPromiseRejectHandlerUserInteraction.js"] + +["browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js"] + +["browser_storageAccessPromiseResolveHandlerUserInteraction.js"] + +["browser_storageAccessRejectsInsecureContexts.js"] + +["browser_storageAccessRemovalNavigateSubframe.js"] + +["browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js"] + +["browser_storageAccessRemovalNavigateTopframe.js"] + +["browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js"] + +["browser_storageAccessSandboxed.js"] + +["browser_storageAccessSandboxed_alwaysPartition.js"] + +["browser_storageAccessScopeDifferentSite.js"] + +["browser_storageAccessScopeSameOrigin.js"] + +["browser_storageAccessScopeSameSiteRead.js"] + +["browser_storageAccessScopeSameSiteWrite.js"] + +["browser_storageAccessThirdPartyChecks.js"] + +["browser_storageAccessThirdPartyChecks_alwaysPartition.js"] +support-files = [ + "trackingPage.html", + "trackingAPI.js", +] + +["browser_storageAccessWithDynamicFpi.js"] + +["browser_storageAccessWithHeuristics.js"] + +["browser_storageAccess_TopLevel_Arguments.js"] + +["browser_storageAccess_TopLevel_CookieBehavior.js"] + +["browser_storageAccess_TopLevel_CookiePermission.js"] + +["browser_storageAccess_TopLevel_CrossOriginSameSite.js"] + +["browser_storageAccess_TopLevel_Doorhanger.js"] + +["browser_storageAccess_TopLevel_Embed.js"] + +["browser_storageAccess_TopLevel_Enable.js"] + +["browser_storageAccess_TopLevel_RequireIntermediatePermission.js"] + +["browser_storageAccess_TopLevel_StorageAccessPermission.js"] + +["browser_storageAccess_TopLevel_UserActivation.js"] +skip-if = ["debug"] # Bug 1700551 + +["browser_storageAccess_cookies_on_grant.js"] + +["browser_storageAccess_userActivation.js"] + +["browser_subResources.js"] +support-files = ["subResources.sjs"] + +["browser_subResourcesPartitioned.js"] +support-files = ["subResources.sjs"] + +["browser_subResourcesPartitioned_alwaysPartition.js"] +support-files = ["subResources.sjs"] + +["browser_thirdPartyStorageRejectionForCORS.js"] + +["browser_urlDecorationStripping.js"] + +["browser_urlDecorationStripping_alwaysPartition.js"] +tags = "remote-settings" + +["browser_urlQueryStringStripping.js"] +skip-if = ["os == 'linux' && asan"] # Bug 1713909 - new Fission platform triage +support-files = ["file_stripping.html"] + +["browser_urlQueryStringStripping_allowList.js"] +support-files = ["file_stripping.html"] + +["browser_urlQueryStringStripping_nimbus.js"] +support-files = ["file_stripping.html"] + +["browser_urlQueryStringStripping_pbmode.js"] +support-files = ["file_stripping.html"] + +["browser_urlQueryStringStripping_telemetry.js"] +support-files = ["file_stripping.html"] + +["browser_urlQueryStringStripping_telemetry_2.js"] +support-files = ["file_stripping.html"] + +["browser_urlQueryStrippingListService.js"] + +["browser_userInteraction.js"] + +["browser_workerPropagation.js"] +support-files = ["workerIframe.html"] diff --git a/toolkit/components/antitracking/test/browser/browser_AntiTrackingETPHeuristic.js b/toolkit/components/antitracking/test/browser/browser_AntiTrackingETPHeuristic.js new file mode 100644 index 0000000000..f5274d8ba9 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_AntiTrackingETPHeuristic.js @@ -0,0 +1,261 @@ +"use strict"; + +const TEST_PAGE = TEST_DOMAIN + TEST_PATH + "page.html"; +const TEST_REDIRECT_PAGE = TEST_DOMAIN + TEST_PATH + "redirect.sjs"; +const TEST_TRACKING_PAGE = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "page.html"; +const TEST_TRACKING_REDIRECT_PAGE = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "redirect.sjs"; +const TEST_ANOTHER_TRACKING_REDIRECT_PAGE = + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS + TEST_PATH + "redirect.sjs"; + +const TEST_CASES = [ + // Tracker(Interacted) -> Non-Tracker + { + trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN], + redirects: [TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE], + expectedPermissionNumber: 1, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.ALLOW_ACTION, + ], + ], + }, + // Tracker(No interaction) -> Non-Tracker + { + trackersHasUserInteraction: [], + redirects: [TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE], + expectedPermissionNumber: 0, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.UNKNOWN_ACTION, + ], + ], + }, + // Non-Tracker -> Tracker(Interacted) -> Non-Tracker + { + trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN], + redirects: [TEST_REDIRECT_PAGE, TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE], + expectedPermissionNumber: 1, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.ALLOW_ACTION, + ], + ], + }, + // Tracker(Interacted) -> Tracker(Interacted) -> Tracker(Interacted) + { + trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN], + redirects: [ + TEST_TRACKING_REDIRECT_PAGE, + TEST_TRACKING_REDIRECT_PAGE, + TEST_TRACKING_PAGE, + ], + expectedPermissionNumber: 0, + expects: [ + [ + TEST_3RD_PARTY_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.UNKNOWN_ACTION, + ], + ], + }, + // Tracker1(Interacted) -> Tracker2(Interacted) -> Non-Tracker + { + trackersHasUserInteraction: [ + TEST_3RD_PARTY_DOMAIN, + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS, + ], + redirects: [ + TEST_TRACKING_REDIRECT_PAGE, + TEST_ANOTHER_TRACKING_REDIRECT_PAGE, + TEST_PAGE, + ], + expectedPermissionNumber: 1, + expects: [ + [ + TEST_DOMAIN, + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS, + Ci.nsIPermissionManager.ALLOW_ACTION, + ], + ], + }, + // Tracker1(Interacted) -> Non-Tracker -> Tracker2(No interaction) -> Non-Tracker + { + trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN], + redirects: [ + TEST_TRACKING_REDIRECT_PAGE, + TEST_REDIRECT_PAGE, + TEST_ANOTHER_TRACKING_REDIRECT_PAGE, + TEST_PAGE, + ], + expectedPermissionNumber: 1, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.ALLOW_ACTION, + ], + ], + }, + // Tracker1(Interacted) -> Non-Tracker -> Tracker2(Interacted) -> Non-Tracker + // Note that the result is not quite correct in this case. We are supposed to + // grant access to the least tracker instead of the first one. But, this is + // the behavior how we act so far. We would fix this in another bug. + { + trackersHasUserInteraction: [ + TEST_3RD_PARTY_DOMAIN, + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS, + ], + redirects: [ + TEST_TRACKING_REDIRECT_PAGE, + TEST_REDIRECT_PAGE, + TEST_ANOTHER_TRACKING_REDIRECT_PAGE, + TEST_PAGE, + ], + expectedPermissionNumber: 1, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.ALLOW_ACTION, + ], + ], + }, + // Tracker(Interacted) -> Non-Tracker (heuristic disabled) + { + trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN], + redirects: [TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE], + expectedPermissionNumber: 0, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.UNKNOWN_ACTION, + ], + ], + extraPrefs: [["privacy.antitracking.enableWebcompat", false]], + }, +]; + +async function interactWithSpecificTracker(aTracker) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: aTracker }, + async function (browser) { + info("Let's interact with the tracker"); + + await SpecialPowers.spawn(browser, [], async function () { + SpecialPowers.wrap(content.document).userInteractionForTesting(); + }); + } + ); + await BrowserTestUtils.closeWindow(win); +} + +function getNumberOfStorageAccessPermissions() { + let num = 0; + for (let perm of Services.perms.all) { + if (perm.type.startsWith("3rdPartyStorage^")) { + num++; + } + } + return num; +} + +async function verifyStorageAccessPermission(aExpects) { + for (let expect of aExpects) { + let uri = Services.io.newURI(expect[0]); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + let access = Services.perms.testPermissionFromPrincipal( + principal, + `3rdPartyStorage^${expect[1].slice(0, -1)}` + ); + + is(access, expect[2], "The storage access is set correctly"); + } +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ["privacy.restrict3rdpartystorage.heuristic.redirect", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +add_task(async function testETPRedirectHeuristic() { + info("Starting testing ETP redirect heuristic ..."); + + for (const test of TEST_CASES) { + let { extraPrefs } = test; + if (extraPrefs) { + await SpecialPowers.pushPrefEnv({ + set: extraPrefs, + }); + } + + // First, clear all permissions. + Services.perms.removeAll(); + + for (const tracker of test.trackersHasUserInteraction) { + info(`Interact with ${tracker} in top-level.`); + await interactWithSpecificTracker(tracker); + } + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading the tracking page and trigger the top-level redirect."); + SpecialPowers.spawn(browser, [test.redirects], async redirects => { + let link = content.document.createElement("a"); + link.appendChild(content.document.createTextNode("click me!")); + link.href = redirects.shift() + "?" + redirects.join("|"); + content.document.body.appendChild(link); + link.click(); + }); + + let finalRedirectDest = test.redirects[test.redirects.length - 1]; + + await BrowserTestUtils.browserLoaded(browser, false, finalRedirectDest); + + is( + getNumberOfStorageAccessPermissions(), + test.expectedPermissionNumber, + "The number of storage permissions is correct." + ); + + await verifyStorageAccessPermission(test.expects); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (extraPrefs) { + await SpecialPowers.popPrefEnv(); + } + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_PBMCookieBehavior.js b/toolkit/components/antitracking/test/browser/browser_PBMCookieBehavior.js new file mode 100644 index 0000000000..ebed00f23c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_PBMCookieBehavior.js @@ -0,0 +1,108 @@ +"use strict"; + +// This test will run all combinations of CookieBehavior. So, request a longer +// timeout here +requestLongerTimeout(3); + +const COOKIE_BEHAVIORS = [ + Ci.nsICookieService.BEHAVIOR_ACCEPT, + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + Ci.nsICookieService.BEHAVIOR_REJECT, + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, +]; + +async function verifyCookieBehavior(browser, expected) { + await SpecialPowers.spawn( + browser, + [{ expected, page: TEST_3RD_PARTY_PAGE }], + async obj => { + is( + content.document.cookieJarSettings.cookieBehavior, + obj.expected, + "The tab in the window has the expected CookieBehavior." + ); + + // Create an 3rd party iframe and check the cookieBehavior. + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + await SpecialPowers.spawn( + ifr.browsingContext, + [obj.expected], + async expected => { + is( + content.document.cookieJarSettings.cookieBehavior, + expected, + "The iframe in the window has the expected CookieBehavior." + ); + } + ); + } + ); +} + +add_task(async function () { + for (let regularCookieBehavior of COOKIE_BEHAVIORS) { + for (let PBMCookieBehavior of COOKIE_BEHAVIORS) { + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", regularCookieBehavior], + ["network.cookie.cookieBehavior.pbmode", PBMCookieBehavior], + ["dom.security.https_first_pbm", false], + ], + }); + + info( + ` Start testing with regular cookieBehavior(${regularCookieBehavior}) and PBM cookieBehavior(${PBMCookieBehavior})` + ); + + info(" Open a tab in regular window."); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info( + " Verify if the tab in regular window has the expected cookieBehavior." + ); + await verifyCookieBehavior(tab.linkedBrowser, regularCookieBehavior); + BrowserTestUtils.removeTab(tab); + + info(" Open a tab in private window."); + let pb_win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + tab = await BrowserTestUtils.openNewForegroundTab( + pb_win.gBrowser, + TEST_TOP_PAGE + ); + + let expectPBMCookieBehavior = PBMCookieBehavior; + + // The private cookieBehavior will mirror the regular pref if the regular + // pref has a user value and the private pref doesn't have a user pref. + if ( + Services.prefs.prefHasUserValue("network.cookie.cookieBehavior") && + !Services.prefs.prefHasUserValue("network.cookie.cookieBehavior.pbmode") + ) { + expectPBMCookieBehavior = regularCookieBehavior; + } + + info( + " Verify if the tab in private window has the expected cookieBehavior." + ); + await verifyCookieBehavior(tab.linkedBrowser, expectPBMCookieBehavior); + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(pb_win); + } + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_aboutblank.js b/toolkit/components/antitracking/test/browser/browser_aboutblank.js new file mode 100644 index 0000000000..f80a948771 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_aboutblank.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_aboutblankInIframe() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser, [], async function (obj) { + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + ifr.src = "about:blank"; + content.document.body.appendChild(ifr); + await loading; + + await SpecialPowers.spawn(ifr, [], async function (obj) { + ok( + content.navigator.cookieEnabled, + "Cookie should be enabled in about blank" + ); + }); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_addonHostPermissionIgnoredInTP.js b/toolkit/components/antitracking/test/browser/browser_addonHostPermissionIgnoredInTP.js new file mode 100644 index 0000000000..44664e239a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_addonHostPermissionIgnoredInTP.js @@ -0,0 +1,46 @@ +add_task(async function () { + info("Starting test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.trackingprotection.enabled", true]], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { permissions: ["https://tracking.example.com/"] }, + files: { + "page.html": + '<html><head></head><body><script src="script.js"></script><iframe src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/container2.html"></iframe></body></html>', + "script.js": + 'window.count=0;window.p=new Promise(resolve=>{onmessage=e=>{count=e.data.data;resolve();};});p.then(()=>{document.documentElement.setAttribute("count",count);});', + }, + async background() { + browser.test.sendMessage("ready", browser.runtime.getURL("page.html")); + }, + }); + await extension.startup(); + let url = await extension.awaitMessage("ready"); + + info("Creating a new tab"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + let browser = tab.linkedBrowser; + + info("Verify the number of script nodes found"); + await ContentTask.spawn(browser, [], async function (obj) { + // Need to wait a bit for cross-process postMessage... + await ContentTaskUtils.waitForCondition( + () => content.document.documentElement.getAttribute("count") !== null, + "waiting for 'count' attribute" + ); + let count = content.document.documentElement.getAttribute("count"); + is(count, 3, "Expected script nodes found"); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + await extension.unload(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_allowListNotifications.js b/toolkit/components/antitracking/test/browser/browser_allowListNotifications.js new file mode 100644 index 0000000000..999c6f93a0 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_allowListNotifications.js @@ -0,0 +1,141 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + gProtectionsHandler.disableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + // Now load subresources from a few third-party origins. + // We should expect to see none of these origins in the content blocking log at the end. + await fetch( + "https://test1.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for images"); + }); + + await fetch( + "https://test2.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for images"); + }); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE, + blockingCallback: (async _ => {}).toString(), + nonBlockingCallback: (async _ => {}).toString(), + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.blockingCallback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + let expectTrackerFound = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let log = JSON.parse(await browser.getContentBlockingLog()); + for (let trackerOrigin in log) { + is( + trackerOrigin + "/", + TEST_3RD_PARTY_DOMAIN, + "Correct tracker origin must be reported" + ); + let originLog = log[trackerOrigin]; + is(originLog.length, 1, "We should have 1 entry in the compressed log"); + expectTrackerFound(originLog[0]); + } + + gProtectionsHandler.enableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_allowListNotifications_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_allowListNotifications_alwaysPartition.js new file mode 100644 index 0000000000..0fd677e27f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_allowListNotifications_alwaysPartition.js @@ -0,0 +1,142 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + true, + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + gProtectionsHandler.disableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + // Now load subresources from a few third-party origins. + // We should expect to see none of these origins in the content blocking log at the end. + await fetch( + "https://test1.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for images"); + }); + + await fetch( + "https://test2.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for images"); + }); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE, + blockingCallback: (async _ => {}).toString(), + nonBlockingCallback: (async _ => {}).toString(), + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.blockingCallback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + const nsIWPL = Ci.nsIWebProgressListener; + let expectTrackerFound = (item, expect) => { + is(item[0], expect, "Correct blocking type reported"); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let log = JSON.parse(await browser.getContentBlockingLog()); + for (let trackerOrigin in log) { + is( + trackerOrigin + "/", + TEST_3RD_PARTY_DOMAIN, + "Correct tracker origin must be reported" + ); + let originLog = log[trackerOrigin]; + + is(originLog.length, 1, "We should have one entry in the compressed log"); + expectTrackerFound( + originLog[0], + nsIWPL.STATE_LOADED_LEVEL_1_TRACKING_CONTENT + ); + } + + gProtectionsHandler.enableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_allowListSeparationInPrivateAndNormalWindows.js b/toolkit/components/antitracking/test/browser/browser_allowListSeparationInPrivateAndNormalWindows.js new file mode 100644 index 0000000000..38eb9fc090 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_allowListSeparationInPrivateAndNormalWindows.js @@ -0,0 +1,52 @@ +// This test works by setting up an exception for the private window allow list +// manually, and it then expects to see some blocking notifications (note the +// document.cookie setter in the blocking callback.) +// If the exception lists aren't handled separately, we'd get confused and put +// the pages loaded under this test in the allow list, which would result in +// the test not passing because no blocking notifications would be observed. + +// Testing the reverse case would also be interesting, but unfortunately there +// isn't a super easy way to do that with our antitracking test framework since +// private windows wouldn't send any blocking notifications as they don't have +// storage access in the first place. + +"use strict"; +add_task(async _ => { + let uri = Services.io.newURI("https://example.net"); + PermissionTestUtils.add( + uri, + "trackingprotection-pb", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +AntiTracking.runTest( + "Test that we don't honour a private allow list exception in a normal window", + // Blocking callback + async _ => { + document.cookie = "name=value"; + }, + + // Non blocking callback + async _ => { + // Nothing to do here. + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, // no extra prefs + false, // run the window.open() test + false, // run the user interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false +); // run in a normal window diff --git a/toolkit/components/antitracking/test/browser/browser_allowPermissionForTracker.js b/toolkit/components/antitracking/test/browser/browser_allowPermissionForTracker.js new file mode 100644 index 0000000000..e628199ead --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_allowPermissionForTracker.js @@ -0,0 +1,64 @@ +// This test works by setting up an exception for the tracker domain, which +// disables all the anti-tracking tests. + +add_task(async _ => { + PermissionTestUtils.add( + "https://tracking.example.org", + "cookie", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://tracking.example.com", + "cookie", + Services.perms.ALLOW_ACTION + ); + // Grant interaction permission so we can directly call + // requestStorageAccess from the tracker. + PermissionTestUtils.add( + "https://tracking.example.org", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +AntiTracking._createTask({ + name: "Test that we do honour a cookie permission for nested windows", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: false, + allowList: false, + callback: async _ => { + document.cookie = "name=value"; + ok(document.cookie != "", "Nothing is blocked"); + + // requestStorageAccess should resolve + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await document + .requestStorageAccess() + .then(() => { + ok(true, "Should grant storage access"); + }) + .catch(() => { + ok(false, "Should grant storage access"); + }); + SpecialPowers.wrap(document).clearUserGestureActivation(); + }, + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + extraPrefs: [["network.cookie.sameSite.laxByDefault", false]], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_backgroundImageAssertion.js b/toolkit/components/antitracking/test/browser/browser_backgroundImageAssertion.js new file mode 100644 index 0000000000..16eec0da9e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_backgroundImageAssertion.js @@ -0,0 +1,69 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [{ page: TEST_3RD_PARTY_PAGE_WITH_SVG }], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + ok(true, "No crash, hopefully!"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingCookies.js b/toolkit/components/antitracking/test/browser/browser_blockingCookies.js new file mode 100644 index 0000000000..dc22ad77b9 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingCookies.js @@ -0,0 +1,253 @@ +requestLongerTimeout(4); + +// Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); + +AntiTracking.runTestInNormalAndPrivateMode( + "Set/Get Cookies", + // Blocking callback + async _ => { + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookies for me"); + + for (let arg of ["?checkonly", "?redirect-checkonly"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + // Let's do it twice. + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + } + + is(document.cookie, "", "Still no cookies for me"); + }, + + // Non blocking callback + async _ => { + is(document.cookie, "", "No cookies for me"); + + // Note: The ?redirect test is _not_ using checkonly, so it will actually + // set our foopy=1 cookie. + for (let arg of ["?checkonly", "?redirect"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + } + + document.cookie = "name=value"; + ok(document.cookie.includes("name=value"), "Some cookies for me"); + ok(document.cookie.includes("foopy=1"), "Some cookies for me"); + + for (let arg of ["", "?redirect"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-present", "We should have cookies"); + }); + } + + ok(document.cookie.length, "Some Cookies for me"); + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +AntiTracking.runTestInNormalAndPrivateMode( + "Cookies and Storage Access API", + // Blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookies for me"); + + for (let arg of ["", "?redirect"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + // Let's do it twice. + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + } + + is(document.cookie, "", "Still no cookies for me"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + is(document.cookie, "", "No cookies for me"); + } else { + is(document.cookie, "name=value", "I have the cookies!"); + } + }, + + // Non blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + + // Note: The ?redirect test is _not_ using checkonly, so it will actually + // set our foopy=1 cookie. + for (let arg of ["?checkonly", "?redirect"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + } + + document.cookie = "name=value"; + ok(document.cookie.includes("name=value"), "Some cookies for me"); + ok(document.cookie.includes("foopy=1"), "Some cookies for me"); + + for (let arg of ["", "?redirect"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-present", "We should have cookies"); + }); + } + + ok(document.cookie.length, "Some Cookies for me"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + ok(document.cookie.length, "Still some Cookies for me"); + ok(document.cookie.includes("name=value"), "Some cookies for me"); + ok(document.cookie.includes("foopy=1"), "Some cookies for me"); + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); + +AntiTracking._createTask({ + name: "Block cookies with BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN when preference is enabled", + cookieBehavior: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + allowList: false, + callback: async _ => { + document.cookie = "name=value"; + is(document.cookie, "", "Document cookie is blocked"); + await fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have HTTP cookies"); + }); + await fetch("server.sjs?checkonly") + .then(r => r.text()) + .then(text => { + is( + text, + "cookie-not-present", + "We should still not have HTTP cookies after setting them via HTTP" + ); + }); + is( + document.cookie, + "", + "Document cookie is still blocked after setting via HTTP" + ); + }, + extraPrefs: [["network.cookie.cookieBehavior.optInPartitioning", true]], + thirdPartyPage: TEST_4TH_PARTY_PAGE, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, +}); + +AntiTracking._createTask({ + name: "Block cookies with BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN when preference is enabled in pbmode", + cookieBehavior: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + allowList: false, + callback: async _ => { + document.cookie = "name=value"; + is(document.cookie, "", "Document cookie is blocked"); + await fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have HTTP cookies"); + }); + await fetch("server.sjs?checkonly") + .then(r => r.text()) + .then(text => { + is( + text, + "cookie-not-present", + "We should still not have HTTP cookies after setting them via HTTP" + ); + }); + is( + document.cookie, + "", + "Document cookie is still blocked after setting via HTTP" + ); + }, + extraPrefs: [["network.cookie.cookieBehavior.optInPartitioning", true]], + thirdPartyPage: TEST_4TH_PARTY_PAGE, + runInPrivateWindow: true, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, +}); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCache.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCache.js new file mode 100644 index 0000000000..a8353cc8f1 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCache.js @@ -0,0 +1,39 @@ +requestLongerTimeout(2); + +AntiTracking.runTest( + "DOM Cache", + async _ => { + await caches.open("wow").then( + _ => { + ok(false, "DOM Cache cannot be used!"); + }, + _ => { + ok(true, "DOM Cache cannot be used!"); + } + ); + }, + async _ => { + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.caches.testing.enabled", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ] +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartition.js new file mode 100644 index 0000000000..4cb90af8ad --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartition.js @@ -0,0 +1,54 @@ +requestLongerTimeout(2); + +AntiTracking.runTest( + "DOM Cache Always Partition Storage", + async _ => { + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + // caches.open still fails for cookieBehavior 2 (reject) and + // 1 (reject for third parties), so we need to account for that. + let shouldFail = + effectiveCookieBehavior == + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT || + (effectiveCookieBehavior == + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN && + !document.location.href.includes("3rdPartyWO") && + !document.location.href.includes("3rdPartyUI")); + + await caches.open("wow").then( + _ => { + ok(!shouldFail, "DOM Cache can be used!"); + }, + _ => { + ok(shouldFail, "DOM Cache can be used!"); + } + ); + }, + async _ => { + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.caches.testing.enabled", true], + ["privacy.partition.always_partition_third_party_non_cookie_storage", true], + ] +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartitionSAA.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartitionSAA.js new file mode 100644 index 0000000000..bef53f2936 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartitionSAA.js @@ -0,0 +1,80 @@ +requestLongerTimeout(2); + +AntiTracking.runTest( + "DOM Cache Always Partition Storage and Storage Access API", + async _ => { + await noStorageAccessInitially(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + await caches.open("wow").then( + _ => { + ok(!shouldThrow, "DOM Cache can be used!"); + }, + _ => { + ok(shouldThrow, "DOM Cache can be used!"); + } + ); + + await callRequestStorageAccess(); + + await caches.open("wow").then( + _ => { + ok(!shouldThrow, "DOM Cache can be used!"); + }, + _ => { + ok(shouldThrow, "DOM Cache can be used!"); + } + ); + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.caches.testing.enabled", true], + ["privacy.partition.always_partition_third_party_non_cookie_storage", true], + ], + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheSAA.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheSAA.js new file mode 100644 index 0000000000..06526972dd --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheSAA.js @@ -0,0 +1,85 @@ +requestLongerTimeout(2); + +AntiTracking.runTest( + "DOM Cache and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await caches.open("wow").then( + _ => { + ok(false, "DOM Cache cannot be used!"); + }, + _ => { + ok(true, "DOM Cache cannot be used!"); + } + ); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + await caches.open("wow").then( + _ => { + ok(!shouldThrow, "DOM Cache can be used!"); + }, + _ => { + ok(shouldThrow, "DOM Cache can be used!"); + } + ); + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.caches.testing.enabled", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js new file mode 100644 index 0000000000..7fde2365bb --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js @@ -0,0 +1,102 @@ +requestLongerTimeout(4); + +AntiTracking.runTestInNormalAndPrivateMode( + "IndexedDB", + // blocking callback + async _ => { + try { + indexedDB.open("test", "1"); + ok(false, "IDB should be blocked"); + } catch (e) { + ok(true, "IDB should be blocked"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + // non-blocking callback + async _ => { + indexedDB.open("test", "1"); + ok(true, "IDB should be allowed"); + }, + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["dom.indexedDB.hide_in_pbmode.enabled", false]] +); + +AntiTracking.runTestInNormalAndPrivateMode( + "IndexedDB and Storage Access API", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + try { + indexedDB.open("test", "1"); + ok(false, "IDB should be blocked"); + } catch (e) { + ok(true, "IDB should be blocked"); + is(e.name, "SecurityError", "We want a security error message."); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + indexedDB.open("test", "1"); + hasThrown = false; + } catch (e) { + hasThrown = true; + is(e.name, "SecurityError", "We want a security error message."); + } + + is( + hasThrown, + shouldThrow, + "IDB should be allowed if not in cookieBehavior pref value BEHAVIOR_REJECT/BEHAVIOR_REJECT_FOREIGN" + ); + }, + // non-blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + indexedDB.open("test", "1"); + ok(true, "IDB should be allowed"); + + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + indexedDB.open("test", "1"); + ok(true, "IDB should be allowed"); + }, + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["dom.indexedDB.hide_in_pbmode.enabled", false]], + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers.js b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers.js new file mode 100644 index 0000000000..55db3cc05b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers.js @@ -0,0 +1,73 @@ +AntiTracking.runTestInNormalAndPrivateMode( + "IndexedDB in workers", + async _ => { + function blockCode() { + try { + indexedDB.open("test", "1"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + + let blob = new Blob([blockCode.toString() + "; blockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + function nonBlockCode() { + indexedDB.open("test", "1"); + postMessage(true); + } + + let blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["dom.indexedDB.hide_in_pbmode.enabled", false]] +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers2.js b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers2.js new file mode 100644 index 0000000000..02ce8dc588 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers2.js @@ -0,0 +1,152 @@ +requestLongerTimeout(6); + +AntiTracking.runTestInNormalAndPrivateMode( + "IndexedDB in workers and Storage Access API", + async _ => { + function blockCode() { + try { + indexedDB.open("test", "1"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + function nonBlockCode() { + indexedDB.open("test", "1"); + postMessage(true); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + let blob = new Blob([blockCode.toString() + "; blockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + blob = new Blob([blockCode.toString() + "; blockCode();"]); + } else { + blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]); + } + ok(blob, "Blob has been created"); + + blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + function nonBlockCode() { + indexedDB.open("test", "1"); + postMessage(true); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + let blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["dom.indexedDB.hide_in_pbmode.enabled", false]], + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingLocalStorage.js b/toolkit/components/antitracking/test/browser/browser_blockingLocalStorage.js new file mode 100644 index 0000000000..1362fc7519 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingLocalStorage.js @@ -0,0 +1,94 @@ +requestLongerTimeout(4); + +AntiTracking.runTestInNormalAndPrivateMode( + "localStorage", + async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +AntiTracking.runTestInNormalAndPrivateMode( + "localStorage and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + } else { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + } + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js b/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js new file mode 100644 index 0000000000..34af2b76ce --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js @@ -0,0 +1,322 @@ +if (AppConstants.MOZ_CODE_COVERAGE) { + requestLongerTimeout(12); +} else { + requestLongerTimeout(12); +} + +AntiTracking.runTestInNormalAndPrivateMode( + "BroadcastChannel", + async _ => { + try { + new BroadcastChannel("hello"); + ok(false, "BroadcastChannel cannot be used!"); + } catch (e) { + ok(true, "BroadcastChannel cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async _ => { + new BroadcastChannel("hello"); + ok(true, "BroadcastChannel be used"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +AntiTracking.runTestInNormalAndPrivateMode( + "BroadcastChannel in workers", + async _ => { + function blockingCode() { + try { + new BroadcastChannel("hello"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + + let blob = new Blob([blockingCode.toString() + "; blockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + function nonBlockingCode() { + new BroadcastChannel("hello"); + postMessage(true); + } + + let blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +AntiTracking.runTestInNormalAndPrivateMode( + "BroadcastChannel and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + try { + new BroadcastChannel("hello"); + ok(false, "BroadcastChannel cannot be used!"); + } catch (e) { + ok(true, "BroadcastChannel cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + try { + new BroadcastChannel("hello"); + ok(false, "BroadcastChannel cannot be used!"); + } catch (e) { + ok(true, "BroadcastChannel cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + } else { + new BroadcastChannel("hello"); + ok(true, "BroadcastChannel can be used"); + } + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + new BroadcastChannel("hello"); + ok(true, "BroadcastChanneli can be used"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + new BroadcastChannel("hello"); + ok(true, "BroadcastChannel can be used"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); + +AntiTracking.runTestInNormalAndPrivateMode( + "BroadcastChannel in workers and Storage Access API", + async _ => { + function blockingCode() { + try { + new BroadcastChannel("hello"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + function nonBlockingCode() { + new BroadcastChannel("hello"); + postMessage(true); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + let blob = new Blob([blockingCode.toString() + "; blockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + blob = new Blob([blockingCode.toString() + "; blockingCode();"]); + } else { + blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]); + } + + ok(blob, "Blob has been created"); + + blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + function nonBlockingCode() { + new BroadcastChannel("hello"); + postMessage(true); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + let blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingNoOpener.js b/toolkit/components/antitracking/test/browser/browser_blockingNoOpener.js new file mode 100644 index 0000000000..ba75454e18 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingNoOpener.js @@ -0,0 +1,41 @@ +gFeatures = "noopener"; + +AntiTracking.runTestInNormalAndPrivateMode( + "Blocking in the case of noopener windows", + async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async phase => { + switch (phase) { + case 1: + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + break; + case 2: + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + break; + } + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + true, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkers.js b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkers.js new file mode 100644 index 0000000000..c61f85d69f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkers.js @@ -0,0 +1,30 @@ +AntiTracking.runTest( + "ServiceWorkers", + async _ => { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(false, "ServiceWorker cannot be used!"); + }, + _ => { + ok(true, "ServiceWorker cannot be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + null, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ] +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkersStorageAccessAPI.js b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkersStorageAccessAPI.js new file mode 100644 index 0000000000..a7676bf939 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkersStorageAccessAPI.js @@ -0,0 +1,133 @@ +/* import-globals-from antitracking_head.js */ + +requestLongerTimeout(2); + +AntiTracking.runTest( + "ServiceWorkers and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(false, "ServiceWorker cannot be used!"); + }, + _ => { + ok(true, "ServiceWorker cannot be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(false, "ServiceWorker cannot be used!"); + }, + _ => { + ok(true, "ServiceWorker cannot be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + } else { + await navigator.serviceWorker + .register("empty.js") + .then( + reg => { + ok(true, "ServiceWorker can be used!"); + return reg; + }, + _ => { + ok(false, "ServiceWorker cannot be used! " + _); + } + ) + .then( + reg => reg.unregister(), + _ => { + ok(false, "unregister failed"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + } + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + await navigator.serviceWorker + .register("empty.js") + .then( + reg => { + ok(true, "ServiceWorker can be used!"); + return reg; + }, + _ => { + ok(false, "ServiceWorker cannot be used!"); + } + ) + .then( + reg => reg.unregister(), + _ => { + ok(false, "unregister failed"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + await navigator.serviceWorker + .register("empty.js") + .then( + reg => { + ok(true, "ServiceWorker can be used!"); + return reg; + }, + _ => { + ok(false, "ServiceWorker cannot be used!"); + } + ) + .then( + reg => reg.unregister(), + _ => { + ok(false, "unregister failed"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingSessionStorage.js b/toolkit/components/antitracking/test/browser/browser_blockingSessionStorage.js new file mode 100644 index 0000000000..25a926ed3f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingSessionStorage.js @@ -0,0 +1,126 @@ +requestLongerTimeout(6); + +AntiTracking.runTestInNormalAndPrivateMode( + "sessionStorage", + async _ => { + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + sessionStorage.foo = 42; + hasThrown = false; + } catch (e) { + hasThrown = true; + is(e.name, "SecurityError", "We want a security error message."); + } + + is( + hasThrown, + shouldThrow, + "SessionStorage show thrown only if cookieBehavior is REJECT" + ); + }, + async _ => { + sessionStorage.foo = 42; + ok(true, "SessionStorage is always allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [], + true, + true +); + +AntiTracking.runTestInNormalAndPrivateMode( + "sessionStorage and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + sessionStorage.foo = 42; + hasThrown = false; + } catch (e) { + hasThrown = true; + is(e.name, "SecurityError", "We want a security error message."); + } + + is( + hasThrown, + shouldThrow, + "SessionStorage show thrown only if cookieBehavior is REJECT" + ); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + try { + sessionStorage.foo = 42; + hasThrown = false; + } catch (e) { + hasThrown = true; + is(e.name, "SecurityError", "We want a security error message."); + } + + is( + hasThrown, + shouldThrow, + "SessionStorage show thrown only if cookieBehavior is REJECT" + ); + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + sessionStorage.foo = 42; + ok(true, "SessionStorage is always allowed"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + sessionStorage.foo = 42; + ok( + true, + "SessionStorage is allowed after calling the storage access API too" + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingSharedWorkers.js b/toolkit/components/antitracking/test/browser/browser_blockingSharedWorkers.js new file mode 100644 index 0000000000..0daffc4565 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingSharedWorkers.js @@ -0,0 +1,94 @@ +requestLongerTimeout(4); + +AntiTracking.runTestInNormalAndPrivateMode( + "SharedWorkers", + async _ => { + try { + new SharedWorker("a.js", "foo"); + ok(false, "SharedWorker cannot be used!"); + } catch (e) { + ok(true, "SharedWorker cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async _ => { + new SharedWorker("a.js", "foo"); + ok(true, "SharedWorker is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +AntiTracking.runTestInNormalAndPrivateMode( + "SharedWorkers and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + try { + new SharedWorker("a.js", "foo"); + ok(false, "SharedWorker cannot be used!"); + } catch (e) { + ok(true, "SharedWorker cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + try { + new SharedWorker("a.js", "foo"); + ok(false, "SharedWorker cannot be used!"); + } catch (e) { + ok(true, "SharedWorker cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + } else { + new SharedWorker("a.js", "foo"); + ok(true, "SharedWorker is allowed"); + } + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + new SharedWorker("a.js", "foo"); + ok(true, "SharedWorker is allowed"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + new SharedWorker("a.js", "bar"); + ok(true, "SharedWorker is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_contentBlockingAllowListPrincipal.js b/toolkit/components/antitracking/test/browser/browser_contentBlockingAllowListPrincipal.js new file mode 100644 index 0000000000..a9e2ac6fce --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_contentBlockingAllowListPrincipal.js @@ -0,0 +1,237 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_SANDBOX_URL = + "https://example.com/browser/toolkit/components/antitracking/test/browser/sandboxed.html"; + +/** + * Tests the contentBlockingAllowListPrincipal. + * @param {Browser} browser - Browser to test. + * @param {("content"|"system")} type - Expected principal type. + * @param {String} [origin] - Expected origin of principal. Defaults to the + * origin of the browsers content principal. + */ +function checkAllowListPrincipal( + browser, + type, + origin = browser.contentPrincipal.origin +) { + let principal = + browser.browsingContext.currentWindowGlobal + .contentBlockingAllowListPrincipal; + ok(principal, "Principal is set"); + + if (type == "content") { + ok(principal.isContentPrincipal, "Is content principal"); + + ok( + principal.schemeIs("https"), + "allowlist content principal must have https scheme" + ); + } else if (type == "system") { + ok(principal.isSystemPrincipal, "Is system principal"); + } else { + throw new Error("Unexpected principal type"); + } + + is(principal.origin, origin, "Correct origin"); +} + +/** + * Runs a given test in a normal window and in a private browsing window. + * @param {String} initialUrl - URL to load in the initial test tab. + * @param {Function} testCallback - Test function to run in both windows. + */ +async function runTestInNormalAndPrivateMode(initialUrl, testCallback) { + for (let i = 0; i < 2; i++) { + let isPrivateBrowsing = !!i; + info("Running test. Private browsing: " + !!i); + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: isPrivateBrowsing, + }); + let tab = BrowserTestUtils.addTab(win.gBrowser, initialUrl); + let browser = tab.linkedBrowser; + + await BrowserTestUtils.browserLoaded(browser); + + await testCallback(browser, isPrivateBrowsing); + + await BrowserTestUtils.closeWindow(win); + } +} + +/** + * Creates an iframe in the passed browser and waits for it to load. + * @param {Browser} browser - Browser to create the frame in. + * @param {String} src - Frame source url. + * @param {String} id - Frame id. + * @param {String} [sandboxAttr] - Optional list of sandbox attributes to set + * for the iframe. Defaults to no sandbox. + * @returns {Promise} - Resolves once the frame has loaded. + */ +function createFrame(browser, src, id, sandboxAttr) { + return SpecialPowers.spawn( + browser, + [{ page: src, frameId: id, sandboxAttr }], + async function (obj) { + await new content.Promise(resolve => { + let frame = content.document.createElement("iframe"); + frame.src = obj.page; + frame.id = obj.frameId; + if (obj.sandboxAttr) { + frame.setAttribute("sandbox", obj.sandboxAttr); + } + frame.addEventListener("load", resolve, { once: true }); + content.document.body.appendChild(frame); + }); + } + ); +} + +add_task(async setup => { + // Disable heuristics. We don't need them and if enabled the resulting + // telemetry can race with the telemetry in the next test. + // See Bug 1686836, Bug 1686894. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.restrict3rdpartystorage.heuristic.redirect", false], + ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false], + ["privacy.restrict3rdpartystorage.heuristic.window_open", false], + ["dom.security.https_first_pbm", false], + ], + }); +}); + +/** + * Test that we get the correct allow list principal which matches the content + * principal for an https site. + */ +add_task(async test_contentPrincipalHTTPS => { + await runTestInNormalAndPrivateMode("https://example.com", browser => { + checkAllowListPrincipal(browser, "content"); + }); +}); + +/** + * Tests that the scheme of the allowlist principal is HTTPS, even though the + * site is loaded via HTTP. + */ +add_task(async test_contentPrincipalHTTP => { + await runTestInNormalAndPrivateMode( + "http://example.net", + (browser, isPrivateBrowsing) => { + checkAllowListPrincipal( + browser, + "content", + "https://example.net" + + (isPrivateBrowsing ? "^privateBrowsingId=1" : "") + ); + } + ); +}); + +/** + * Tests that the allow list principal is a system principal for the preferences + * about site. + */ +add_task(async test_systemPrincipal => { + await runTestInNormalAndPrivateMode("about:preferences", browser => { + checkAllowListPrincipal(browser, "system"); + }); +}); + +/** + * Tests that we get a valid content principal for top level sandboxed pages, + * and not the document principal which is a null principal. + */ +add_task(async test_TopLevelSandbox => { + await runTestInNormalAndPrivateMode( + TEST_SANDBOX_URL, + (browser, isPrivateBrowsing) => { + ok( + browser.contentPrincipal.isNullPrincipal, + "Top level sandboxed page should have null principal" + ); + checkAllowListPrincipal( + browser, + "content", + "https://example.com" + + (isPrivateBrowsing ? "^privateBrowsingId=1" : "") + ); + } + ); +}); + +/** + * Tests that we get a valid content principal for a new tab opened via + * window.open. + */ +add_task(async test_windowOpen => { + await runTestInNormalAndPrivateMode("https://example.com", async browser => { + checkAllowListPrincipal(browser, "content"); + + let promiseTabOpened = BrowserTestUtils.waitForNewTab( + browser.ownerGlobal.gBrowser, + "https://example.org/", + true + ); + + // Call window.open from iframe. + await SpecialPowers.spawn(browser, [], async function () { + content.open("https://example.org/"); + }); + + let tab = await promiseTabOpened; + + checkAllowListPrincipal(tab.linkedBrowser, "content"); + + BrowserTestUtils.removeTab(tab); + }); +}); + +/** + * Tests that we get a valid content principal for a new tab opened via + * window.open from a sandboxed iframe. + */ +add_task(async test_windowOpenFromSandboxedFrame => { + await runTestInNormalAndPrivateMode( + "https://example.com", + async (browser, isPrivateBrowsing) => { + checkAllowListPrincipal(browser, "content"); + + // Create sandboxed iframe, allow popups. + await createFrame( + browser, + "https://example.com", + "sandboxedIframe", + "allow-popups" + ); + // Iframe BC is the only child of the test browser. + let [frameBrowsingContext] = browser.browsingContext.children; + + let promiseTabOpened = BrowserTestUtils.waitForNewTab( + browser.ownerGlobal.gBrowser, + "https://example.org/", + true + ); + + // Call window.open from iframe. + await SpecialPowers.spawn(frameBrowsingContext, [], async function () { + content.open("https://example.org/"); + }); + + let tab = await promiseTabOpened; + + checkAllowListPrincipal( + tab.linkedBrowser, + "content", + "https://example.org" + + (isPrivateBrowsing ? "^privateBrowsingId=1" : "") + ); + + BrowserTestUtils.removeTab(tab); + } + ); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js b/toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js new file mode 100644 index 0000000000..1e006541c8 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js @@ -0,0 +1,418 @@ +/** + * Bug 1668199 - Testing the content blocking telemetry. + */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const LABEL_STORAGE_GRANTED = 0; +const LABEL_STORAGE_ACCESS_API = 1; +const LABEL_OPENER_AFTER_UI = 2; +const LABEL_OPENER = 3; +const LABEL_REDIRECT = 4; + +function clearTelemetry() { + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry.getHistogramById("STORAGE_ACCESS_REMAINING_DAYS").clear(); +} + +function getExpectedExpiredDaysFromPref(pref) { + let expiredSecond = Services.prefs.getIntPref(pref); + + // This is unlikely to happen, but just in case. + if (expiredSecond <= 0) { + return 0; + } + + // We need to subtract one second from the expect expired second because there + // will be a short delay between the time we add the permission and the time + // we record the telemetry. Subtracting one can help us to get the correct + // expected expired days. + // + // Note that 86400 is seconds in one day. + return Math.trunc((expiredSecond - 1) / 86400); +} + +async function testTelemetry( + aProbeInParent, + aExpectedCnt, + aLabel, + aExpectedIdx +) { + info("Trigger the 'idle-daily' to trigger the telemetry probe."); + // Synthesis a fake 'idle-daily' notification to the content blocking + // telemetry service. + let cbts = Cc["@mozilla.org/content-blocking-telemetry-service;1"].getService( + Ci.nsIObserver + ); + cbts.observe(null, "idle-daily", null); + + let storageAccessGrantedHistogram; + + // Wait until the telemetry probe appears. + await BrowserTestUtils.waitForCondition(() => { + let histograms; + if (aProbeInParent) { + histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).parent; + } else { + histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).content; + } + storageAccessGrantedHistogram = histograms.STORAGE_ACCESS_GRANTED_COUNT; + + return ( + !!storageAccessGrantedHistogram && + storageAccessGrantedHistogram.values[LABEL_STORAGE_GRANTED] == + aExpectedCnt + ); + }); + + is( + storageAccessGrantedHistogram.values[LABEL_STORAGE_GRANTED], + aExpectedCnt, + "There should be expected storage access granted count in telemetry." + ); + is( + storageAccessGrantedHistogram.values[aLabel], + 1, + "There should be one reason count in telemetry." + ); + + let storageAccessRemainingDaysHistogram = Services.telemetry.getHistogramById( + "STORAGE_ACCESS_REMAINING_DAYS" + ); + + TelemetryTestUtils.assertHistogram( + storageAccessRemainingDaysHistogram, + aExpectedIdx, + 1 + ); + + // Clear telemetry probes + clearTelemetry(); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ["network.cookie.cookieBehavior.pbmode", BEHAVIOR_REJECT_TRACKER], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ["privacy.restrict3rdpartystorage.heuristic.redirect", true], + ["toolkit.telemetry.ipcBatchTimeout", 0], + // Explicity set the expiration time to 29 days to avoid an intermittent + // issue that we could get 30 days of expiration time if we test the + // telemetry too soon. + ["privacy.restrict3rdpartystorage.expiration", 2591999], + ["privacy.restrict3rdpartystorage.expiration_redirect", 2591999], + ], + }); + + // Clear Telemetry probes before testing. + // There can be telemetry race conditions if the previous test generates + // content blocking telemetry. + // See Bug 1686836, Bug 1686894. + clearTelemetry(); + + await UrlClassifierTestUtils.addTestTrackers(); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +add_task(async function testTelemetryForStorageAccessAPI() { + info("Starting testing if storage access API send telemetry probe ..."); + + // First, clear all permissions. + Services.perms.removeAll(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading the tracking iframe and call the RequestStorageAccess."); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + }, + ], + async obj => { + let msg = {}; + msg.callback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + }).toString(); + + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + let expectedExpiredDays = getExpectedExpiredDaysFromPref( + "privacy.restrict3rdpartystorage.expiration" + ); + + // The storage access permission will be expired in 29 days, so the expected + // index in the telemetry probe would be 29. + await testTelemetry(false, 1, LABEL_STORAGE_ACCESS_API, expectedExpiredDays); +}); + +add_task(async function testTelemetryForWindowOpenHeuristic() { + info("Starting testing if window open heuristic send telemetry probe ..."); + + // First, clear all permissions. + Services.perms.removeAll(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading the tracking iframe and trigger the heuristic"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_WO, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + + info("Checking if storage access is denied"); + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + let expectedExpiredDays = getExpectedExpiredDaysFromPref( + "privacy.restrict3rdpartystorage.expiration" + ); + + // The storage access permission will be expired in 29 days, so the expected + // index in the telemetry probe would be 29. + await testTelemetry(false, 1, LABEL_OPENER, expectedExpiredDays); +}); + +add_task(async function testTelemetryForUserInteractionHeuristic() { + info( + "Starting testing if UserInteraction heuristic send telemetry probe ..." + ); + + // First, clear all permissions. + Services.perms.removeAll(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Interact with the tracker in top-level."); + await AntiTracking.interactWithTracker(); + + info("Loading the tracking iframe and trigger the heuristic"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + await noStorageAccessInitially(); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info("Opening a window from the iframe."); + await SpecialPowers.spawn(ifr, [obj.popup], async popup => { + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + // We need to check the document URI here as well for the same + // reason above. + if ( + aTopic == "domwindowclosed" && + aSubject.document.documentURI == + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html" + ) { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + content.open(popup); + + info("Let's wait for the window to be closed"); + await windowClosed; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + let expectedExpiredDays = getExpectedExpiredDaysFromPref( + "privacy.restrict3rdpartystorage.expiration" + ); + + // The storage access permission will be expired in 29 days, so the expected + // index in the telemetry probe would be 29. + // + // Note that the expected count here is 2. It's because the opener heuristic + // will also be triggered when triggered UserInteraction Heuristic. + await testTelemetry(false, 2, LABEL_OPENER_AFTER_UI, expectedExpiredDays); +}); + +add_task(async function testTelemetryForRedirectHeuristic() { + info("Starting testing if redirect heuristic send telemetry probe ..."); + + const TEST_TRACKING_PAGE = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "page.html"; + const TEST_REDIRECT_PAGE = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "redirect.sjs?" + TEST_TOP_PAGE; + + // First, clear all permissions. + Services.perms.removeAll(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TRACKING_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading the tracking page and trigger the redirect."); + SpecialPowers.spawn(browser, [TEST_REDIRECT_PAGE], async url => { + content.document.userInteractionForTesting(); + + let link = content.document.createElement("a"); + link.appendChild(content.document.createTextNode("click me!")); + link.href = url; + content.document.body.appendChild(link); + link.click(); + }); + + await BrowserTestUtils.browserLoaded(browser, false, TEST_TOP_PAGE); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + let expectedExpiredDaysRedirect = getExpectedExpiredDaysFromPref( + "privacy.restrict3rdpartystorage.expiration_redirect" + ); + + // We would only grant the storage permission for 29 days for the redirect + // heuristic, so the expected index in the telemetry probe would be 29. + await testTelemetry(true, 1, LABEL_REDIRECT, expectedExpiredDaysRedirect); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_cookieBetweenTabs.js b/toolkit/components/antitracking/test/browser/browser_cookieBetweenTabs.js new file mode 100644 index 0000000000..635f145d46 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_cookieBetweenTabs.js @@ -0,0 +1,59 @@ +add_task(async function () { + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT], + ["network.cookie.cookieBehavior.pbmode", BEHAVIOR_REJECT], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ["dom.ipc.processCount", 4], + ], + }); + + info("First tab opened"); + let tab = BrowserTestUtils.addTab( + gBrowser, + TEST_DOMAIN + TEST_PATH + "empty.html" + ); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Disabling content blocking for this page"); + gProtectionsHandler.disableForCurrentPage(); + + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [], async _ => { + is(content.document.cookie, "", "No cookie set"); + content.document.cookie = "a=b"; + is(content.document.cookie, "a=b", "Cookie set"); + }); + + info("Second tab opened"); + let tab2 = BrowserTestUtils.addTab( + gBrowser, + TEST_DOMAIN + TEST_PATH + "empty.html" + ); + gBrowser.selectedTab = tab2; + + let browser2 = gBrowser.getBrowserForTab(tab2); + await BrowserTestUtils.browserLoaded(browser2); + + await SpecialPowers.spawn(browser2, [], async _ => { + is(content.document.cookie, "a=b", "Cookie set"); + }); + + info("Removing tabs"); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_denyPermissionForTracker.js b/toolkit/components/antitracking/test/browser/browser_denyPermissionForTracker.js new file mode 100644 index 0000000000..cd19fa4466 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_denyPermissionForTracker.js @@ -0,0 +1,64 @@ +// This test works by setting up an exception for the tracker domain, which +// disables all the anti-tracking tests. + +add_task(async _ => { + PermissionTestUtils.add( + "https://tracking.example.org", + "cookie", + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "https://tracking.example.com", + "cookie", + Services.perms.DENY_ACTION + ); + // Grant interaction permission so we can directly call + // requestStorageAccess from the tracker. + PermissionTestUtils.add( + "https://tracking.example.org", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +AntiTracking._createTask({ + name: "Test that we do honour a cookie permission for nested windows", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + document.cookie = "name=value"; + ok(document.cookie == "", "All is blocked"); + + // requestStorageAccess should reject + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await document + .requestStorageAccess() + .then(() => { + ok(false, "Should not grant storage access"); + }) + .catch(() => { + ok(true, "Should not grant storage access"); + }); + SpecialPowers.wrap(document).clearUserGestureActivation(); + }, + extraPrefs: null, + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js b/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js new file mode 100644 index 0000000000..c15e3abd5f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js @@ -0,0 +1,130 @@ +add_task(async function () { + info("Starting doubly nested tracker test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_3RD_PARTY_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + async function loadSubpage() { + async function runChecks() { + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "I have the cookies!"); + } + + await new Promise(resolve => { + let ifr = document.createElement("iframe"); + ifr.onload = _ => { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(runChecks.toString(), "*"); + }; + + addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + document.body.appendChild(ifr); + ifr.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdParty.html"; + }); + } + + // We need to use the same scheme in Fission test. + let testAnotherThirdPartyPage = SpecialPowers.useRemoteSubframes + ? TEST_ANOTHER_3RD_PARTY_PAGE_HTTPS + : TEST_ANOTHER_3RD_PARTY_PAGE; + + await SpecialPowers.spawn( + browser, + [{ page: testAnotherThirdPartyPage, callback: loadSubpage.toString() }], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = _ => { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.callback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_emailtracking.js b/toolkit/components/antitracking/test/browser/browser_emailtracking.js new file mode 100644 index 0000000000..90616aba6d --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_emailtracking.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +add_setup(async function () { + // Disable other tracking protection feature to avoid interfering with the + // current test. This also setup prefs for testing email tracking. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", false], + ["privacy.trackingprotection.cryptomining.enabled", false], + ["privacy.trackingprotection.emailtracking.enabled", true], + ["privacy.trackingprotection.fingerprinting.enabled", false], + ["privacy.trackingprotection.socialtracking.enabled", false], + [ + "urlclassifier.features.emailtracking.blocklistTables", + "mochitest5-track-simple", + ], + ["urlclassifier.features.emailtracking.allowlistTables", ""], + [ + "urlclassifier.features.emailtracking.datacollection.blocklistTables", + "mochitest5-track-simple", + ], + [ + "urlclassifier.features.emailtracking.datacollection.allowlistTables", + "", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + registerCleanupFunction(_ => { + UrlClassifierTestUtils.cleanupTestTrackers(); + }); +}); + +function runTest(obj) { + add_task(async _ => { + info("Test: " + obj.testName); + + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.trackingprotection.emailtracking.enabled", + obj.protectionEnabled, + ], + [ + "privacy.trackingprotection.emailtracking.pbmode.enabled", + obj.protectionPrivateEnabled, + ], + ], + }); + + let win; + + if (obj.testPrivate) { + win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + } else { + win = window; + } + + info("Creating a non-tracker top-level context"); + let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + info("The non-tracker page opens an email tracker iframe"); + await SpecialPowers.spawn( + browser, + [ + { + image: TEST_EMAIL_TRACKER_DOMAIN + TEST_PATH + "raptor.jpg", + script: TEST_EMAIL_TRACKER_DOMAIN + TEST_PATH + "empty.js", + loading: obj.loading, + }, + ], + async obj => { + info("Image loading ..."); + let loading = await new content.Promise(resolve => { + let image = new content.Image(); + image.src = obj.image + "?" + Math.random(); + image.onload = _ => resolve(true); + image.onerror = _ => resolve(false); + }); + + is(loading, obj.loading, "Image loading expected"); + + let script = content.document.createElement("script"); + script.setAttribute("src", obj.script); + + info("Script loading ..."); + loading = await new content.Promise(resolve => { + script.onload = _ => resolve(true); + script.onerror = _ => resolve(false); + content.document.body.appendChild(script); + }); + + is(loading, obj.loading, "Script loading expected"); + } + ); + + info("Checking content blocking log."); + let contentBlockingLog = JSON.parse(await browser.getContentBlockingLog()); + let origins = Object.keys(contentBlockingLog); + is(origins.length, 1, "There should be one origin entry in the log."); + for (let origin of origins) { + is( + origin + "/", + TEST_EMAIL_TRACKER_DOMAIN, + "Correct tracker origin must be reported" + ); + Assert.deepEqual( + contentBlockingLog[origin], + obj.expectedLogItems, + "Content blocking log should be as expected" + ); + } + + BrowserTestUtils.removeTab(tab); + if (obj.testPrivate) { + await BrowserTestUtils.closeWindow(win); + } + await SpecialPowers.popPrefEnv(); + }); +} + +runTest({ + testName: + "EmailTracking-dataCollection feature enabled but not considered for tracking detection.", + protectionEnabled: false, + protectionPrivateEnabled: false, + loading: true, + expectedLogItems: [ + [ + Ci.nsIWebProgressListener.STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT, + true, + 2, + ], + ], +}); + +runTest({ + testName: "Emailtracking-protection feature enabled.", + protectionEnabled: true, + protectionPrivateEnabled: true, + loading: false, + expectedLogItems: [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_EMAILTRACKING_CONTENT, true, 2], + ], +}); + +runTest({ + testName: + "Emailtracking-protection feature enabled for private windows and doesn't block in normal windows", + protectionEnabled: false, + protectionPrivateEnabled: true, + loading: true, + expectedLogItems: [ + [ + Ci.nsIWebProgressListener.STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT, + true, + 2, + ], + ], +}); + +runTest({ + testName: + "Emailtracking-protection feature enabled for private windows and block in private windows", + testPrivate: true, + protectionEnabled: true, + protectionPrivateEnabled: true, + loading: false, + expectedLogItems: [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_EMAILTRACKING_CONTENT, true, 1], + ], +}); diff --git a/toolkit/components/antitracking/test/browser/browser_existingCookiesForSubresources.js b/toolkit/components/antitracking/test/browser/browser_existingCookiesForSubresources.js new file mode 100644 index 0000000000..35e9dfb169 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_existingCookiesForSubresources.js @@ -0,0 +1,235 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_3RD_PARTY_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info( + "Loading tracking scripts and tracking images before restricting 3rd party cookies" + ); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "1", "Cookies received for images"); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + is(text, "1", "Cookies received for scripts"); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + Services.perms.removeAll(); + + // Now set up our prefs + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + info("Creating a new tab"); + tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [{ page: TEST_3RD_PARTY_PAGE, callback: (async _ => {}).toString() }], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.callback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Loading tracking scripts and tracking images again"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "No cookie received for images."); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "No cookie received received for scripts."); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_fileUrl.js b/toolkit/components/antitracking/test/browser/browser_fileUrl.js new file mode 100644 index 0000000000..509c143a9b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_fileUrl.js @@ -0,0 +1,41 @@ +/** + * Bug 1663192 - Testing for ensuring the top-level window in a fire url is + * treated as first-party. + */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", 1], + ["network.cookie.cookieBehavior.pbmode", 1], + ], + }); +}); + +add_task(async function () { + let dir = getChromeDir(getResolvedURI(gTestPath)); + dir.append("file_localStorage.html"); + const uriString = Services.io.newFileURI(dir).spec; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uriString); + + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + let result = content.document.getElementById("result"); + + is( + result.textContent, + "PASS", + "The localStorage is accessible in top-level window" + ); + + let loadInfo = content.docShell.currentDocumentChannel.loadInfo; + + ok( + !loadInfo.isThirdPartyContextToTopWindow, + "The top-level window shouldn't be third-party" + ); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_firstPartyCookieRejectionHonoursAllowList.js b/toolkit/components/antitracking/test/browser/browser_firstPartyCookieRejectionHonoursAllowList.js new file mode 100644 index 0000000000..d3d06d2950 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_firstPartyCookieRejectionHonoursAllowList.js @@ -0,0 +1,77 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Disabling content blocking for this page"); + gProtectionsHandler.disableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [], async function (obj) { + await new content.Promise(async resolve => { + let document = content.document; + let window = document.defaultView; + + is(document.cookie, "", "No cookies for me"); + + await window + .fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + + document.cookie = "name=value"; + ok(document.cookie.includes("name=value"), "Some cookies for me"); + ok(document.cookie.includes("foopy=1"), "Some cookies for me"); + + await window + .fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-present", "We should have cookies"); + }); + + ok(document.cookie.length, "Some Cookies for me"); + + resolve(); + }); + }); + + info("Enabling content blocking for this page"); + gProtectionsHandler.enableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js new file mode 100644 index 0000000000..a2733f0a53 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js @@ -0,0 +1,218 @@ +/* import-globals-from antitracking_head.js */ + +// This test ensures HasStorageAccess API returns the right value under different +// scenarios. + +var settings = [ + // same-origin no-tracker + { + name: "Test whether same-origin non-tracker frame has storage access", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_DOMAIN_HTTPS + TEST_PATH + "3rdParty.html", + }, + // 3rd-party no-tracker + { + name: "Test whether 3rd-party non-tracker frame has storage access", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_4TH_PARTY_PAGE_HTTPS, + }, + // 3rd-party no-tracker with permission + { + name: "Test whether 3rd-party non-tracker frame has storage access when storage permission is granted before", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_4TH_PARTY_PAGE_HTTPS, + setup: () => { + let type = "3rdPartyFrameStorage^https://not-tracking.example.com"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add( + TEST_DOMAIN_HTTPS, + type, + permission, + expireType, + 0 + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); + }, + }, + // 3rd-party tracker + { + name: "Test whether 3rd-party tracker frame has storage access", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + }, + // 3rd-party tracker with permission + { + name: "Test whether 3rd-party tracker frame has storage access when storage access permission is granted before", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + setup: () => { + let type = "3rdPartyFrameStorage^https://example.org"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add( + TEST_DOMAIN_HTTPS, + type, + permission, + expireType, + 0 + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); + }, + }, + // same-site 3rd-party tracker + { + name: "Test whether same-site 3rd-party tracker frame has storage access", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE_HTTPS, + }, + // same-origin 3rd-party tracker + { + name: "Test whether same-origin 3rd-party tracker frame has storage access", + topPage: TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS + TEST_PATH + "page.html", + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE_HTTPS, + }, + // Insecure 3rd-party tracker + { + name: "Test whether insecure 3rd-party tracker frame has storage access", + topPage: TEST_TOP_PAGE + TEST_PATH + "page.html", + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + }, +]; + +var testCases = [ + { + behavior: BEHAVIOR_ACCEPT, // 0 + hasStorageAccess: [ + true /* same-origin non-tracker */, + true /* 3rd-party non-tracker */, + true /* 3rd-party non-tracker with permission */, + true /* 3rd-party tracker */, + true /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + true /* Insecure context */, + ], + }, + { + behavior: BEHAVIOR_REJECT_FOREIGN, // 1 + hasStorageAccess: [ + true /* same-origin non-tracker */, + false /* 3rd-party non-tracker */, + false /* 3rd-party non-tracker with permission */, + false /* 3rd-party tracker */, + false /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + false /* Insecure context */, + ], + }, + { + behavior: BEHAVIOR_REJECT, // 2 + hasStorageAccess: [ + false /* same-origin non-tracker */, + false /* 3rd-party non-tracker */, + false /* 3rd-party non-tracker with permission */, + false /* 3rd-party tracker */, + false /* 3rd-party tracker with permission */, + false /* same-site tracker */, + false /* same-origin tracker */, + false /* Insecure context */, + ], + }, + { + behavior: BEHAVIOR_LIMIT_FOREIGN, // 3 + hasStorageAccess: [ + true /* same-origin non-tracker */, + false /* 3rd-party non-tracker */, + false /* 3rd-party non-tracker with permission */, + false /* 3rd-party tracker */, + false /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + false /* Insecure context */, + ], + }, + { + behavior: BEHAVIOR_REJECT_TRACKER, // 4 + hasStorageAccess: [ + true /* same-origin non-tracker */, + true /* 3rd-party non-tracker */, + true /* 3rd-party non-tracker with permission */, + false /* 3rd-party tracker */, + false /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + false /* Insecure context */, + ], + }, + { + behavior: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // 5 + hasStorageAccess: [ + true /* same-origin non-tracker */, + false /* 3rd-party non-tracker */, + false /* 3rd-party non-tracker with permission */, + false /* 3rd-party tracker */, + false /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + false /* Insecure context */, + ], + }, +]; + +(function () { + settings.forEach(setting => { + if (setting.setup) { + add_task(async _ => { + setting.setup(); + }); + } + + testCases.forEach(test => { + let callback = test.hasStorageAccess[settings.indexOf(setting)] + ? async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + } + : async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }; + + AntiTracking._createTask({ + name: setting.name, + cookieBehavior: test.behavior, + allowList: false, + callback, + extraPrefs: [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: setting.topPage, + thirdPartyPage: setting.thirdPartyPage, + }); + }); + + add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }); + }); +})(); diff --git a/toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js new file mode 100644 index 0000000000..ea52ff2fad --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js @@ -0,0 +1,227 @@ +/* import-globals-from antitracking_head.js */ + +// This test ensures HasStorageAccess API returns the right value under different +// scenarios. + +var settings = [ + // same-origin no-tracker + { + name: "Test whether same-origin non-tracker frame has storage access", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_DOMAIN_HTTPS + TEST_PATH + "3rdParty.html", + }, + // 3rd-party no-tracker + { + name: "Test whether 3rd-party non-tracker frame has storage access", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_4TH_PARTY_PAGE_HTTPS, + }, + // 3rd-party no-tracker with permission + { + name: "Test whether 3rd-party non-tracker frame has storage access when storage permission is granted before", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_4TH_PARTY_PAGE_HTTPS, + setup: () => { + let type = "3rdPartyFrameStorage^https://not-tracking.example.com"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add( + TEST_DOMAIN_HTTPS, + type, + permission, + expireType, + 0 + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); + }, + }, + // 3rd-party tracker + { + name: "Test whether 3rd-party tracker frame has storage access", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + }, + // 3rd-party tracker with permission + { + name: "Test whether 3rd-party tracker frame has storage access when storage access permission is granted before", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + setup: () => { + let type = "3rdPartyFrameStorage^https://example.org"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add( + TEST_DOMAIN_HTTPS, + type, + permission, + expireType, + 0 + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); + }, + }, + // same-site 3rd-party tracker + { + name: "Test whether same-site 3rd-party tracker frame has storage access", + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE_HTTPS, + }, + // same-origin 3rd-party tracker + { + name: "Test whether same-origin 3rd-party tracker frame has storage access", + topPage: TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS + TEST_PATH + "page.html", + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE_HTTPS, + }, + // Insecure 3rd-party tracker + { + name: "Test whether insecure 3rd-party tracker frame has storage access", + topPage: TEST_TOP_PAGE + TEST_PATH + "page.html", + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + }, +]; + +const allBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL; +const foreignBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN; +const trackerBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER; + +var testCases = [ + { + behavior: BEHAVIOR_ACCEPT, // 0 + cases: [ + [true] /* same-origin non-tracker */, + [true] /* 3rd-party non-tracker */, + [true] /* 3rd-party non-tracker with permission */, + [true] /* 3rd-party tracker */, + [true] /* 3rd-party tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + [true] /* insecure tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_FOREIGN, // 1 + cases: [ + [true] /* same-origin non-tracker */, + [false, foreignBlocked] /* 3rd-party non-tracker */, + [false, foreignBlocked] /* 3rd-party tracker with permission */, + [false, foreignBlocked] /* 3rd-party tracker */, + [false, foreignBlocked] /* 3rd-party non-tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + [false, foreignBlocked] /* insecure tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT, // 2 + cases: [ + [false, allBlocked] /* same-origin non-tracker */, + [false, allBlocked] /* 3rd-party non-tracker */, + [false, allBlocked] /* 3rd-party non-tracker with permission */, + [false, allBlocked] /* 3rd-party tracker */, + [false, allBlocked] /* 3rd-party tracker with permission */, + [false, allBlocked] /* same-site tracker */, + [false, allBlocked] /* same-origin tracker */, + [false, allBlocked] /* insecure tracker */, + ], + }, + { + behavior: BEHAVIOR_LIMIT_FOREIGN, // 3 + cases: [ + [true] /* same-origin non-tracker */, + [false, foreignBlocked] /* 3rd-party non-tracker */, + [false, foreignBlocked] /* 3rd-party non-tracker with permission */, + [false, foreignBlocked] /* 3rd-party tracker */, + [false, foreignBlocked] /* 3rd-party tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + [false, foreignBlocked] /* insecure tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_TRACKER, // 4 + cases: [ + [true] /* same-origin non-tracker */, + [true] /* 3rd-party non-tracker */, + [true] /* 3rd-party non-tracker with permission */, + [false, trackerBlocked] /* 3rd-party tracker */, + [false, trackerBlocked] /* 3rd-party tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + [false, trackerBlocked] /* insecure tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // 5 + cases: [ + [true] /* same-origin non-tracker */, + [false] /* 3rd-party non-tracker */, + [false] /* 3rd-party non-tracker with permission */, + [false, trackerBlocked] /* 3rd-party tracker */, + [false, trackerBlocked] /* 3rd-party tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + [false, trackerBlocked] /* insecure tracker */, + ], + }, +]; + +(function () { + settings.forEach(setting => { + ok(true, JSON.stringify(setting)); + if (setting.setup) { + add_task(async _ => { + setting.setup(); + }); + } + + testCases.forEach(test => { + let [hasStorageAccess, expectedBlockingNotifications] = + test.cases[settings.indexOf(setting)]; + let callback = hasStorageAccess + ? async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + } + : async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }; + + AntiTracking._createTask({ + name: setting.name, + cookieBehavior: test.behavior, + allowList: false, + callback, + extraPrefs: [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + true, + ], + // Testing Storage Access API grants constrained to secure contexts + ["dom.storage_access.dont_grant_insecure_contexts", true], + ], + expectedBlockingNotifications, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: setting.topPage, + thirdPartyPage: setting.thirdPartyPage, + }); + }); + + add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }); + }); +})(); diff --git a/toolkit/components/antitracking/test/browser/browser_iframe_document_open.js b/toolkit/components/antitracking/test/browser/browser_iframe_document_open.js new file mode 100644 index 0000000000..73876ee1a5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_iframe_document_open.js @@ -0,0 +1,86 @@ +// +// Bug 1725996 - Test if the cookie set in a document which is created by +// document.open() in an about:blank iframe has a correct +// partitionKey +// + +const TEST_PAGE = TEST_DOMAIN + TEST_PATH + "file_iframe_document_open.html"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["privacy.dynamic_firstparty.use_site", true], + ], + }); +}); + +add_task(async function test_firstParty_iframe() { + // Clear all cookies before test. + Services.cookies.removeAll(); + + // Open the test page which creates an iframe and then calls document.write() + // to write a cookie in the iframe. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + + // Wait until the cookie appears. + await TestUtils.waitForCondition(_ => Services.cookies.cookies.length); + + // Check the partitionKey in the cookie. + let cookie = Services.cookies.cookies[0]; + is( + cookie.originAttributes.partitionKey, + "", + "The partitionKey should remain empty for first-party iframe." + ); + + // Clean up. + Services.cookies.removeAll(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_thirdParty_iframe() { + // Clear all cookies before test. + Services.cookies.removeAll(); + + // Open a tab with a different domain with the test page. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE_2 + ); + + // Open the test page within a third-party context. + await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_PAGE], + async function (page) { + let ifr = content.document.createElement("iframe"); + let loading = ContentTaskUtils.waitForEvent(ifr, "load"); + ifr.src = page; + content.document.body.appendChild(ifr); + await loading; + } + ); + + // Wait until the cookie appears. + await TestUtils.waitForCondition(_ => Services.cookies.cookies.length); + + // Check the partitionKey in the cookie. + let cookie = Services.cookies.cookies[0]; + is( + cookie.originAttributes.partitionKey, + "(http,xn--exmple-cua.test)", + "The partitionKey should exist for third-party iframe." + ); + + // Clean up. + Services.cookies.removeAll(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_imageCache4.js b/toolkit/components/antitracking/test/browser/browser_imageCache4.js new file mode 100644 index 0000000000..8fcc298cf0 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_imageCache4.js @@ -0,0 +1,13 @@ +let cookieBehavior = BEHAVIOR_REJECT_TRACKER; +let blockingByAllowList = false; +let expectedBlockingNotifications = + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER; + +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + "/"; +} +/* import-globals-from imageCacheWorker.js */ +Services.scriptloader.loadSubScript(rootDir + "imageCacheWorker.js", this); diff --git a/toolkit/components/antitracking/test/browser/browser_imageCache8.js b/toolkit/components/antitracking/test/browser/browser_imageCache8.js new file mode 100644 index 0000000000..b57bf1dca5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_imageCache8.js @@ -0,0 +1,13 @@ +let cookieBehavior = BEHAVIOR_REJECT_TRACKER; +let blockingByAllowList = true; +let expectedBlockingNotifications = + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER; + +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + "/"; +} +/* import-globals-from imageCacheWorker.js */ +Services.scriptloader.loadSubScript(rootDir + "imageCacheWorker.js", this); diff --git a/toolkit/components/antitracking/test/browser/browser_localStorageEvents.js b/toolkit/components/antitracking/test/browser/browser_localStorageEvents.js new file mode 100644 index 0000000000..b46cc60b91 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_localStorageEvents.js @@ -0,0 +1,186 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); +}); + +add_task(async function testLocalStorageEventPropagation() { + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorage.html", + }, + ], + async obj => { + info("Creating tracker iframe"); + + let ifr = content.document.createElement("iframe"); + ifr.src = obj.page; + + await new content.Promise(resolve => { + ifr.onload = function () { + resolve(); + }; + content.document.body.appendChild(ifr); + }); + + info("LocalStorage should be blocked."); + await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + if (e.data.type == "test") { + is(e.data.status, false, "LocalStorage blocked"); + } else { + ok(false, "Unknown message"); + } + resolve(); + }, + { once: true } + ); + ifr.contentWindow.postMessage("test", "*"); + }); + + info("Let's open the popup"); + await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + if (e.data.type == "test") { + is(e.data.status, true, "LocalStorage unblocked"); + } else { + ok(false, "Unknown message"); + } + resolve(); + }, + { once: true } + ); + ifr.contentWindow.postMessage("open", "*"); + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function testBlockedLocalStorageEventPropagation() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorage.html", + }, + ], + async obj => { + info("Creating tracker iframe"); + + let ifr = content.document.createElement("iframe"); + ifr.src = obj.page; + + await new content.Promise(resolve => { + ifr.onload = function () { + resolve(); + }; + content.document.body.appendChild(ifr); + }); + + info("LocalStorage should be blocked."); + await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + if (e.data.type == "test") { + is(e.data.status, false, "LocalStorage blocked"); + } else { + ok(false, "Unknown message"); + } + resolve(); + }, + { once: true } + ); + ifr.contentWindow.postMessage("test", "*"); + }); + + info("Let's open the popup"); + await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + if (e.data.type == "test") { + is(e.data.status, false, "LocalStorage still blocked"); + } else { + ok(false, "Unknown message"); + } + resolve(); + }, + { once: true } + ); + ifr.contentWindow.postMessage("open and test", "*"); + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_onBeforeRequestNotificationForTrackingResources.js b/toolkit/components/antitracking/test/browser/browser_onBeforeRequestNotificationForTrackingResources.js new file mode 100644 index 0000000000..847bfd7dc9 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_onBeforeRequestNotificationForTrackingResources.js @@ -0,0 +1,96 @@ +/** + * This test ensures that onBeforeRequest is dispatched for webRequest loads that + * are blocked by tracking protection. It sets up a page with a third-party script + * resource on it that is blocked by TP, and sets up an onBeforeRequest listener + * which waits to be notified about that resource. The test would time out if the + * onBeforeRequest listener isn't called dispatched before the load is canceled. + */ + +let extension; +add_task(async function () { + extension = ExtensionTestUtils.loadExtension({ + manifest: { permissions: ["webRequest", "webRequestBlocking", "*://*/*"] }, + async background() { + let gExpectedResourcesSeen = 0; + function onBeforeRequest(details) { + let spec = details.url; + browser.test.log("Observed channel for " + spec); + // We would use TEST_3RD_PARTY_DOMAIN_TP here, but the variable is inaccessible + // since it is defined in head.js! + if (!spec.startsWith("https://tracking.example.com/")) { + return undefined; + } + if (spec.endsWith("empty.js")) { + browser.test.succeed("Correct resource observed"); + ++gExpectedResourcesSeen; + } else if (spec.endsWith("empty.js?redirect")) { + return { redirectUrl: spec.replace("empty.js?redirect", "head.js") }; + } else if (spec.endsWith("head.js")) { + ++gExpectedResourcesSeen; + } + if (gExpectedResourcesSeen == 2) { + browser.webRequest.onBeforeRequest.removeListener(onBeforeRequest); + browser.test.sendMessage("finish"); + } + return undefined; + } + + browser.webRequest.onBeforeRequest.addListener( + onBeforeRequest, + { urls: ["*://*/*"] }, + ["blocking"] + ); + browser.test.sendMessage("ready"); + }, + }); + await extension.startup(); + await extension.awaitMessage("ready"); +}); + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", true], + // the test doesn't open a private window, so we don't care about this pref's value + ["privacy.trackingprotection.pbmode.enabled", false], + // tracking annotations aren't needed in this test, only TP is needed + ["privacy.trackingprotection.annotate_channels", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ["privacy.trackingprotection.testing.report_blocked_node", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let promise = extension.awaitMessage("finish"); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_EMBEDDER_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await promise; + + info("Verify the number of tracking nodes found"); + await SpecialPowers.spawn(browser, [{ expected: 3 }], async function (obj) { + is( + content.document.blockedNodeByClassifierCount, + obj.expected, + "Expected tracking nodes found" + ); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + await extension.unload(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_onModifyRequestNotificationForTrackingResources.js b/toolkit/components/antitracking/test/browser/browser_onModifyRequestNotificationForTrackingResources.js new file mode 100644 index 0000000000..0674b87136 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_onModifyRequestNotificationForTrackingResources.js @@ -0,0 +1,96 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +/** + * This test ensures that http-on-modify-request is dispatched for channels that + * are blocked by tracking protection. It sets up a page with a third-party script + * resource on it that is blocked by TP, and sets up an http-on-modify-request + * observer which waits to be notified about that resource. The test would time out + * if the http-on-modify-request notification isn't dispatched before the channel is + * canceled. + */ + +let gExpectedResourcesSeen = 0; +async function onModifyRequest() { + return new Promise((resolve, reject) => { + Services.obs.addObserver(function observer(subject, topic, data) { + let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); + let spec = httpChannel.URI.spec; + info("Observed channel for " + spec); + if (httpChannel.URI.prePath + "/" != TEST_3RD_PARTY_DOMAIN_TP) { + return; + } + if (spec.endsWith("empty.js")) { + ok(true, "Correct resource observed"); + ++gExpectedResourcesSeen; + } else if (spec.endsWith("empty.js?redirect")) { + httpChannel.redirectTo( + Services.io.newURI(spec.replace("empty.js?redirect", "head.js")) + ); + } else if (spec.endsWith("empty.js?redirect2")) { + httpChannel.suspend(); + setTimeout(() => { + httpChannel.redirectTo( + Services.io.newURI(spec.replace("empty.js?redirect2", "head.js")) + ); + httpChannel.resume(); + }, 100); + } else if (spec.endsWith("head.js")) { + ++gExpectedResourcesSeen; + } + if (gExpectedResourcesSeen == 3) { + Services.obs.removeObserver(observer, "http-on-modify-request"); + resolve(); + } + }, "http-on-modify-request"); + }); +} + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", true], + // the test doesn't open a private window, so we don't care about this pref's value + ["privacy.trackingprotection.pbmode.enabled", false], + // tracking annotations aren't needed in this test, only TP is needed + ["privacy.trackingprotection.annotate_channels", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ["privacy.trackingprotection.testing.report_blocked_node", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let promise = onModifyRequest(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_EMBEDDER_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await promise; + + info("Verify the number of tracking nodes found"); + await SpecialPowers.spawn( + browser, + [{ expected: gExpectedResourcesSeen }], + async function (obj) { + is( + content.document.blockedNodeByClassifierCount, + obj.expected, + "Expected tracking nodes found" + ); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js b/toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js new file mode 100644 index 0000000000..7c79ecbe32 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js @@ -0,0 +1,593 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +/** + * Tests that when receiving the "clear-site-data" header - with dFPI enabled - + * we clear storage under the correct partition. + */ + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +const HOST_A = "example.com"; +const HOST_B = "example.org"; +const ORIGIN_A = `https://${HOST_A}`; +const ORIGIN_B = `https://${HOST_B}`; +const CLEAR_SITE_DATA_PATH = `/${TEST_PATH}clearSiteData.sjs`; +const CLEAR_SITE_DATA_URL_ORIGIN_B = ORIGIN_B + CLEAR_SITE_DATA_PATH; +const CLEAR_SITE_DATA_URL_ORIGIN_A = ORIGIN_A + CLEAR_SITE_DATA_PATH; +const THIRD_PARTY_FRAME_ID_ORIGIN_B = "thirdPartyFrame"; +const THIRD_PARTY_FRAME_ID_ORIGIN_A = "thirdPartyFrame2"; +const STORAGE_KEY = "testKey"; + +// Skip localStorage tests when using legacy localStorage. The legacy +// localStorage implementation does not support clearing data by principal. See +// Bug 1688221, Bug 1688665. +const skipLocalStorageTests = Services.prefs.getBoolPref( + "dom.storage.enable_unsupported_legacy_implementation" +); + +/** + * Creates an iframe in the passed browser and waits for it to load. + * @param {Browser} browser - Browser to create the frame in. + * @param {String} src - Frame source url. + * @param {String} id - Frame id. + * @param {boolean} sandbox - Whether the frame should be sandboxed. + * @returns {Promise} - Resolves once the frame has loaded. + */ +function createFrame(browser, src, id, sandbox) { + return SpecialPowers.spawn( + browser, + [{ page: src, frameId: id, sandbox }], + async function (obj) { + await new content.Promise(resolve => { + let frame = content.document.createElement("iframe"); + if (obj.sandbox) { + frame.setAttribute("sandbox", "allow-scripts"); + } + frame.src = obj.page; + frame.id = obj.frameId; + frame.addEventListener("load", resolve, { once: true }); + content.document.body.appendChild(frame); + }); + } + ); +} + +/** + * Creates a new tab, loads a url and creates an iframe. + * Callers need to clean up the tab before the test ends. + * @param {String} firstPartyUrl - Url to load in tab. + * @param {String} thirdPartyUrl - Url to load in frame. + * @param {String} frameId - Id of iframe element. + * @param {boolean} sandbox - Whether the frame should be sandboxed. + * @returns {Promise} - Resolves with the tab and the frame BrowsingContext once + * the tab and the frame have loaded. + */ +async function createTabWithFrame( + firstPartyUrl, + thirdPartyUrl, + frameId, + sandbox +) { + // Create tab and wait for it to be loaded. + let tab = BrowserTestUtils.addTab(gBrowser, firstPartyUrl); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + // Create cross origin iframe. + await createFrame(tab.linkedBrowser, thirdPartyUrl, frameId, sandbox); + + // Return BrowsingContext of created iframe. + return { tab, frameBC: tab.linkedBrowser.browsingContext.children[0] }; +} + +/** + * Test wrapper for the ClearSiteData tests. + * Loads ORIGIN_A and ORIGIN_B in two tabs and inserts a cross origin pointing + * to the other iframe each. + * Both frames ORIGIN_B under ORIGIN_A and ORIGIN_A under ORIGIN_B will be + * storage partitioned. + * Depending on the clearDataContext variable we then either navigate ORIGIN_A + * (as top level) or ORIGIN_B (as third party frame) to the clear-site-data + * endpoint. + * @param {function} cbPreClear - Called after initial setup, once top levels + * and frames have been loaded. + * @param {function} cbPostClear - Called after data has been cleared via the + * "Clear-Site-Data" header. + * @param {("firstParty"|"thirdPartyPartitioned")} clearDataContext - Whether to + * navigate to the path that sets the "Clear-Site-Data" header with the first or + * third party. + * @param {boolean} [sandboxFrame] - Whether the frames should be sandboxed. No + * sandbox by default. + */ +async function runClearSiteDataTest( + cbPreClear, + cbPostClear, + clearDataContext, + sandboxFrame = false +) { + // Create a tabs for origin A and B with cross origins frames B and A + let [ + { frameBC: frameContextB, tab: tabA }, + { frameBC: frameContextA, tab: tabB }, + ] = await Promise.all([ + createTabWithFrame( + ORIGIN_A, + ORIGIN_B, + THIRD_PARTY_FRAME_ID_ORIGIN_B, + sandboxFrame + ), + createTabWithFrame( + ORIGIN_B, + ORIGIN_A, + THIRD_PARTY_FRAME_ID_ORIGIN_A, + sandboxFrame + ), + ]); + + let browserA = tabA.linkedBrowser; + let contextA = browserA.browsingContext; + let browserB = tabB.linkedBrowser; + let contextB = browserB.browsingContext; + + // Run test callback before clear-site-data + if (cbPreClear) { + await cbPreClear(contextA, contextB, frameContextB, frameContextA); + } + + // Navigate to path with clear-site-data header + // Depending on the clearDataContext variable we either do this with the + // top browser or the third party storage partitioned frame (B embedded in A). + info(`Opening path with clear-site-data-header for ${clearDataContext}`); + if (clearDataContext == "firstParty") { + // Open in new tab so we keep our current test tab intact. The + // post-clear-callback might need it. + await BrowserTestUtils.withNewTab(CLEAR_SITE_DATA_URL_ORIGIN_A, () => {}); + } else if (clearDataContext == "thirdPartyPartitioned") { + // Navigate frame to path with clear-site-data header + await SpecialPowers.spawn( + browserA, + [ + { + page: CLEAR_SITE_DATA_URL_ORIGIN_B, + frameId: THIRD_PARTY_FRAME_ID_ORIGIN_B, + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let frame = content.document.getElementById(obj.frameId); + frame.addEventListener("load", resolve, { once: true }); + frame.src = obj.page; + }); + } + ); + } else { + ok(false, "Invalid context requested for clear-site-data"); + } + + if (cbPostClear) { + await cbPostClear(contextA, contextB, frameContextB, frameContextA); + } + + info("Cleaning up."); + BrowserTestUtils.removeTab(tabA); + BrowserTestUtils.removeTab(tabB); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve); + }); +} + +/** + * Create an origin with partitionKey. + * @param {String} originNoSuffix - Origin without origin attributes. + * @param {String} [firstParty] - First party to create partitionKey. + * @returns {String} Origin with suffix. If not passed this will return the + * umodified origin. + */ +function getOrigin(originNoSuffix, firstParty) { + let origin = originNoSuffix; + if (firstParty) { + let [scheme, host] = firstParty.split("://"); + origin += `^partitionKey=(${scheme},${host})`; + } + return origin; +} + +/** + * Sets a storage item for an origin. + * @param {("cookie"|"localStorage")} storageType - Which storage type to use. + * @param {String} originNoSuffix - Context to set storage item in. + * @param {String} [firstParty] - Optional first party domain to partition + * under. + * @param {String} key - Key of the entry. + * @param {String} value - Value of the entry. + */ +function setStorageEntry(storageType, originNoSuffix, firstParty, key, value) { + if (storageType != "cookie" && storageType != "localStorage") { + ok(false, "Invalid storageType passed"); + return; + } + + let origin = getOrigin(originNoSuffix, firstParty); + + if (storageType == "cookie") { + SiteDataTestUtils.addToCookies({ origin, name: key, value }); + return; + } + // localStorage + SiteDataTestUtils.addToLocalStorage(origin, key, value); +} + +/** + * Tests whether a host sets a cookie. + * For the purpose of this test we assume that there is either one or no cookie + * set. + * This performs cookie lookups directly via the cookie service. + * @param {boolean} hasCookie - Whether we expect to see a cookie. + * @param {String} originNoSuffix - Origin the cookie is stored for. + * @param {String|null} firstParty - Whether to test for a partitioned cookie. + * If set this will be used to construct the partitionKey. + * @param {String} [key] - Expected key / name of the cookie. + * @param {String} [value] - Expected value of the cookie. + */ +function testHasCookie(hasCookie, originNoSuffix, firstParty, key, value) { + let origin = getOrigin(originNoSuffix, firstParty); + + let label = `${originNoSuffix}${ + firstParty ? ` (partitioned under ${firstParty})` : "" + }`; + + if (!hasCookie) { + ok( + !SiteDataTestUtils.hasCookies(origin), + `Cookie for ${label} is not set for key ${key}` + ); + return; + } + + ok( + SiteDataTestUtils.hasCookies(origin, [{ key, value }]), + `Cookie for ${label} is set ${key}=${value}` + ); +} + +/** + * Tests whether a context has a localStorage entry. + * @param {boolean} hasEntry - Whether we expect to see an entry. + * @param {String} originNoSuffix - Origin to test localStorage for. + * @param {String} [firstParty] - First party context to test under. + * @param {String} key - key of the localStorage item. + * @param {String} [expectedValue] - Expected value of the item. + */ +function testHasLocalStorageEntry( + hasEntry, + originNoSuffix, + firstParty, + key, + expectedValue +) { + if (key == null) { + ok(false, "localStorage key is mandatory"); + return; + } + let label = `${originNoSuffix}${ + firstParty ? ` (partitioned under ${firstParty})` : "" + }`; + let origin = getOrigin(originNoSuffix, firstParty); + if (hasEntry) { + let hasEntry = SiteDataTestUtils.hasLocalStorage(origin, [ + { key, value: expectedValue }, + ]); + ok( + hasEntry, + `localStorage for ${label} has expected value ${key}=${expectedValue}` + ); + } else { + let hasEntry = SiteDataTestUtils.hasLocalStorage(origin); + ok(!hasEntry, `localStorage for ${label} is not set for key ${key}`); + } +} + +/** + * Sets the initial storage entries used by the storage tests in this file. + * 1. first party ( A ) + * 2. first party ( B ) + * 3. third party partitioned ( B under A) + * 4. third party partitioned ( A under B) + * The entry values reflect which context they are set for. + * @param {("cookie"|"localStorage")} storageType - Storage type to initialize. + */ +async function setupInitialStorageState(storageType) { + if (storageType != "cookie" && storageType != "localStorage") { + ok(false, "Invalid storageType passed"); + return; + } + + // Set a first party entry + setStorageEntry(storageType, ORIGIN_A, null, STORAGE_KEY, "firstParty"); + + // Set a storage entry in the storage partitioned third party frame + setStorageEntry( + storageType, + ORIGIN_B, + ORIGIN_A, + STORAGE_KEY, + "thirdPartyPartitioned" + ); + + // Set a storage entry in the non storage partitioned third party page + setStorageEntry(storageType, ORIGIN_B, null, STORAGE_KEY, "thirdParty"); + + // Set a storage entry in the second storage partitioned third party frame. + setStorageEntry( + storageType, + ORIGIN_A, + ORIGIN_B, + STORAGE_KEY, + "thirdPartyPartitioned2" + ); + + info("Test that storage entries are set for all contexts"); + + if (storageType == "cookie") { + testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty"); + testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty"); + testHasCookie( + true, + ORIGIN_B, + ORIGIN_A, + STORAGE_KEY, + "thirdPartyPartitioned" + ); + testHasCookie( + true, + ORIGIN_A, + ORIGIN_B, + STORAGE_KEY, + "thirdPartyPartitioned2" + ); + return; + } + + testHasLocalStorageEntry(true, ORIGIN_A, null, STORAGE_KEY, "firstParty"); + testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty"); + testHasLocalStorageEntry( + true, + ORIGIN_B, + ORIGIN_A, + STORAGE_KEY, + "thirdPartyPartitioned" + ); + testHasLocalStorageEntry( + true, + ORIGIN_A, + ORIGIN_B, + STORAGE_KEY, + "thirdPartyPartitioned2" + ); +} + +add_setup(async function () { + info("Starting ClearSiteData test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + // Needed for SiteDataTestUtils#hasLocalStorage + ["dom.storage.client_validation", false], + ], + }); +}); + +/** + * Test clearing partitioned cookies via clear-site-data header + * (Cleared via the cookie service). + */ + +/** + * Tests that when a storage partitioned third party frame loads a site with + * "Clear-Site-Data", the cookies are cleared for only that partitioned frame. + */ +add_task(async function cookieClearThirdParty() { + await runClearSiteDataTest( + // Pre Clear-Site-Data + () => setupInitialStorageState("cookie"), + // Post Clear-Site-Data + () => { + info("Testing: First party cookie has not changed"); + testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty"); + info("Testing: Unpartitioned cookie has not changed"); + testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty"); + info("Testing: Partitioned cookie for HOST_B (HOST_A) has been cleared"); + testHasCookie(false, ORIGIN_B, ORIGIN_A); + info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed"); + testHasCookie( + true, + ORIGIN_A, + ORIGIN_B, + STORAGE_KEY, + "thirdPartyPartitioned2" + ); + }, + // Send clear-site-data header in partitioned third party context. + "thirdPartyPartitioned" + ); +}); + +/** + * Tests that when a sandboxed storage partitioned third party frame loads a + * site with "Clear-Site-Data", no cookies are cleared and we don't crash. + * Crash details in Bug 1686938. + */ +add_task(async function cookieClearThirdPartySandbox() { + await runClearSiteDataTest( + // Pre Clear-Site-Data + () => setupInitialStorageState("cookie"), + // Post Clear-Site-Data + () => { + info("Testing: First party cookie has not changed"); + testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty"); + info("Testing: Unpartitioned cookie has not changed"); + testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty"); + info("Testing: Partitioned cookie for HOST_B (HOST_A) has not changed"); + testHasCookie( + true, + ORIGIN_B, + ORIGIN_A, + STORAGE_KEY, + "thirdPartyPartitioned" + ); + info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed"); + testHasCookie( + true, + ORIGIN_A, + ORIGIN_B, + STORAGE_KEY, + "thirdPartyPartitioned2" + ); + }, + // Send clear-site-data header in partitioned third party context. + "thirdPartyPartitioned", + true + ); +}); + +/** + * Tests that when the we load a path with "Clear-Site-Data" at top level, the + * cookies are cleared only in the first party context. + */ +add_task(async function cookieClearFirstParty() { + await runClearSiteDataTest( + // Pre Clear-Site-Data + () => setupInitialStorageState("cookie"), + // Post Clear-Site-Data + () => { + info("Testing: First party cookie has been cleared"); + testHasCookie(false, ORIGIN_A, null); + info("Testing: Unpartitioned cookie has not changed"); + testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty"); + info("Testing: Partitioned cookie for HOST_B (HOST_A) has not changed"); + testHasCookie( + true, + ORIGIN_B, + ORIGIN_A, + STORAGE_KEY, + "thirdPartyPartitioned" + ); + info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed"); + testHasCookie( + true, + ORIGIN_A, + ORIGIN_B, + STORAGE_KEY, + "thirdPartyPartitioned2" + ); + }, + // Send clear-site-data header in first party context. + "firstParty" + ); +}); + +/** + * Test clearing partitioned localStorage via clear-site-data header + * (Cleared via the quota manager). + */ + +/** + * Tests that when a storage partitioned third party frame loads a site with + * "Clear-Site-Data", localStorage is cleared for only that partitioned frame. + */ +add_task(async function localStorageClearThirdParty() { + // Bug 1688221, Bug 1688665. + if (skipLocalStorageTests) { + info("Skipping test"); + return; + } + await runClearSiteDataTest( + // Pre Clear-Site-Data + () => setupInitialStorageState("localStorage"), + // Post Clear-Site-Data + async () => { + info("Testing: First party localStorage has not changed"); + testHasLocalStorageEntry(true, ORIGIN_A, null, STORAGE_KEY, "firstParty"); + info("Testing: Unpartitioned localStorage has not changed"); + testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty"); + info( + "Testing: Partitioned localStorage for HOST_B (HOST_A) has been cleared" + ); + testHasLocalStorageEntry(false, ORIGIN_B, ORIGIN_A, STORAGE_KEY); + info( + "Testing: Partitioned localStorage for HOST_A (HOST_B) has not changed" + ); + testHasLocalStorageEntry( + true, + ORIGIN_A, + ORIGIN_B, + STORAGE_KEY, + "thirdPartyPartitioned2" + ); + }, + // Send clear-site-data header in partitioned third party context. + "thirdPartyPartitioned" + ); +}); + +/** + * Tests that when the we load a path with "Clear-Site-Data" at top level, + * localStorage is cleared only in the first party context. + */ +add_task(async function localStorageClearFirstParty() { + // Bug 1688221, Bug 1688665. + if (skipLocalStorageTests) { + info("Skipping test"); + return; + } + await runClearSiteDataTest( + // Pre Clear-Site-Data + () => setupInitialStorageState("localStorage"), + // Post Clear-Site-Data + () => { + info("Testing: First party localStorage has been cleared"); + testHasLocalStorageEntry(false, ORIGIN_A, null, STORAGE_KEY); + info("Testing: Unpartitioned thirdParty localStorage has not changed"); + testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty"); + info( + "Testing: Partitioned localStorage for HOST_B (HOST_A) has not changed" + ); + testHasLocalStorageEntry( + true, + ORIGIN_B, + ORIGIN_A, + STORAGE_KEY, + "thirdPartyPartitioned" + ); + info( + "Testing: Partitioned localStorage for HOST_A (HOST_B) has not changed" + ); + testHasLocalStorageEntry( + true, + ORIGIN_A, + ORIGIN_B, + STORAGE_KEY, + "thirdPartyPartitioned2" + ); + }, + // Send clear-site-data header in first party context. + "firstParty" + ); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedConsoleMessage.js b/toolkit/components/antitracking/test/browser/browser_partitionedConsoleMessage.js new file mode 100644 index 0000000000..ce74076825 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedConsoleMessage.js @@ -0,0 +1,80 @@ +/* vim: set ts=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/. */ + +/* + * Bug 1759496 - A test to verify if the console message of partitioned storage + * was sent correctly. + */ + +"use strict"; + +add_setup(async function () { + await setCookieBehaviorPref( + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + false + ); +}); + +add_task(async function runTest() { + info("Creating the tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + let consolePromise = new Promise(resolve => { + let consoleListener = { + observe(msg) { + if ( + msg + .QueryInterface(Ci.nsIScriptError) + .category.startsWith("cookiePartitioned") + ) { + Services.console.unregisterListener(consoleListener); + resolve(msg.QueryInterface(Ci.nsIScriptError).errorMessage); + } + }, + }; + + Services.console.registerListener(consoleListener); + }); + + info("Creating the third-party iframe"); + let ifrBC = await SpecialPowers.spawn( + browser, + [TEST_TOP_PAGE_7], + async page => { + let ifr = content.document.createElement("iframe"); + + let loading = ContentTaskUtils.waitForEvent(ifr, "load"); + content.document.body.appendChild(ifr); + ifr.src = page; + await loading; + + return ifr.browsingContext; + } + ); + + info("Write cookie to the third-party iframe to ensure the console message"); + await SpecialPowers.spawn(ifrBC, [], async _ => { + content.document.cookie = "foo"; + }); + + let msg = await consolePromise; + + ok( + msg.startsWith("Partitioned cookie or storage access was provided to"), + "The partitioned console message was sent correctly" + ); + + info("Clean up"); + BrowserTestUtils.removeTab(tab); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedCookies.js b/toolkit/components/antitracking/test/browser/browser_partitionedCookies.js new file mode 100644 index 0000000000..d2d1e87dd4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedCookies.js @@ -0,0 +1,137 @@ +PartitionedStorageHelper.runTestInNormalAndPrivateMode( + "HTTP Cookies", + async (win3rdParty, win1stParty, allowed) => { + await win3rdParty.fetch("cookies.sjs?3rd").then(r => r.text()); + await win3rdParty + .fetch("cookies.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie:foopy=3rd", "3rd party cookie set"); + }); + + await win1stParty.fetch("cookies.sjs?first").then(r => r.text()); + await win1stParty + .fetch("cookies.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie:foopy=first", "First party cookie set"); + }); + + await win3rdParty + .fetch("cookies.sjs") + .then(r => r.text()) + .then(text => { + if (allowed) { + is( + text, + "cookie:foopy=first", + "3rd party has the first party cookie set" + ); + } else { + is( + text, + "cookie:foopy=3rd", + "3rd party has not the first party cookie set" + ); + } + }); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +PartitionedStorageHelper.runTestInNormalAndPrivateMode( + "DOM Cookies", + async (win3rdParty, win1stParty, allowed) => { + win3rdParty.document.cookie = "foo=3rd"; + is(win3rdParty.document.cookie, "foo=3rd", "3rd party cookie set"); + + win1stParty.document.cookie = "foo=first"; + is(win1stParty.document.cookie, "foo=first", "First party cookie set"); + + if (allowed) { + is( + win3rdParty.document.cookie, + "foo=first", + "3rd party has the first party cookie set" + ); + } else { + is( + win3rdParty.document.cookie, + "foo=3rd", + "3rd party has not the first party cookie set" + ); + } + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode( + "Partitioned tabs - DOM Cookies", + "cookies", + + // getDataCallback + async win => { + return win.document.cookie; + }, + + // addDataCallback + async (win, value) => { + win.document.cookie = value; + return true; + }, + + // cleanup + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + true +); + +PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode( + "Partitioned tabs - Network Cookies", + "cookies", + + // getDataCallback + async win => { + return win + .fetch("cookies.sjs") + .then(r => r.text()) + .then(text => { + return text.substring("cookie:foopy=".length); + }); + }, + + // addDataCallback + async (win, value) => { + await win.fetch("cookies.sjs?" + value).then(r => r.text()); + return true; + }, + + // cleanup + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + true +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedDOMCache.js b/toolkit/components/antitracking/test/browser/browser_partitionedDOMCache.js new file mode 100644 index 0000000000..8f901e2977 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedDOMCache.js @@ -0,0 +1,110 @@ +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +PartitionedStorageHelper.runTest( + "DOMCache", + async (win3rdParty, win1stParty, allowed) => { + await win3rdParty.caches.open("wow").then( + _ => { + ok(allowed, "DOM Cache cannot be used!"); + }, + _ => { + ok(!allowed, "DOM Cache cannot be used!"); + } + ); + + await win1stParty.caches.open("wow").then( + _ => { + ok(true, "DOM Cache should be available"); + }, + _ => { + ok(false, "DOM Cache should be available"); + } + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [[APS_PREF, false]], + + { runInSecureContext: true } +); + +PartitionedStorageHelper.runTest( + "DOMCache", + async (win3rdParty, win1stParty, allowed) => { + await win1stParty.caches.open("wow").then( + async cache => { + ok(true, "DOM Cache should be available"); + await cache.add("/"); + }, + _ => { + ok(false, "DOM Cache should be available"); + } + ); + + await win3rdParty.caches.open("wow").then( + async cache => { + ok(true, "DOM Cache can be used!"); + is(undefined, await cache.match("/"), "DOM Cache is partitioned"); + }, + _ => { + ok(false, "DOM Cache cannot be used!"); + } + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [[APS_PREF, true]], + + { runInSecureContext: true } +); + +// Test that DOM cache is also available in PBM. +PartitionedStorageHelper.runTest( + "DOMCache", + async (win3rdParty, win1stParty, allowed) => { + await win1stParty.caches.open("wow").then( + async cache => { + ok(true, "DOM Cache should be available in PBM"); + }, + _ => { + ok(false, "DOM Cache should be available in PBM"); + } + ); + + await win3rdParty.caches.open("wow").then( + async cache => { + ok(true, "DOM Cache should be available in PBM"); + }, + _ => { + ok(false, "DOM Cache should be available in PBM"); + } + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [], + + { runInSecureContext: true, runInPrivateWindow: true } +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedIndexedDB.js b/toolkit/components/antitracking/test/browser/browser_partitionedIndexedDB.js new file mode 100644 index 0000000000..70dd0b03db --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedIndexedDB.js @@ -0,0 +1,95 @@ +PartitionedStorageHelper.runTest( + "IndexedDB", + async (win3rdParty, win1stParty, allowed) => { + await new Promise(resolve => { + let a = win1stParty.indexedDB.open("test", 1); + ok(!!a, "IDB should not be blocked in 1st party contexts"); + + a.onsuccess = e => { + let db = e.target.result; + is(db.objectStoreNames.length, 1, "We have 1 objectStore"); + is(db.objectStoreNames[0], "foobar", "We have 'foobar' objectStore"); + resolve(); + }; + + a.onupgradeneeded = e => { + let db = e.target.result; + is(db.objectStoreNames.length, 0, "We have 0 objectStores"); + db.createObjectStore("foobar", { keyPath: "test" }); + }; + }); + + await new Promise(resolve => { + let a = win3rdParty.indexedDB.open("test", 1); + ok(!!a, "IDB should not be blocked in 3rd party contexts"); + + a.onsuccess = e => { + let db = e.target.result; + + is(db.objectStoreNames.length, 0, "We have 0 objectStore"); + resolve(); + }; + }); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +PartitionedStorageHelper.runPartitioningTest( + "Partitioned tabs - IndexedDB", + "indexeddb", + + // getDataCallback + async win => { + return new Promise(resolve => { + let a = win.indexedDB.open("test", 1); + + a.onupgradeneeded = e => { + let db = e.target.result; + db.createObjectStore("foobar", { keyPath: "id" }); + }; + + a.onsuccess = e => { + let db = e.target.result; + db.transaction("foobar").objectStore("foobar").get(1).onsuccess = + ee => { + resolve( + ee.target.result === undefined ? "" : ee.target.result.value + ); + }; + }; + }); + }, + + // addDataCallback + async (win, value) => { + return new Promise(resolve => { + let a = win.indexedDB.open("test", 1); + + a.onsuccess = e => { + let db = e.target.result; + db + .transaction("foobar", "readwrite") + .objectStore("foobar") + .put({ id: 1, value }).onsuccess = _ => { + resolve(true); + }; + }; + }); + }, + + // cleanup + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js new file mode 100644 index 0000000000..fe45970132 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js @@ -0,0 +1,115 @@ +AntiTracking.runTestInNormalAndPrivateMode( + "localStorage and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + is(localStorage.foo, "42", "The value matches"); + hasThrown = false; + } catch (e) { + is(e.name, "SecurityError", "We want a security error message."); + hasThrown = true; + } + + is(hasThrown, shouldThrow, "LocalStorage has been exposed correctly"); + + let prevLocalStorage; + if (!shouldThrow) { + prevLocalStorage = localStorage; + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + if (shouldThrow) { + try { + is(localStorage.foo, undefined, "Undefined value after."); + ok(false, "localStorage should not be available"); + } catch (e) { + ok(true, "localStorage should not be available"); + } + } else { + ok(localStorage != prevLocalStorage, "We have a new localStorage"); + is(localStorage.foo, undefined, "Undefined value after."); + + localStorage.foo = 42; + ok(true, "LocalStorage is still allowed"); + is(localStorage.foo, "42", "The value matches"); + } + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + is(localStorage.foo, "42", "The value matches"); + + var prevLocalStorage = localStorage; + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + ok(localStorage == prevLocalStorage, "We have a new localStorage"); + is(localStorage.foo, "42", "The value matches"); + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,tracking.example.com", + ], + ], + false, + false +); + +PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode( + "Partitioned tabs - localStorage", + "localstorage", + + // getDataCallback + async win => { + return "foo" in win.localStorage ? win.localStorage.foo : ""; + }, + + // addDataCallback + async (win, value) => { + win.localStorage.foo = value; + return true; + }, + + // cleanup + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js new file mode 100644 index 0000000000..14ca62d062 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js @@ -0,0 +1,1014 @@ +function log(test) { + if ("iteration" in test) { + info( + `Running test with prefValue: ${test.prefValue} (Test #${ + test.iteration + 1 + })` + ); + test.iteration++; + } else { + test.iteration = 0; + log(test); + } +} + +function runAllTests(prefValue) { + const storagePrincipalTest = + prefValue == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER; + const dynamicFPITest = + prefValue == + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + + const test = { dynamicFPITest, prefValue }; + + let thirdPartyDomain; + if (storagePrincipalTest) { + thirdPartyDomain = TEST_3RD_PARTY_DOMAIN; + } + if (dynamicFPITest) { + thirdPartyDomain = TEST_4TH_PARTY_DOMAIN; + } + ok(thirdPartyDomain, "Sanity check"); + + // A same origin (and same-process via setting "dom.ipc.processCount" to 1) + // top-level window with access to real localStorage does not share storage + // with an ePartitionOrDeny iframe that should have PartitionedLocalStorage and + // no storage events are received in either direction. (Same-process in order + // to avoid having to worry about any e10s propagation issues.) + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", prefValue], + ["network.cookie.cookieBehavior.pbmode", prefValue], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("Creating a tracker top-level context"); + let trackerTab = BrowserTestUtils.addTab( + gBrowser, + thirdPartyDomain + TEST_PATH + "page.html" + ); + let trackerBrowser = gBrowser.getBrowserForTab(trackerTab); + await BrowserTestUtils.browserLoaded(trackerBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok( + value.startsWith("tracker-"), + "The value is correctly set by the tracker" + ); + } + ); + + info("The tracker page should not have received events"); + await SpecialPowers.spawn(trackerBrowser, [], async _ => { + is(content.localStorage.foo, undefined, "Undefined value!"); + content.localStorage.foo = "normal-" + Math.random(); + }); + + info("Let's see if non-tracker page has received events"); + await SpecialPowers.spawn(normalBrowser, [], async _ => { + let ifr = content.document.getElementById("ifr"); + + info("Getting the value..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok( + value.startsWith("tracker-"), + "The value is correctly set by the tracker" + ); + + info("Getting the events..."); + let events = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getEvents", "*"); + }); + + is(events, 0, "No events"); + }); + + BrowserTestUtils.removeTab(trackerTab); + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Two ePartitionOrDeny iframes in the same tab in the same origin see the + // same localStorage values but no storage events are received from each other + // if dFPI is disabled. + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", prefValue], + ["network.cookie.cookieBehavior.pbmode", prefValue], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + dynamicFPITest: test.dynamicFPITest, + }, + ], + async obj => { + let ifr1 = content.document.createElement("iframe"); + ifr1.setAttribute("id", "ifr1"); + ifr1.setAttribute("src", obj.page); + + info("Iframe 1 loading..."); + await new content.Promise(resolve => { + ifr1.onload = resolve; + content.document.body.appendChild(ifr1); + }); + + let ifr2 = content.document.createElement("iframe"); + ifr2.setAttribute("id", "ifr2"); + ifr2.setAttribute("src", obj.page); + + info("Iframe 2 loading..."); + await new content.Promise(resolve => { + ifr2.onload = resolve; + content.document.body.appendChild(ifr2); + }); + + info("Setting localStorage value in ifr1..."); + ifr1.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr1..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr1.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr1"); + + info("Getting the value from ifr2..."); + value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr2.contentWindow.postMessage("getValue", "*"); + }); + + if (obj.dynamicFPITest) { + ok( + value.startsWith("tracker-"), + "The value is correctly set in ifr2" + ); + } else { + is(value, null, "The value is not set in ifr2"); + } + + info("Getting the events received by ifr2..."); + let events = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr2.contentWindow.postMessage("getEvents", "*"); + }); + + if (obj.dynamicFPITest) { + is(events, 1, "one event"); + } else { + is(events, 0, "No events"); + } + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Same as the previous test but with a cookie behavior of BEHAVIOR_ACCEPT + // instead of BEHAVIOR_REJECT_TRACKER so the iframes get real, persistent + // localStorage instead of partitioned localStorage. + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_ACCEPT, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr1 = content.document.createElement("iframe"); + ifr1.setAttribute("id", "ifr1"); + ifr1.setAttribute("src", obj.page); + + info("Iframe 1 loading..."); + await new content.Promise(resolve => { + ifr1.onload = resolve; + content.document.body.appendChild(ifr1); + }); + + let ifr2 = content.document.createElement("iframe"); + ifr2.setAttribute("id", "ifr2"); + ifr2.setAttribute("src", obj.page); + + info("Iframe 2 loading..."); + await new content.Promise(resolve => { + ifr2.onload = resolve; + content.document.body.appendChild(ifr2); + }); + + info("Setting localStorage value in ifr1..."); + ifr1.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr1..."); + let value1 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr1.contentWindow.postMessage("getValue", "*"); + }); + + ok(value1.startsWith("tracker-"), "The value is correctly set in ifr1"); + + info("Getting the value from ifr2..."); + let value2 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr2.contentWindow.postMessage("getValue", "*"); + }); + + is(value2, value1, "The values match"); + + info("Getting the events received by ifr2..."); + let events = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr2.contentWindow.postMessage("getEvents", "*"); + }); + + is(events, 1, "One event"); + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // An ePartitionOrDeny iframe navigated between two distinct pages on the same + // origin does not see the values stored by the previous iframe. + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", prefValue], + ["network.cookie.cookieBehavior.pbmode", prefValue], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + dynamicFPITest: test.dynamicFPITest, + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + + info("Navigate..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + ifr.setAttribute("src", obj.page + "?" + Math.random()); + }); + + info("Getting the value from ifr..."); + let value2 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + if (obj.dynamicFPITest) { + is(value, value2, "The value is received"); + } else { + is(value2, null, "The value is undefined"); + } + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Like the previous test, but accepting trackers + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_ACCEPT, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + + info("Navigate..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + ifr.setAttribute("src", obj.page + "?" + Math.random()); + }); + + info("Getting the value from ifr..."); + let value2 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + is(value, value2, "The value is received"); + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // An ePartitionOrDeny iframe on the same origin that is navigated to itself + // via window.location.reload() or equivalent does not see the values stored + // by its previous self. + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", prefValue], + ["network.cookie.cookieBehavior.pbmode", prefValue], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + dynamicFPITest: test.dynamicFPITest, + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + + info("Reload..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + ifr.contentWindow.postMessage("reload", "*"); + }); + + info("Getting the value from ifr..."); + let value2 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + if (obj.dynamicFPITest) { + is(value, value2, "The value is equal"); + } else { + is(value2, null, "The value is undefined"); + } + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Like the previous test, but accepting trackers + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_ACCEPT, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + + info("Reload..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + ifr.contentWindow.postMessage("reload", "*"); + }); + + info("Getting the value from ifr..."); + let value2 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + is(value, value2, "The value is equal"); + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // An ePartitionOrDeny iframe on different top-level domain tabs + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", prefValue], + ["network.cookie.cookieBehavior.pbmode", prefValue], + ["privacy.firstparty.isolate", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + let result1 = await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + return value; + } + ); + ok(result1.startsWith("tracker-"), "The value is correctly set in tab1"); + + info("Creating a non-tracker top-level context"); + let normalTab2 = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE_2); + let normalBrowser2 = gBrowser.getBrowserForTab(normalTab2); + await BrowserTestUtils.browserLoaded(normalBrowser2); + + info("The non-tracker page opens a tracker iframe"); + let result2 = await SpecialPowers.spawn( + normalBrowser2, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + return value; + } + ); + + ok(!result2, "The value is null"); + + BrowserTestUtils.removeTab(normalTab); + BrowserTestUtils.removeTab(normalTab2); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Like the previous test, but accepting trackers + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_ACCEPT, + ], + ["privacy.firstparty.isolate", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + let result1 = await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + return value; + } + ); + ok(result1.startsWith("tracker-"), "The value is correctly set in tab1"); + + info("Creating a non-tracker top-level context"); + let normalTab2 = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE_2); + let normalBrowser2 = gBrowser.getBrowserForTab(normalTab2); + await BrowserTestUtils.browserLoaded(normalBrowser2); + + info("The non-tracker page opens a tracker iframe"); + let result2 = await SpecialPowers.spawn( + normalBrowser2, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + return value; + } + ); + + is(result1, result2, "The value is undefined"); + + BrowserTestUtils.removeTab(normalTab); + BrowserTestUtils.removeTab(normalTab2); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Cleanup data. + add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }); +} + +for (let pref of [ + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, +]) { + runAllTests(pref); +} diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedLockManager.js b/toolkit/components/antitracking/test/browser/browser_partitionedLockManager.js new file mode 100644 index 0000000000..21c3c9637d --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedLockManager.js @@ -0,0 +1,30 @@ +/* import-globals-from partitionedstorage_head.js */ + +PartitionedStorageHelper.runTest( + "LockManager works in both first and third party contexts", + async (win3rdParty, win1stParty, allowed) => { + let locks = []; + ok(win1stParty.isSecureContext, "1st party is in a secure context"); + ok(win3rdParty.isSecureContext, "3rd party is in a secure context"); + await win1stParty.navigator.locks.request("foo", lock => { + locks.push(lock); + ok(true, "locks.request succeeded for 1st party"); + }); + + await win3rdParty.navigator.locks.request("foo", lock => { + locks.push(lock); + ok(true, "locks.request succeeded for 3rd party"); + }); + + is(locks.length, 2, "We should have granted 2 lock requests at this point"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + /* extraPrefs */ undefined, + { runInSecureContext: true } +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedMessaging.js b/toolkit/components/antitracking/test/browser/browser_partitionedMessaging.js new file mode 100644 index 0000000000..683b1cc874 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedMessaging.js @@ -0,0 +1,20 @@ +PartitionedStorageHelper.runTestInNormalAndPrivateMode( + "BroadcastChannel", + async (win3rdParty, win1stParty, allowed) => { + let a = new win3rdParty.BroadcastChannel("hello"); + ok(!!a, "BroadcastChannel should be created by 3rd party iframe"); + + let b = new win1stParty.BroadcastChannel("hello"); + ok(!!b, "BroadcastChannel should be created by 1st party iframe"); + + // BroadcastChannel uses the incument global, this means that its CTOR will + // always use the 3rd party iframe's window as global. + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js b/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js new file mode 100644 index 0000000000..5fca844dfe --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js @@ -0,0 +1,695 @@ +/* import-globals-from storageAccessAPIHelpers.js */ + +PartitionedStorageHelper.runTest( + "ServiceWorkers - disable partitioning", + async (win3rdParty, win1stParty, allowed) => { + // Partitioned serviceWorkers are disabled in third-party context. + await win3rdParty.navigator.serviceWorker.register("empty.js").then( + _ => { + ok(allowed, "Success: ServiceWorker cannot be used!"); + }, + _ => { + ok(!allowed, "Failed: ServiceWorker cannot be used!"); + } + ); + + await win1stParty.navigator.serviceWorker.register("empty.js").then( + _ => { + ok(true, "Success: ServiceWorker should be available!"); + }, + _ => { + ok(false, "Failed: ServiceWorker should be available!"); + } + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", false], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - enable partitioning", + async (win3rdParty, win1stParty, allowed) => { + // Partitioned serviceWorkers are enabled in third-party context. + await win3rdParty.navigator.serviceWorker.register("empty.js").then( + _ => { + ok( + true, + "Success: ServiceWorker should be available in third parties." + ); + }, + _ => { + ok( + false, + "Failed: ServiceWorker should be available in third parties." + ); + } + ); + + await win1stParty.navigator.serviceWorker.register("empty.js").then( + _ => { + ok(true, "Success: ServiceWorker should be available!"); + }, + _ => { + ok(false, "Failed: ServiceWorker should be available!"); + } + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - MatchAll", + async (win3rdParty, win1stParty, allowed) => { + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker(win1stParty, "matchAll.js"); + } + + let msgPromise = new Promise(resolve => { + win1stParty.navigator.serviceWorker.addEventListener("message", msg => { + resolve(msg.data); + }); + }); + + win1stParty.sw.postMessage(win3rdParty.location.href); + let msg = await msgPromise; + + // The service worker will always be partitioned. So, the first party window + // won't have control on the third-party window. + is( + false, + msg, + "We won't have the 3rd party window controlled regardless of StorageAccess." + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Partition ScriptContext", + async (win3rdParty, win1stParty, allowed) => { + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Set a script value to first-party service worker. + let res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { + type: "SetScriptValue", + value: "1stParty", + } + ); + ok(res.result, "OK", "Set script value to first-party service worker."); + + // Set a script value to third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { + type: "SetScriptValue", + value: "3rdParty", + } + ); + ok(res.result, "OK", "Set script value to third-party service worker."); + + // Get and check script value from the first-party service worker. + res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "GetScriptValue" } + ); + is( + res.value, + "1stParty", + "The script value in first party window is correct" + ); + + // Get and check script value from the third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "GetScriptValue" } + ); + + is( + res.value, + "3rdParty", + "The script value in third party window is correct" + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Partition DOM Cache", + async (win3rdParty, win1stParty, allowed) => { + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Set DOM cache to first-party service worker. + let res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { + type: "SetCache", + value: "1stParty", + } + ); + ok(res.result, "OK", "Set cache to first-party service worker."); + + // Set DOM cache to third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { + type: "SetCache", + value: "3rdParty", + } + ); + ok(res.result, "OK", "Set cache to third-party service worker."); + + // Check DOM cache from the first-party service worker. + res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "HasCache", value: "1stParty" } + ); + ok( + res.value, + "The '1stParty' cache storage should exist for the first-party window." + ); + res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "HasCache", value: "3rdParty" } + ); + ok( + !res.value, + "The '3rdParty' cache storage should not exist for the first-party window." + ); + + // Check DOM cache from the third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "HasCache", value: "1stParty" } + ); + + ok( + !res.value, + "The '1stParty' cache storage should not exist for the third-party window." + ); + + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "HasCache", value: "3rdParty" } + ); + + ok( + res.value, + "The '3rdParty' cache storage should exist for the third-party window." + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Partition IndexedDB", + async (win3rdParty, win1stParty, allowed) => { + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Set indexedDB value to first-party service worker. + let res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { + type: "SetIndexedDB", + value: "1stParty", + } + ); + ok(res.result, "OK", "Set cache to first-party service worker."); + + // Set indexedDB value to third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { + type: "SetIndexedDB", + value: "3rdParty", + } + ); + ok(res.result, "OK", "Set cache to third-party service worker."); + + // Get and check indexedDB value from the first-party service worker. + res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "GetIndexedDB" } + ); + is( + res.value, + "1stParty", + "The indexedDB value in first party window is correct" + ); + + // Get and check indexedDB from the third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "GetIndexedDB" } + ); + + is( + res.value, + "3rdParty", + "The indexedDB value in third party window is correct" + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Partition Intercept", + async (win3rdParty, win1stParty, allowed) => { + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Fetch a resource in the first-party window. + await win1stParty.fetch("empty.js"); + + // Check that only the first-party service worker gets fetch event. + let res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "GetFetchURL" } + ); + is( + res.value, + "https://not-tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js", + "The first-party service worker received fetch event." + ); + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "GetFetchURL" } + ); + is( + res.value, + "", + "The third-party service worker received no fetch event." + ); + + // Fetch a resource in the third-party window. + await win3rdParty.fetch("empty.js"); + + // Check if the fetch event only happens in third-party service worker. + res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "GetFetchURL" } + ); + is( + res.value, + "", + "The first-party service worker received no fetch event." + ); + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "GetFetchURL" } + ); + is( + res.value, + "https://not-tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js", + "The third-party service worker received fetch event." + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +// Bug1743236 - Verify the content process won't crash if we create a dedicated +// worker in a service worker controlled third-party page with Storage Access. +PartitionedStorageHelper.runTest( + "ServiceWorkers - Create Dedicated Worker", + async (win3rdParty, win1stParty, allowed) => { + // We only do this test when the storage access is granted. + if (!allowed) { + return; + } + + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Create a dedicated worker in first-party window. + let firstPartyWorker = new win1stParty.Worker("dedicatedWorker.js"); + + // Post a message to the dedicated worker and wait until the message circles + // back. + await new Promise(resolve => { + firstPartyWorker.addEventListener("message", msg => { + if (msg.data == "1stParty") { + resolve(); + } + }); + + firstPartyWorker.postMessage("1stParty"); + }); + + // Create a dedicated worker in third-party window. + let thirdPartyWorker = new win3rdParty.Worker("dedicatedWorker.js"); + + // Post a message to the dedicated worker and wait until the message circles + // back. + await new Promise(resolve => { + thirdPartyWorker.addEventListener("message", msg => { + if (msg.data == "3rdParty") { + resolve(); + } + }); + + thirdPartyWorker.postMessage("3rdParty"); + }); + + firstPartyWorker.terminate(); + thirdPartyWorker.terminate(); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +// Bug1768193 - Verify the parent process won't crash if we create a shared +// worker in a service worker controlled third-party page with Storage Access. +PartitionedStorageHelper.runTest( + "ServiceWorkers - Create Shared Worker", + async (win3rdParty, win1stParty, allowed) => { + // We only do this test when the storage access is granted. + if (!allowed) { + return; + } + + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Create a shared worker in third-party window. + let thirdPartyWorker = new win3rdParty.SharedWorker("sharedWorker.js"); + + // Post a message to the dedicated worker and wait until the message circles + // back. + await new Promise(resolve => { + thirdPartyWorker.port.onmessage = msg => { + resolve(); + }; + thirdPartyWorker.onerror = _ => { + ok(false, "We should not be here"); + resolve(); + }; + thirdPartyWorker.port.postMessage("count"); + }); + + thirdPartyWorker.port.postMessage("close"); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Private Browsing with partitioning disabled", + async (win3rdParty, win1stParty, allowed) => { + // Partitioned serviceWorkers are disabled in third-party context. + ok( + !win3rdParty.navigator.serviceWorker, + "ServiceWorker should not be available" + ); + ok( + !win1stParty.navigator.serviceWorker, + "ServiceWorker should not be available" + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", false], + ], + + { + runInPrivateWindow: true, + } +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Private Browsing with partitioning enabled", + async (win3rdParty, win1stParty, allowed) => { + // Partitioned serviceWorkers are disabled in third-party context. + ok( + !win3rdParty.navigator.serviceWorker, + "ServiceWorker should not be available" + ); + ok( + !win1stParty.navigator.serviceWorker, + "ServiceWorker should not be available" + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ], + + { + runInPrivateWindow: true, + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js b/toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js new file mode 100644 index 0000000000..97c58c5217 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js @@ -0,0 +1,76 @@ +PartitionedStorageHelper.runTestInNormalAndPrivateMode( + "SharedWorkers", + async (win3rdParty, win1stParty, allowed) => { + // This test fails if run with an HTTPS 3rd-party URL because the shared worker + // which would start from the window opened from 3rdPartyStorage.html will become + // secure context and per step 11.4.3 of + // https://html.spec.whatwg.org/multipage/workers.html#dom-sharedworker attempting + // to run the SharedWorker constructor would emit an error event. + is( + win3rdParty.location.protocol, + "http:", + "Our 3rd party URL shouldn't be HTTPS" + ); + + let sh1 = new win1stParty.SharedWorker("sharedWorker.js"); + await new Promise(resolve => { + sh1.port.onmessage = e => { + is(e.data, 1, "We expected 1 connection"); + resolve(); + }; + sh1.port.postMessage("count"); + }); + + let sh3 = new win3rdParty.SharedWorker("sharedWorker.js"); + await new Promise(resolve => { + sh3.port.onmessage = e => { + is(e.data, 1, `We expected 1 connection for 3rd party SharedWorker`); + resolve(); + }; + sh3.onerror = _ => { + ok(false, "We should not be here"); + resolve(); + }; + sh3.port.postMessage("count"); + }); + + sh1.port.postMessage("close"); + sh3.port.postMessage("close"); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [], + false +); + +PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode( + "Partitioned tabs - SharedWorker", + "sharedworker", + + // getDataCallback + async win => { + win.sh = new win.SharedWorker("partitionedSharedWorker.js"); + return new Promise(resolve => { + win.sh.port.onmessage = e => { + resolve(e.data); + }; + win.sh.port.postMessage({ what: "get" }); + }); + }, + + // addDataCallback + async (win, value) => { + win.sh = new win.SharedWorker("partitionedSharedWorker.js"); + win.sh.port.postMessage({ what: "put", value }); + return true; + }, + + // cleanup + async _ => {} +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionkey_bloburl.js b/toolkit/components/antitracking/test/browser/browser_partitionkey_bloburl.js new file mode 100644 index 0000000000..3dc3f36742 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionkey_bloburl.js @@ -0,0 +1,193 @@ +const BASE_URI = + "https://example.net/browser/toolkit/components/antitracking/test/browser/blobPartitionPage.html"; +const EMPTY_URI = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "https://example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.bloburl_per_partition_key", true]], + }); +}); + +// Ensuring Blob URL cannot be resolved under a different +// top-level domain other than its original creation top-level domain +add_task(async function test_different_tld_with_iframe() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + let blobURL = await SpecialPowers.spawn(browser1, [], function () { + return content.URL.createObjectURL(new content.Blob(["hello world!"])); + }); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, EMPTY_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + await SpecialPowers.spawn( + browser2, + [ + { + page: BASE_URI, + blob: blobURL, + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + let value = await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data == "error"); + }, + { once: true } + ); + ifr.contentWindow.postMessage(obj.blob, "*"); + }); + + ok(value, "Blob URL was unable to be resolved"); + } + ); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Ensuring if Blob URL can be resolved if a domain1 creates a blob URL +// and domain1 trys to resolve blob URL within an iframe of itself +add_task(async function test_same_tld_with_iframe() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + let blobURL = await SpecialPowers.spawn(browser1, [], function () { + return content.URL.createObjectURL(new content.Blob(["hello world!"])); + }); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + await SpecialPowers.spawn( + browser2, + [ + { + page: BASE_URI, + blob: blobURL, + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + let value = await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data == "hello world!"); + }, + { once: true } + ); + ifr.contentWindow.postMessage(obj.blob, "*"); + }); + + ok(value, "Blob URL was able to be resolved"); + } + ); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Ensuring Blob URL can be resolved in an iframe +// under the same top-level domain where it creates. +add_task(async function test_no_iframes_same_tld() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let blobURL = await SpecialPowers.spawn(browser1, [], function () { + return content.URL.createObjectURL(new content.Blob(["hello world!"])); + }); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn( + browser2, + [blobURL], + function (blobURL) { + return new content.Promise(resolve => { + var xhr = new content.XMLHttpRequest(); + xhr.open("GET", blobURL); + xhr.onloadend = function () { + resolve(xhr.response == "hello world!"); + }; + + xhr.send(); + }); + } + ); + + ok(status, "Blob URL was able to be resolved"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Ensuring Blob URL can be resolved in a sandboxed +// iframe under the top-level domain where it creates. +add_task(async function test_same_tld_with_iframe() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + let blobURL = await SpecialPowers.spawn(browser1, [], function () { + return content.URL.createObjectURL(new content.Blob(["hello world!"])); + }); + + await SpecialPowers.spawn( + browser1, + [ + { + page: BASE_URI, + blob: blobURL, + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("sandbox", "allow-scripts allow-same-origin"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + let value = await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data == "hello world!"); + }, + { once: true } + ); + ifr.contentWindow.postMessage(obj.blob, "*"); + }); + + ok(value, "Blob URL was able to be resolved"); + } + ); + + BrowserTestUtils.removeTab(tab1); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js new file mode 100644 index 0000000000..69c5902a91 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js @@ -0,0 +1,112 @@ +AntiTracking.runTest( + "Test whether we receive any persistent permissions in normal windows", + // Blocking callback + async _ => { + // Nothing to do here! + }, + + // Non blocking callback + async _ => { + try { + // We load the test script in the parent process to check permissions. + let chromeScript = SpecialPowers.loadChromeScript(_ => { + /* eslint-env mozilla/chrome-script */ + addMessageListener("go", _ => { + function ok(what, msg) { + sendAsyncMessage("ok", { what: !!what, msg }); + } + + function is(a, b, msg) { + ok(a === b, msg); + } + + // We should use the principal of the TEST_DOMAIN since the storage + // permission is saved under it. + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.net/" + ); + + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if ( + !perm.type.startsWith("3rdPartyStorage^") && + !perm.type.startsWith("3rdPartyFrameStorage^") + ) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_TIME, + "Permission must expire at a specific time" + ); + ok(perm.expireTime > 0, "Permission must have a expiry time"); + } + + sendAsyncMessage("done"); + }); + }); + + chromeScript.addMessageListener("ok", obj => { + ok(obj.what, obj.msg); + }); + + await new Promise(resolve => { + chromeScript.addMessageListener("done", _ => { + chromeScript.destroy(); + resolve(); + }); + + chromeScript.sendAsyncMessage("go"); + }); + + // We check the permission in tracking processes for non-Fission mode. In + // Fission mode, the permission won't be synced to the tracking process, + // so we don't check it. + if (!SpecialPowers.useRemoteSubframes) { + let Services = SpecialPowers.Services; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.net/" + ); + + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if ( + !perm.type.startsWith("3rdPartyStorage^") && + !perm.type.startsWith("3rdPartyFrameStorage^") + ) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_TIME, + "Permission must expire at a specific time" + ); + ok(perm.expireTime > 0, "Permission must have a expiry time"); + } + } + } catch (e) { + alert(e); + } + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], // extra prefs + true, // run the window.open() test + true, // run the user interaction test + 0, // don't expect blocking notifications + false +); // run in normal windows diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js new file mode 100644 index 0000000000..99c40bb0de --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js @@ -0,0 +1,111 @@ +AntiTracking.runTest( + "Test whether we receive any persistent permissions in normal windows", + // Blocking callback + async _ => { + // Nothing to do here! + }, + + // Non blocking callback + async _ => { + try { + // We load the test script in the parent process to check permissions. + let chromeScript = SpecialPowers.loadChromeScript(_ => { + /* eslint-env mozilla/chrome-script */ + addMessageListener("go", _ => { + function ok(what, msg) { + sendAsyncMessage("ok", { what: !!what, msg }); + } + + function is(a, b, msg) { + ok(a === b, msg); + } + + // We should use the principal of the TEST_DOMAIN since the storage + // permission is saved under it. + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.net/" + ); + + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if ( + !perm.type.startsWith("3rdPartyStorage^") && + !perm.type.startsWith("3rdPartyFrameStorage^") + ) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_TIME, + "Permission must expire at a specific time" + ); + ok(perm.expireTime > 0, "Permission must have a expiry time"); + } + + sendAsyncMessage("done"); + }); + }); + + chromeScript.addMessageListener("ok", obj => { + ok(obj.what, obj.msg); + }); + + await new Promise(resolve => { + chromeScript.addMessageListener("done", _ => { + chromeScript.destroy(); + resolve(); + }); + + chromeScript.sendAsyncMessage("go"); + }); + + // We check the permission in tracking processes for non-Fission mode. In + // Fission mode, the permission won't be synced to the tracking process, + // so we don't check it. + if (!SpecialPowers.useRemoteSubframes) { + let Services = SpecialPowers.Services; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.net/" + ); + + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if ( + !perm.type.startsWith("3rdPartyStorage^") && + !perm.type.startsWith("3rdPartyFrameStorage^") + ) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_TIME, + "Permission must expire at a specific time" + ); + ok(perm.expireTime > 0, "Permission must have a expiry time"); + } + } + } catch (e) { + alert(e); + } + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs + true, // run the window.open() test + true, // run the user interaction test + [ + // expected blocking notifications + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL, + ], + false +); // run in normal windows diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js new file mode 100644 index 0000000000..84f8d82420 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js @@ -0,0 +1,53 @@ +AntiTracking.runTest( + "Test whether we receive any persistent permissions in private windows", + // Blocking callback + async _ => { + // Nothing to do here! + }, + + // Non blocking callback + async _ => { + try { + let Services = SpecialPowers.Services; + // We would use TEST_3RD_PARTY_DOMAIN here, except that the variable isn't + // accessible in the context of the web page... + let principal = SpecialPowers.wrap(document).nodePrincipal; + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if ( + !perm.type.startsWith("3rdPartyStorage^") && + !perm.type.startsWith("3rdPartyFrameStorage^") + ) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_SESSION, + "Permission must expire at the end of session" + ); + is(perm.expireTime, 0, "Permission must have no expiry time"); + } + } catch (e) { + alert(e); + } + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], // extra prefs + true, // run the window.open() test + true, // run the user interaction test + 0, // don't expect blocking notifications + true +); // run in private windows diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js new file mode 100644 index 0000000000..6ebcfae5a2 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js @@ -0,0 +1,52 @@ +AntiTracking.runTest( + "Test whether we receive any persistent permissions in private windows", + // Blocking callback + async _ => { + // Nothing to do here! + }, + + // Non blocking callback + async _ => { + try { + let Services = SpecialPowers.Services; + // We would use TEST_3RD_PARTY_DOMAIN here, except that the variable isn't + // accessible in the context of the web page... + let principal = SpecialPowers.wrap(document).nodePrincipal; + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if ( + !perm.type.startsWith("3rdPartyStorage^") && + !perm.type.startsWith("3rdPartyFrameStorage^") + ) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_SESSION, + "Permission must expire at the end of session" + ); + is(perm.expireTime, 0, "Permission must have no expiry time"); + } + } catch (e) { + alert(e); + } + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs + true, // run the window.open() test + true, // run the user interaction test + [ + // expected blocking notifications + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL, + ], + true +); // run in private windows diff --git a/toolkit/components/antitracking/test/browser/browser_permissionPropagation.js b/toolkit/components/antitracking/test/browser/browser_permissionPropagation.js new file mode 100644 index 0000000000..73dc9e7145 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionPropagation.js @@ -0,0 +1,426 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +/** + * This test makes sure the when we grant the storage permission, the + * permission is also propagated to iframes within the same agent cluster, + * but not to iframes in the other tabs. + */ + +async function createTab(topUrl, iframeCount, opener, params) { + let newTab; + let browser; + if (opener) { + let promise = BrowserTestUtils.waitForNewTab(gBrowser, topUrl); + await SpecialPowers.spawn(opener, [topUrl], function (url) { + content.window.open(url, "_blank"); + }); + newTab = await promise; + browser = gBrowser.getBrowserForTab(newTab); + } else { + newTab = BrowserTestUtils.addTab(gBrowser, topUrl); + + browser = gBrowser.getBrowserForTab(newTab); + await BrowserTestUtils.browserLoaded(browser); + } + + await SpecialPowers.spawn( + browser, + [params, iframeCount, createTrackerFrame.toString()], + async function (params, count, fn) { + // eslint-disable-next-line no-eval + let fnCreateTrackerFrame = eval(`(() => (${fn}))()`); + await fnCreateTrackerFrame(params, count, ifr => { + ifr.contentWindow.postMessage( + { callback: params.msg.blockingCallback }, + "*" + ); + }); + } + ); + + return Promise.resolve(newTab); +} + +async function createTrackerFrame(params, count, callback) { + let iframes = []; + for (var i = 0; i < count; i++) { + iframes[i] = content.document.createElement("iframe"); + await new content.Promise(resolve => { + iframes[i].id = "ifr" + i; + iframes[i].src = params.page; + iframes[i].onload = resolve; + content.document.body.appendChild(iframes[i]); + }); + + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + callback(iframes[i]); + }); + } +} + +async function testPermission(browser, block, params, frameNumber) { + await SpecialPowers.spawn( + browser, + [block, params, frameNumber], + async function (block, params, frameNumber) { + for (let i = 0; ; i++) { + let ifr = content.document.getElementById("ifr" + i); + if (!ifr || (frameNumber !== undefined && i != frameNumber)) { + break; + } + + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + if (block) { + ifr.contentWindow.postMessage( + { callback: params.msg.blockingCallback }, + "*" + ); + } else { + ifr.contentWindow.postMessage( + { callback: params.msg.nonBlockingCallback }, + "*" + ); + } + }); + } + } + ); +} + +add_task(async function testPermissionGrantedOn3rdParty() { + info("Starting permission propagation test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await new Promise(resolve => { + // eslint-disable-next-line no-undef + let w = worker; + w.addEventListener( + "message", + e => { + ok(!e.data, "IDB is disabled"); + resolve(); + }, + { once: true } + ); + w.postMessage("go"); + }); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + console.log("test hasStorageAccessInitially\n"); + await hasStorageAccessInitially(); + + await new Promise(resolve => { + // eslint-disable-next-line no-undef + let w = worker; + w.addEventListener( + "message", + e => { + ok(e.data, "IDB is enabled"); + resolve(); + }, + { once: true } + ); + w.postMessage("go"); + }); + }).toString(); + + let top = TEST_TOP_PAGE; + let page = TEST_3RD_PARTY_PAGE_WORKER; + let pageOther = + TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyWorker.html"; + let params = { page, msg, pageOther }; + // Create 4 tabs: + // 1. The first tab has two tracker iframes, said A & B. + // 2. The second tab is opened by the first tab, and it has one tracker iframe, said C. + // 3. The third tab has one tracker iframe, said D. + // 4. The fourth tab is opened by the first tab but with a different top-level url). + // The tab has one tracker iframe, said E. + // + // This test grants permission on iframe A, which then should not propagate storage + // permission to iframe B, C, D, E + + info("Creating the first tab"); + let tab1 = await createTab(top, 2, null, params); + let browser1 = gBrowser.getBrowserForTab(tab1); + + info("Creating the second tab"); + let tab2 = await createTab(top, 1, browser1 /* opener */, params); + let browser2 = gBrowser.getBrowserForTab(tab2); + + info("Creating the third tab"); + let tab3 = await createTab(top, 1, null, params); + let browser3 = gBrowser.getBrowserForTab(tab3); + + info("Creating the fourth tab"); + let tab4 = await createTab(TEST_TOP_PAGE_2, 1, browser1, params); + let browser4 = gBrowser.getBrowserForTab(tab4); + + info("Grant storage permission to the first iframe in the first tab"); + await SpecialPowers.spawn(browser1, [page, msg], async function (page, msg) { + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + let ifr = content.document.getElementById("ifr0"); + ifr.contentWindow.postMessage( + { + callback: (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + }).toString(), + }, + "*" + ); + }); + }); + + info("Second iframe of the first tab should not have stroage permission"); + await testPermission(browser1, false /* block */, params, 0); + await testPermission(browser1, true /* block */, params, 1); + + info("The iframe of the second tab should not have storage permission"); + await testPermission(browser2, true /* block */, params); + + info("The iframe of the third tab should not have storage permission"); + await testPermission(browser3, true /* block */, params); + + info("The iframe of the fourth tab should not have storage permission"); + await testPermission(browser4, true /* block */, params); + + info("Removing the tabs"); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function testPermissionGrantedOnFirstParty() { + info("Starting permission propagation test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await new Promise(resolve => { + // eslint-disable-next-line no-undef + let w = worker; + w.addEventListener( + "message", + e => { + ok(!e.data, "IDB is disabled"); + resolve(); + }, + { once: true } + ); + w.postMessage("go"); + }); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + console.log("test hasStorageAccessInitially\n"); + await hasStorageAccessInitially(); + + await new Promise(resolve => { + // eslint-disable-next-line no-undef + let w = worker; + w.addEventListener( + "message", + e => { + ok(e.data, "IDB is enabled"); + resolve(); + }, + { once: true } + ); + w.postMessage("go"); + }); + }).toString(); + + let top = TEST_TOP_PAGE; + let page = TEST_3RD_PARTY_PAGE_WORKER; + let params = { page, msg }; + // Create 4 tabs: + // 1. The first tab has two tracker iframes, said A & B. + // 2. The second tab is opened by the first tab, and it has one tracker iframe, said C. + // 3. The third tab has one tracker iframe, said D. + // 4. The fourth tab is opened by the first tab but with a different top-level url). + // The tab has one tracker iframe, said E. + // + // This test grants permission on iframe A, which then should propagate storage + // permission to iframe B & C, but not D, E + + info("Creating the first tab"); + let tab1 = await createTab(top, 2, null, params); + let browser1 = gBrowser.getBrowserForTab(tab1); + + info("Creating the second tab"); + let tab2 = await createTab(top, 1, browser1 /* opener */, params); + let browser2 = gBrowser.getBrowserForTab(tab2); + + info("Creating the third tab"); + let tab3 = await createTab(top, 1, null, params); + let browser3 = gBrowser.getBrowserForTab(tab3); + + info("Creating the fourth tab"); + let tab4 = await createTab(TEST_TOP_PAGE_2, 1, browser1, params); + let browser4 = gBrowser.getBrowserForTab(tab4); + + info("Grant storage permission to the first iframe in the first tab"); + let promise = BrowserTestUtils.waitForNewTab(gBrowser, page); + await SpecialPowers.spawn(browser1, [page], async function (page) { + content.window.open(page, "_blank"); + }); + let tab = await promise; + BrowserTestUtils.removeTab(tab); + + info("Both iframs of the first tab should have stroage permission"); + await testPermission(browser1, false /* block */, params); + + info("The iframe of the second tab should have storage permission"); + await testPermission(browser2, false /* block */, params); + + info("The iframe of the third tab should not have storage permission"); + await testPermission(browser3, true /* block */, params); + + info("The iframe of the fourth tab should not have storage permission"); + await testPermission(browser4, true /* block */, params); + + info("Removing the tabs"); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_referrerDefaultPolicy.js b/toolkit/components/antitracking/test/browser/browser_referrerDefaultPolicy.js new file mode 100644 index 0000000000..e9030b98e4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_referrerDefaultPolicy.js @@ -0,0 +1,634 @@ +"use strict"; + +requestLongerTimeout(8); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/base/content/test/general/head.js", + this +); + +async function openAWindow(usePrivate) { + info("Creating a new " + (usePrivate ? "private" : "normal") + " window"); + let win = OpenBrowserWindow({ private: usePrivate }); + await TestUtils.topicObserved( + "browser-delayed-startup-finished", + subject => subject == win + ).then(() => win); + await BrowserTestUtils.firstBrowserLoaded(win); + return win; +} + +async function testOnWindowBody(win, expectedReferrer, rp) { + let browser = win.gBrowser; + let tab = browser.selectedTab; + let b = browser.getBrowserForTab(tab); + await promiseTabLoadEvent(tab, TEST_TOP_PAGE); + + info("Loading tracking scripts and tracking images"); + let referrer = await SpecialPowers.spawn( + b, + [{ rp }], + async function ({ rp }) { + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + if (rp) { + src.referrerPolicy = rp; + } + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=script"; + await p; + } + + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + if (rp) { + img.referrerPolicy = rp; + } + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=image"; + await p; + } + + { + let iframe = content.document.createElement("iframe"); + let p = new content.Promise(resolve => { + iframe.onload = resolve; + }); + content.document.body.appendChild(iframe); + if (rp) { + iframe.referrerPolicy = rp; + } + iframe.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=iframe"; + await p; + + p = new content.Promise(resolve => { + content.onmessage = event => { + resolve(event.data); + }; + }); + iframe.contentWindow.postMessage("ping", "*"); + return p; + } + } + ); + + is(referrer, expectedReferrer, "The correct referrer must be read from DOM"); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + is(text, expectedReferrer, "We sent the correct Referer header"); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, expectedReferrer, "We sent the correct Referer header"); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=iframe" + ) + .then(r => r.text()) + .then(text => { + is(text, expectedReferrer, "We sent the correct Referer header"); + }); +} + +async function closeAWindow(win) { + await BrowserTestUtils.closeWindow(win); +} + +let gRecording = true; +let gScenarios = []; +let gRPs = []; +let gTests = { private: [], nonPrivate: [] }; +const kPBPref = "network.http.referer.defaultPolicy.trackers.pbmode"; +const kNonPBPref = "network.http.referer.defaultPolicy.trackers"; + +function recordScenario(isPrivate, expectedReferrer, rp) { + if (!gRPs.includes(rp)) { + gRPs.push(rp); + } + gScenarios.push({ + private: isPrivate, + expectedReferrer, + rp, + pbPref: Services.prefs.getIntPref(kPBPref), + nonPBPref: Services.prefs.getIntPref(kNonPBPref), + }); +} + +async function testOnWindow(isPrivate, expectedReferrer, rp) { + if (gRecording) { + recordScenario(isPrivate, expectedReferrer, rp); + } +} + +function compileScenarios() { + let keys = { false: [], true: [] }; + for (let s of gScenarios) { + let key = { + rp: s.rp, + pbPref: s.pbPref, + nonPBPref: s.nonPBPref, + }; + let skip = false; + for (let k of keys[s.private]) { + if ( + key.rp == k.rp && + key.pbPref == k.pbPref && + key.nonPBPref == k.nonPBPref + ) { + skip = true; + break; + } + } + if (!skip) { + keys[s.private].push(key); + gTests[s.private ? "private" : "nonPrivate"].push({ + rp: s.rp, + pbPref: s.pbPref, + nonPBPref: s.nonPBPref, + expectedReferrer: s.expectedReferrer, + }); + } + } + + // Verify that all scenarios are checked + let counter = 1; + for (let s of gScenarios) { + let checked = false; + for (let tt in gTests) { + let isPrivate = tt == "private"; + for (let t of gTests[tt]) { + if ( + isPrivate == s.private && + t.rp == s.rp && + t.pbPref == s.pbPref && + t.nonPBPref == s.nonPBPref && + t.expectedReferrer == s.expectedReferrer + ) { + checked = true; + break; + } + } + } + ok(checked, `Scenario number ${counter++} checked`); + } +} + +async function executeTests() { + compileScenarios(); + + gRecording = false; + for (let mode in gTests) { + info(`Open a ${mode} window`); + while (gTests[mode].length) { + let test = gTests[mode].shift(); + info(`Running test ${test.toSource()}`); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.http.referer.defaultPolicy.trackers", test.nonPBPref], + ["network.http.referer.defaultPolicy.trackers.pbmode", test.pbPref], + ["dom.security.https_first_pbm", false], + ["network.http.referer.disallowCrossSiteRelaxingDefault", false], + ], + }); + + let win = await openAWindow(mode == "private"); + + await testOnWindowBody(win, test.expectedReferrer, test.rp); + + await closeAWindow(win); + } + } + + Services.prefs.clearUserPref(kPBPref); + Services.prefs.clearUserPref(kNonPBPref); +} + +function pn(name, isPrivate) { + return isPrivate ? name + ".pbmode" : name; +} + +async function testOnNoReferrer(isPrivate) { + // no-referrer pref when no-referrer is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, "", "no-referrer"); + + // strict-origin-when-cross-origin pref when no-referrer is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, "", "no-referrer"); + + // same-origin pref when no-referrer is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + await testOnWindow(isPrivate, "", "no-referrer"); + + // no-referrer pref when no-referrer is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + await testOnWindow(isPrivate, "", "no-referrer"); +} + +async function testOnSameOrigin(isPrivate) { + // same-origin pref when same-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, "", "same-origin"); + + // strict-origin-when-cross-origin pref when same-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, "", "same-origin"); + + // same-origin pref when same-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + await testOnWindow(isPrivate, "", "same-origin"); + + // same-origin pref when same-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + await testOnWindow(isPrivate, "", "same-origin"); +} + +async function testOnNoReferrerWhenDowngrade(isPrivate) { + // The setting referrer policy will be ignored if it is + // no-referrer-when-downgrade in private mode. It will fallback to the default + // value. + // + // The pref 'network.http.referer.disallowCrossSiteRelaxingDefault.pbmode' + // controls this behavior in private mode. + + // no-referrer-when-downgrade pref when no-referrer-when-downgrade is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade"); + + // strict-origin-when-cross-origin pref when no-referrer-when-downgrade is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, TEST_DOMAIN, "no-referrer-when-downgrade"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade"); + } + + // same-origin pref when no-referrer-when-downgrade is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "no-referrer-when-downgrade"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade"); + } + + // no-referrer pref when no-referrer-when-downgrade is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "no-referrer-when-downgrade"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade"); + } +} + +async function testOnOrigin(isPrivate) { + // origin pref when origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "origin"); + + // strict-origin pref when origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "origin"); + + // same-origin pref when origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "origin"); + + // no-referrer pref when origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "origin"); +} + +async function testOnStrictOrigin(isPrivate) { + // strict-origin pref when strict-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin"); + + // strict-origin pref when strict-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin"); + + // same-origin pref when strict-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin"); + + // no-referrer pref when strict-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin"); +} + +async function testOnOriginWhenCrossOrigin(isPrivate) { + // The setting referrer policy will be ignored if it is + // origin-when-cross-origin in private mode. It will fallback to the default + // value. The pref controls this behavior mentioned above. + + // no-referrer-when-downgrade pref when origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "origin-when-cross-origin"); + } else { + await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin"); + } + + // strict-origin-when-cross-origin pref when origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin"); + + // same-origin pref when origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "origin-when-cross-origin"); + } else { + await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin"); + } + + // no-referrer pref when origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "origin-when-cross-origin"); + } else { + await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin"); + } +} + +async function testOnStrictOriginWhenCrossOrigin(isPrivate) { + // origin-when-cross-origin pref when strict-origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin"); + + // strict-origin-when-cross-origin pref when strict-origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin"); + + // same-origin pref when strict-origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin"); + + // no-referrer pref when strict-origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin"); +} + +async function testOnUnsafeUrl(isPrivate) { + // The setting referrer policy will be ignored if it is unsafe in private + // mode. It will fallback to the default value. The pref controls this + // behavior mentioned above. + + // no-referrer-when-downgrade pref when unsafe-url is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url"); + + // strict-origin-when-cross-origin pref when unsafe-url is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, TEST_DOMAIN, "unsafe-url"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url"); + } + + // same-origin pref when unsafe-url is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "unsafe-url"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url"); + } + + // no-referrer pref when unsafe-url is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "unsafe-url"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url"); + } +} + +add_task(async function () { + info("Starting referrer default policy test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["network.http.referer.defaultPolicy", 3], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + + // no-referrer-when-downgrade + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers", 3]], + }); + await testOnWindow(false, TEST_TOP_PAGE, null); + + // strict-origin-when-cross-origin + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers", 2]], + }); + await testOnWindow(false, TEST_DOMAIN, null); + + // same-origin + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers", 1]], + }); + await testOnWindow(false, "", null); + + // no-referrer + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers", 0]], + }); + await testOnWindow(false, "", null); + + // override with no-referrer + await testOnNoReferrer(false); + + // override with same-origin + await testOnSameOrigin(false); + + // override with no-referrer-when-downgrade + await testOnNoReferrerWhenDowngrade(false); + + // override with origin + await testOnOrigin(false); + + // override with strict-origin + await testOnStrictOrigin(false); + + // override with origin-when-cross-origin + await testOnOriginWhenCrossOrigin(false); + + // override with strict-origin-when-cross-origin + await testOnStrictOriginWhenCrossOrigin(false); + + // override with unsafe-url + await testOnUnsafeUrl(false); + + // Reset the pref. + Services.prefs.clearUserPref("network.http.referer.defaultPolicy.trackers"); + + // no-referrer-when-downgrade + await SpecialPowers.pushPrefEnv({ + set: [ + // Set both prefs, because if we only set the trackers pref, then the PB + // mode default policy pref (2) would apply! + ["network.http.referer.defaultPolicy.pbmode", 3], + ["network.http.referer.defaultPolicy.trackers.pbmode", 3], + ], + }); + await testOnWindow(true, TEST_TOP_PAGE, null); + + // strict-origin-when-cross-origin + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers.pbmode", 2]], + }); + await testOnWindow(true, TEST_DOMAIN, null); + + // same-origin + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers.pbmode", 1]], + }); + await testOnWindow(true, "", null); + + // no-referrer + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers.pbmode", 0]], + }); + await testOnWindow(true, "", null); + + // override with no-referrer + await testOnNoReferrer(true); + + // override with same-origin + await testOnSameOrigin(true); + + // override with no-referrer-when-downgrade + await testOnNoReferrerWhenDowngrade(true); + + // override with origin + await testOnOrigin(true); + + // override with strict-origin + await testOnStrictOrigin(true); + + // override with origin-when-cross-origin + await testOnOriginWhenCrossOrigin(true); + + // override with strict-origin-when-cross-origin + await testOnStrictOriginWhenCrossOrigin(true); + + // override with unsafe-url + await testOnUnsafeUrl(true); + + // Reset the pref. + Services.prefs.clearUserPref( + "network.http.referer.defaultPolicy.trackers.pbmode" + ); +}); + +add_task(async function () { + await UrlClassifierTestUtils.addTestTrackers(); + + await executeTests(); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_script.js b/toolkit/components/antitracking/test/browser/browser_script.js new file mode 100644 index 0000000000..ef6c7f67c6 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_script.js @@ -0,0 +1,224 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + scriptURL: TEST_DOMAIN + TEST_PATH + "tracker.js", + page: TEST_3RD_PARTY_PAGE, + }, + ], + async obj => { + info("Checking if permission is denied"); + let callbackBlocked = async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }; + + let assertBlocked = () => + new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(callbackBlocked.toString(), "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + + await assertBlocked(); + + info("Triggering a 3rd party script..."); + let p = new content.Promise(resolve => { + let bc = new content.BroadcastChannel("a"); + bc.onmessage = resolve; + }); + + let src = content.document.createElement("script"); + content.document.body.appendChild(src); + src.src = obj.scriptURL; + + await p; + + info("Checking if permission is denied before interacting with tracker"); + await assertBlocked(); + } + ); + + await AntiTracking.interactWithTracker(); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + scriptURL: TEST_DOMAIN + TEST_PATH + "tracker.js", + page: TEST_3RD_PARTY_PAGE, + }, + ], + async obj => { + info("Checking if permission is denied"); + let callbackBlocked = async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }; + + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(callbackBlocked.toString(), "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + + info("Triggering a 3rd party script..."); + let p = new content.Promise(resolve => { + let bc = new content.BroadcastChannel("a"); + bc.onmessage = resolve; + }); + + let src = content.document.createElement("script"); + content.document.body.appendChild(src); + src.src = obj.scriptURL; + + await p; + + info("Checking if permission is granted"); + let callbackAllowed = async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }; + + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(callbackAllowed.toString(), "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_serviceWorkersWithStorageAccessGranted.js b/toolkit/components/antitracking/test/browser/browser_serviceWorkersWithStorageAccessGranted.js new file mode 100644 index 0000000000..4ae4ba74dd --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_serviceWorkersWithStorageAccessGranted.js @@ -0,0 +1,148 @@ +/** This tests that the service worker can be used if we have storage access + * permission. We manually write the storage access permission into the + * permission manager to simulate the storage access has been granted. We would + * test the service worker three times. The fist time is to check the service + * work is allowed. The second time is to load again and check it won't hit + * assertion, this assertion would only be hit if we have registered a service + * worker, see Bug 1631234. + * + * The third time is to load again but in a sandbox iframe to check it won't + * hit the assertion. See Bug 1637226 for details. + * + * The fourth time is to load again in a nested iframe to check it won't hit + * the assertion. See Bug 1641153 for details. + * */ + +add_task(async _ => { + // Manually add the storage permission. + PermissionTestUtils.add( + TEST_DOMAIN, + "3rdPartyStorage^https://tracking.example.org", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); + + AntiTracking._createTask({ + name: "Test that we can use service worker if we have the storage access permission", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: async _ => { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(true, "ServiceWorker can be used!"); + }, + _ => { + ok(false, "ServiceWorker can be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + extraPrefs: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + }); + + AntiTracking._createTask({ + name: "Test again to check if we can still use service worker without hit the assertion.", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: async _ => { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(true, "ServiceWorker can be used!"); + }, + _ => { + ok(false, "ServiceWorker can be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + extraPrefs: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + }); + + AntiTracking._createTask({ + name: "Test again to check if we cannot use service worker in a sandbox iframe without hit the assertion.", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: async _ => { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(false, "ServiceWorker cannot be used in sandbox iframe!"); + }, + _ => { + ok(true, "ServiceWorker cannot be used in sandbox iframe!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + extraPrefs: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications, + runInPrivateWindow: false, + iframeSandbox: "allow-scripts allow-same-origin", + accessRemoval: null, + callbackAfterRemoval: null, + }); + + const NESTED_THIRD_PARTY_PAGE = + TEST_DOMAIN + TEST_PATH + "3rdPartyRelay.html?" + TEST_3RD_PARTY_PAGE; + + AntiTracking._createTask({ + name: "Test again to check if we can use service worker in a nested iframe without hit the assertion.", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: async _ => { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(true, "ServiceWorker can be used in nested iframe!"); + }, + _ => { + ok(false, "ServiceWorker can be used in nested iframe!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + extraPrefs: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: NESTED_THIRD_PARTY_PAGE, + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js b/toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js new file mode 100644 index 0000000000..b35fbea8c7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js @@ -0,0 +1,153 @@ +AntiTracking.runTest( + "localStorage with a tracker that is entitylisted via a pref", + async _ => { + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + localStorage.foo = 42; + hasThrown = false; + } catch (e) { + hasThrown = true; + } + + is(hasThrown, shouldThrow, "LocalStorage is allowed"); + }, + async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["urlclassifier.trackingAnnotationSkipURLs", "TRACKING.EXAMPLE.ORG"]], + false, // run the window.open() test + false, // run the user interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false +); // run in a normal window + +AntiTracking.runTest( + "localStorage with a tracker that is entitylisted via a fancy pref", + async _ => { + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + localStorage.foo = 42; + hasThrown = false; + } catch (e) { + hasThrown = true; + } + + is(hasThrown, shouldThrow, "LocalStorage is allowed"); + }, + async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "urlclassifier.trackingAnnotationSkipURLs", + "foobar.example,*.example.org,baz.example", + ], + ], + false, // run the window.open() test + false, // run the user interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false +); // run in a normal window + +AntiTracking.runTest( + "localStorage with a tracker that is entitylisted via a misconfigured pref", + async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["urlclassifier.trackingAnnotationSkipURLs", "*.tracking.example.org"]], + false, // run the window.open() test + false, // run the user interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false +); // run in a normal window + +AntiTracking.runTest( + "localStorage with a tracker that is entitylisted via a pref, but skip lists are disabled.", + async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["urlclassifier.trackingAnnotationSkipURLs", "TRACKING.EXAMPLE.ORG"], + ["privacy.antitracking.enableWebcompat", false], + ], + false, // run the window.open() test + false, // run the user interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false +); // run in a normal window diff --git a/toolkit/components/antitracking/test/browser/browser_socialtracking.js b/toolkit/components/antitracking/test/browser/browser_socialtracking.js new file mode 100644 index 0000000000..1cbf5d9278 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_socialtracking.js @@ -0,0 +1,147 @@ +function runTest(obj) { + add_task(async _ => { + info("Test: " + obj.testName); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.trackingprotection.socialtracking.enabled", + obj.protectionEnabled, + ], + ["privacy.socialtracking.block_cookies.enabled", obj.cookieBlocking], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_DOMAIN_STP + TEST_PATH + "localStorage.html", + image: TEST_3RD_PARTY_DOMAIN_STP + TEST_PATH + "raptor.jpg", + loading: obj.loading, + result: obj.result, + }, + ], + async obj => { + let loading = await new content.Promise(resolve => { + let image = new content.Image(); + image.src = obj.image + "?" + Math.random(); + image.onload = _ => resolve(true); + image.onerror = _ => resolve(false); + }); + + is(loading, obj.loading, "Loading expected"); + + if (obj.loading) { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + let p = new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + }); + + info("Setting localStorage value..."); + ifr.contentWindow.postMessage("test", "*"); + + info("Getting the value..."); + let value = await p; + is(value.status, obj.result, "We expect to succeed"); + } + } + ); + + info("Checking content blocking log."); + let contentBlockingLog = JSON.parse(await browser.getContentBlockingLog()); + let origins = Object.keys(contentBlockingLog); + is(origins.length, 1, "There should be one origin entry in the log."); + for (let origin of origins) { + is( + origin + "/", + TEST_3RD_PARTY_DOMAIN_STP, + "Correct tracker origin must be reported" + ); + Assert.deepEqual( + contentBlockingLog[origin], + obj.expectedLogItems, + "Content blocking log should be as expected" + ); + } + + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); +} + +runTest({ + testName: + "Socialtracking-annotation feature enabled but not considered for tracking detection.", + protectionEnabled: false, + loading: true, + cookieBlocking: false, + result: true, + expectedLogItems: [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1], + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1], + ], +}); + +runTest({ + testName: + "Socialtracking-annotation feature enabled and considered for tracking detection.", + protectionEnabled: false, + loading: true, + cookieBlocking: true, + result: false, + expectedLogItems: [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1], + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1], + [Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT, true, 2], + // We cache the storage allowed decision, so we will only get one block + // event per window and origin. + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER, true, 1], + ], +}); + +runTest({ + testName: "Socialtracking-protection feature enabled.", + protectionEnabled: true, + loading: false, + cookieBlocking: true, + result: false, + expectedLogItems: [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT, true, 1], + ], +}); diff --git a/toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js b/toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js new file mode 100644 index 0000000000..e7389b1456 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js @@ -0,0 +1,114 @@ +/** + * Bug 1663992 - Testing the 'Save Image As' works in an image document if the + * image is blocked by social tracker. + */ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this +); + +const TEST_IMAGE_URL = + "http://social-tracking.example.org/browser/toolkit/components/antitracking/test/browser/raptor.jpg"; + +let MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +const tempDir = createTemporarySaveDirectory(); +MockFilePicker.displayDirectory = tempDir; + +function createTemporarySaveDirectory() { + let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + saveDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + return saveDir; +} + +function createPromiseForTransferComplete() { + return new Promise(resolve => { + MockFilePicker.showCallback = fp => { + info("MockFilePicker showCallback"); + + let fileName = fp.defaultString; + let destFile = tempDir.clone(); + destFile.append(fileName); + + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + + MockFilePicker.showCallback = null; + mockTransferCallback = function (downloadSuccess) { + ok(downloadSuccess, "Image should have been downloaded successfully"); + mockTransferCallback = () => {}; + resolve(); + }; + }; + }); +} + +add_setup(async function () { + info("Setting up the prefs."); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.socialtracking.enabled", true], + [ + "urlclassifier.features.socialtracking.blacklistHosts", + "social-tracking.example.org", + ], + ], + }); + + info("Setting MockFilePicker."); + mockTransferRegisterer.register(); + + registerCleanupFunction(function () { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + tempDir.remove(true); + }); +}); + +add_task(async function () { + info("Open a new tab for testing"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_IMAGE_URL + ); + + let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown"); + + let browser = gBrowser.selectedBrowser; + + info("Open the context menu."); + await BrowserTestUtils.synthesizeMouseAtCenter( + "img", + { + type: "contextmenu", + button: 2, + }, + browser + ); + + await popupShownPromise; + + let transferCompletePromise = createPromiseForTransferComplete(); + let saveElement = document.getElementById(`context-saveimage`); + info("Triggering the save process."); + saveElement.doCommand(); + + info("Wait until the save is finished."); + await transferCompletePromise; + + info("Close the context menu."); + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js new file mode 100644 index 0000000000..b95c34fe26 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TEST_PAGE = + "http://example.org/browser/toolkit/components/antitracking/test/browser/empty.html"; +const TEST_ANOTHER_PAGE = + "http://example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; +const TEST_PREFLIGHT_IFRAME_PAGE = + "http://mochi.test:8888/browser/toolkit/components/antitracking/test/browser/empty.html"; +const TEST_PREFLIGHT_PAGE = + "http://example.net/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs"; + +add_task(async function () { + let uuidGenerator = Services.uuid; + + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + // First, create one tab under one first party. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE + ); + let token = uuidGenerator.generateUUID().toString(); + + // Use fetch to verify that preflight cache is working. The preflight + // cache is keyed by the loading principal and the url. So, we load an + // iframe with one origin and use fetch in there to ensure we will have + // the same loading principal. + await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_PREFLIGHT_PAGE, TEST_PREFLIGHT_IFRAME_PAGE, token], + async (url, iframe_url, token) => { + let iframe = content.document.createElement("iframe"); + + await new Promise(resolve => { + iframe.onload = () => { + resolve(); + }; + content.document.body.appendChild(iframe); + iframe.src = iframe_url; + }); + + await SpecialPowers.spawn( + iframe, + [url, token], + async (url, token) => { + const test_url = `${url}?token=${token}`; + let response = await content.fetch( + new content.Request(test_url, { + mode: "cors", + method: "GET", + headers: [["x-test-header", "check"]], + }) + ); + + is( + await response.text(), + "1", + "The preflight should be sent at first time" + ); + + response = await content.fetch( + new content.Request(test_url, { + mode: "cors", + method: "GET", + headers: [["x-test-header", "check"]], + }) + ); + + is( + await response.text(), + "0", + "The preflight shouldn't be sent due to the preflight cache" + ); + } + ); + } + ); + + // Load the tab with a different first party. And use fetch to check if + // the preflight cache is partitioned. The fetch will also be performed in + // the iframe with the same origin as above to ensure we use the same + // loading principal. + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + TEST_ANOTHER_PAGE + ); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [ + TEST_PREFLIGHT_PAGE, + TEST_PREFLIGHT_IFRAME_PAGE, + token, + networkIsolation, + ], + async (url, iframe_url, token, partitioned) => { + let iframe = content.document.createElement("iframe"); + + await new Promise(resolve => { + iframe.onload = () => { + resolve(); + }; + content.document.body.appendChild(iframe); + iframe.src = iframe_url; + }); + + await SpecialPowers.spawn( + iframe, + [url, token, partitioned], + async (url, token, partitioned) => { + const test_url = `${url}?token=${token}`; + + let response = await content.fetch( + new content.Request(test_url, { + mode: "cors", + method: "GET", + headers: [["x-test-header", "check"]], + }) + ); + + if (partitioned) { + is( + await response.text(), + "1", + "The preflight cache should be partitioned" + ); + } else { + is( + await response.text(), + "0", + "The preflight cache shouldn't be partitioned" + ); + } + } + ); + } + ); + + BrowserTestUtils.removeTab(tab); + } + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs new file mode 100644 index 0000000000..22ce6aad20 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs @@ -0,0 +1,38 @@ +/* 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/. */ + +"use strict"; + +function handleRequest(request, response) { + let query = new URLSearchParams(request.queryString); + let token = query.get("token"); + + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Headers", "x-test-header", false); + + if (request.method == "OPTIONS") { + response.setHeader( + "Access-Control-Allow-Methods", + request.getHeader("Access-Control-Request-Method"), + false + ); + response.setHeader("Access-Control-Max-Age", "20", false); + + setState(token, token); + } else { + let test_op = request.getHeader("x-test-header"); + + if (test_op == "check") { + let value = getState(token); + + if (value) { + response.write("1"); + setState(token, ""); + } else { + response.write("0"); + } + } + } +} diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.js new file mode 100644 index 0000000000..6cf614dfaa --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.js @@ -0,0 +1,251 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var unsecureEmptyURL = + "http://example.org/browser/toolkit/components/antitracking/test/browser/empty.html"; +var secureEmptyURL = + "https://example.org/browser/toolkit/components/antitracking/test/browser/empty.html"; +var secureAnotherEmptyURL = + "https://example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; +var secureURL = + "https://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs"; +var unsecureURL = + "http://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs"; +var secureImgURL = + "https://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs?image"; +var unsecureImgURL = + "http://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs?image"; +var secureIncludeSubURL = + "https://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs?includeSub"; +var unsecureSubEmptyURL = + "http://test1.example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; +var secureSubEmptyURL = + "https://test1.example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; +var unsecureNoCertSubEmptyURL = + "http://nocert.example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; + +function cleanupHSTS(aPartitionEnabled, aUseSite) { + // Ensure to remove example.com from the HSTS list. + let sss = Cc["@mozilla.org/ssservice;1"].getService( + Ci.nsISiteSecurityService + ); + + for (let origin of ["example.com", "example.org"]) { + let originAttributes = {}; + + if (aPartitionEnabled) { + if (aUseSite) { + originAttributes = { partitionKey: `(http,${origin})` }; + } else { + originAttributes = { partitionKey: origin }; + } + } + + sss.resetState(NetUtil.newURI("http://example.com/"), originAttributes); + } +} + +function promiseTabLoadEvent(aTab, aURL, aFinalURL) { + info("Wait for load tab event"); + BrowserTestUtils.startLoadingURIString(aTab.linkedBrowser, aURL); + return BrowserTestUtils.browserLoaded(aTab.linkedBrowser, false, aFinalURL); +} + +function waitFor(host, type) { + return new Promise(resolve => { + const observer = channel => { + if ( + channel instanceof Ci.nsIHttpChannel && + channel.URI.host === host && + channel.loadInfo.internalContentPolicyType === type + ) { + Services.obs.removeObserver(observer, "http-on-stop-request"); + resolve(channel.URI.spec); + } + }; + Services.obs.addObserver(observer, "http-on-stop-request"); + }); +} + +add_task(async function () { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ["security.mixed_content.upgrade_display_content", false], + ], + }); + + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser)); + + // Let's load the secureURL as first-party in order to activate HSTS. + await promiseTabLoadEvent(tab, secureURL, secureURL); + + // Let's test HSTS: unsecure -> secure. + await promiseTabLoadEvent(tab, unsecureURL, secureURL); + ok(true, "unsecure -> secure, first-party works!"); + + // Let's load a first-party. + await promiseTabLoadEvent(tab, unsecureEmptyURL, unsecureEmptyURL); + + let finalURL = waitFor( + "example.com", + Ci.nsIContentPolicy.TYPE_INTERNAL_IFRAME + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [unsecureURL], async url => { + let ifr = content.document.createElement("iframe"); + content.document.body.appendChild(ifr); + ifr.src = url; + }); + + if (networkIsolation) { + is(await finalURL, unsecureURL, "HSTS doesn't work for 3rd parties"); + } else { + is(await finalURL, secureURL, "HSTS works for 3rd parties"); + } + + gBrowser.removeCurrentTab(); + cleanupHSTS(networkIsolation, partitionPerSite); + } + } +}); + +add_task(async function test_subresource() { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ["security.mixed_content.upgrade_display_content", false], + ], + }); + + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser)); + + // Load a secure page as first party. + await promiseTabLoadEvent(tab, secureEmptyURL, secureEmptyURL); + + let loadPromise = waitFor( + "example.com", + Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE + ); + + // Load a secure subresource. HSTS won't be activated, since third + // parties can't set HSTS. + await SpecialPowers.spawn( + tab.linkedBrowser, + [secureImgURL], + async url => { + let ifr = content.document.createElement("img"); + content.document.body.appendChild(ifr); + ifr.src = url; + } + ); + + // Ensure the subresource is loaded. + await loadPromise; + + // Reload the secure page as first party. + await promiseTabLoadEvent(tab, secureEmptyURL, secureEmptyURL); + + let finalURL = waitFor( + "example.com", + Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE + ); + + // Load an unsecure subresource. It should not be upgraded to https. + await SpecialPowers.spawn( + tab.linkedBrowser, + [unsecureImgURL], + async url => { + let ifr = content.document.createElement("img"); + content.document.body.appendChild(ifr); + ifr.src = url; + } + ); + + is(await finalURL, unsecureImgURL, "HSTS isn't set for 3rd parties"); + + // Load the secure page with a different origin as first party. + await promiseTabLoadEvent( + tab, + secureAnotherEmptyURL, + secureAnotherEmptyURL + ); + + finalURL = waitFor( + "example.com", + Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE + ); + + // Load a unsecure subresource + await SpecialPowers.spawn( + tab.linkedBrowser, + [unsecureImgURL], + async url => { + let ifr = content.document.createElement("img"); + content.document.body.appendChild(ifr); + ifr.src = url; + } + ); + + is(await finalURL, unsecureImgURL, "HSTS isn't set for 3rd parties"); + + gBrowser.removeCurrentTab(); + cleanupHSTS(networkIsolation, partitionPerSite); + } + } +}); + +add_task(async function test_includeSubDomains() { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ["security.mixed_content.upgrade_display_content", false], + ], + }); + + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser)); + + // Load a secure page as first party to activate HSTS. + await promiseTabLoadEvent(tab, secureIncludeSubURL, secureIncludeSubURL); + + // Load a unsecure sub-domain page as first party to see if it's upgraded. + await promiseTabLoadEvent(tab, unsecureSubEmptyURL, secureSubEmptyURL); + + // Load a sub domain page which will trigger the cert error page. + let certErrorLoaded = BrowserTestUtils.waitForErrorPage( + tab.linkedBrowser + ); + BrowserTestUtils.startLoadingURIString( + tab.linkedBrowser, + unsecureNoCertSubEmptyURL + ); + await certErrorLoaded; + + // Verify the error page has the 'badStsCert' in its query string + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + let searchParams = new content.URLSearchParams( + content.document.documentURI + ); + + is( + searchParams.get("s"), + "badStsCert", + "The cert error page has 'badStsCert' set" + ); + }); + + gBrowser.removeCurrentTab(); + cleanupHSTS(networkIsolation, partitionPerSite); + } + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs new file mode 100644 index 0000000000..185c7c7e05 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs @@ -0,0 +1,33 @@ +/* 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/. */ + +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA" + + "DUlEQVQImWNgY2P7DwABOgESJhRQtgAAAABJRU5ErkJggg==" +); + +const PAGE = "<!DOCTYPE html><html><body><p>HSTS page</p></body></html>"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, "200", "OK"); + if (request.queryString == "includeSub") { + response.setHeader( + "Strict-Transport-Security", + "max-age=60; includeSubDomains" + ); + } else { + response.setHeader("Strict-Transport-Security", "max-age=60"); + } + + if (request.queryString == "image") { + response.setHeader("Content-Type", "image/png", false); + response.setHeader("Content-Length", IMG_BYTES.length + "", false); + response.write(IMG_BYTES); + return; + } + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Length", PAGE.length + "", false); + response.write(PAGE); +} diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js new file mode 100644 index 0000000000..4d3a72d7a7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js @@ -0,0 +1,194 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const cacheURL = + "http://example.org/browser/browser/components/originattributes/test/browser/file_cache.html"; + +function countMatchingCacheEntries(cacheEntries, domain, fileSuffix) { + return cacheEntries + .map(entry => entry.uri.asciiSpec) + .filter(spec => spec.includes(domain)) + .filter(spec => spec.includes("file_thirdPartyChild." + fileSuffix)).length; +} + +async function checkCache(suffixes, originAttributes) { + const loadContextInfo = Services.loadContextInfo.custom( + false, + originAttributes + ); + + const data = await new Promise(resolve => { + let cacheEntries = []; + let cacheVisitor = { + onCacheStorageInfo(num, consumption) {}, + onCacheEntryInfo(uri, idEnhance) { + cacheEntries.push({ uri, idEnhance }); + }, + onCacheEntryVisitCompleted() { + resolve(cacheEntries); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + // Visiting the disk cache also visits memory storage so we do not + // need to use Services.cache2.memoryCacheStorage() here. + let storage = Services.cache2.diskCacheStorage(loadContextInfo); + storage.asyncVisitStorage(cacheVisitor, true); + }); + + for (let suffix of suffixes) { + let foundEntryCount = countMatchingCacheEntries( + data, + "example.net", + suffix + ); + ok( + foundEntryCount > 0, + `Cache entries expected for ${suffix} and OA=${JSON.stringify( + originAttributes + )}` + ); + } +} + +add_task(async function () { + info("Disable predictor and accept all"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.predictor.enabled", false], + ["network.predictor.enable-prefetch", false], + ["network.cookie.cookieBehavior", 0], + ], + }); + + const tests = [ + { + prefValue: true, + originAttributes: { partitionKey: "(http,example.org)" }, + }, + { + prefValue: false, + originAttributes: {}, + }, + ]; + + for (let test of tests) { + info("Clear image and network caches"); + let tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService( + SpecialPowers.Ci.imgITools + ); + let imageCache = tools.getImgCacheForDocument(window.document); + imageCache.clearCache(true); // true=chrome + imageCache.clearCache(false); // false=content + Services.cache2.clear(); + + info("Enabling network state partitioning"); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.network_state", test.prefValue]], + }); + + info("Let's load a page to populate some entries"); + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser)); + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, cacheURL); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, cacheURL); + + let argObj = { + randomSuffix: Math.random(), + urlPrefix: + "http://example.net/browser/browser/components/originattributes/test/browser/", + }; + + await SpecialPowers.spawn( + tab.linkedBrowser, + [argObj], + async function (arg) { + // The CSS cache needs to be cleared in-process. + content.windowUtils.clearSharedStyleSheetCache(); + + let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.ogv"; + let audioURL = arg.urlPrefix + "file_thirdPartyChild.audio.ogg"; + let URLSuffix = "?r=" + arg.randomSuffix; + + // Create the audio and video elements. + let audio = content.document.createElement("audio"); + let video = content.document.createElement("video"); + let audioSource = content.document.createElement("source"); + + // Append the audio element into the body, and wait until they're finished. + await new content.Promise(resolve => { + let audioLoaded = false; + + let audioListener = () => { + Assert.ok(true, `Audio suspended: ${audioURL + URLSuffix}`); + audio.removeEventListener("suspend", audioListener); + + audioLoaded = true; + if (audioLoaded) { + resolve(); + } + }; + + Assert.ok(true, `Loading audio: ${audioURL + URLSuffix}`); + + // Add the event listeners before everything in case we lose events. + audio.addEventListener("suspend", audioListener); + + // Assign attributes for the audio element. + audioSource.setAttribute("src", audioURL + URLSuffix); + audioSource.setAttribute("type", "audio/ogg"); + + audio.appendChild(audioSource); + audio.autoplay = true; + + content.document.body.appendChild(audio); + }); + + // Append the video element into the body, and wait until it's finished. + await new content.Promise(resolve => { + let listener = () => { + Assert.ok(true, `Video suspended: ${videoURL + URLSuffix}`); + video.removeEventListener("suspend", listener); + resolve(); + }; + + Assert.ok(true, `Loading video: ${videoURL + URLSuffix}`); + + // Add the event listener before everything in case we lose the event. + video.addEventListener("suspend", listener); + + // Assign attributes for the video element. + video.setAttribute("src", videoURL + URLSuffix); + video.setAttribute("type", "video/ogg"); + + content.document.body.appendChild(video); + }); + } + ); + + let maybePartitionedSuffixes = [ + "iframe.html", + "link.css", + "script.js", + "img.png", + "favicon.png", + "object.png", + "embed.png", + "xhr.html", + "worker.xhr.html", + "audio.ogg", + "video.ogv", + "fetch.html", + "worker.fetch.html", + "request.html", + "worker.request.html", + "import.js", + "worker.js", + "sharedworker.js", + ]; + + info("Query the cache (maybe) partitioned cache"); + await checkCache(maybePartitionedSuffixes, test.originAttributes); + + gBrowser.removeCurrentTab(); + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_network.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_network.js new file mode 100644 index 0000000000..a28f7d5adc --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_network.js @@ -0,0 +1,116 @@ +function altSvcCacheKeyIsolated(parsed) { + return parsed.length > 5 && parsed[5] == "I"; +} + +function altSvcPartitionKey(key) { + let parts = key.split(":"); + return parts[parts.length - 2]; +} + +const gHttpHandler = Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler +); + +add_task(async function () { + info("Starting tlsSessionTickets test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.cache.disk.enable", false], + ["browser.cache.memory.enable", false], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_ACCEPT, + ], + ["network.http.altsvc.proxy_checks", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", false], + ["privacy.partition.network_state", true], + ["privacy.partition.network_state.connection_with_proxy", true], + ], + }); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + const thirdPartyURL = + "https://tlsresumptiontest.example.org/browser/toolkit/components/antitracking/test/browser/empty-altsvc.js"; + const partitionKey1 = "^partitionKey=%28http%2Cexample.net%29"; + const partitionKey2 = "^partitionKey=%28http%2Cmochi.test%29"; + + function checkAltSvcCache(keys) { + let arr = gHttpHandler.altSvcCacheKeys; + is( + arr.length, + keys.length, + "Found the expected number of items in the cache" + ); + for (let i = 0; i < arr.length; ++i) { + is( + altSvcPartitionKey(arr[i]), + keys[i], + "Expected top window origin found in the Alt-Svc cache key" + ); + } + } + + checkAltSvcCache([]); + + info("Loading something in the tab"); + await SpecialPowers.spawn(browser, [{ thirdPartyURL }], async function (obj) { + dump("AAA: " + content.window.location.href + "\n"); + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = obj.thirdPartyURL; + await p; + }); + + checkAltSvcCache([partitionKey1]); + + info("Creating a second tab"); + let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE_6); + gBrowser.selectedTab = tab2; + + let browser2 = gBrowser.getBrowserForTab(tab2); + await BrowserTestUtils.browserLoaded(browser2); + + info("Loading something in the second tab"); + await SpecialPowers.spawn( + browser2, + [{ thirdPartyURL }], + async function (obj) { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = obj.thirdPartyURL; + await p; + } + ); + + checkAltSvcCache([partitionKey1, partitionKey2]); + + info("Removing the tabs"); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js new file mode 100644 index 0000000000..22db484e16 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js @@ -0,0 +1,532 @@ +/** + * Bug 1641270 - A test case for ensuring the save channel will use the correct + * cookieJarSettings when doing the saving and the cache would + * work as expected. + */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this +); + +const TEST_IMAGE_URL = TEST_DOMAIN + TEST_PATH + "file_saveAsImage.sjs"; +const TEST_VIDEO_URL = TEST_DOMAIN + TEST_PATH + "file_saveAsVideo.sjs"; +const TEST_PAGEINFO_URL = TEST_DOMAIN + TEST_PATH + "file_saveAsPageInfo.html"; + +let MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +const tempDir = createTemporarySaveDirectory(); +MockFilePicker.displayDirectory = tempDir; + +function createTemporarySaveDirectory() { + let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + saveDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + return saveDir; +} + +function createPromiseForTransferComplete(aDesirableFileName) { + return new Promise(resolve => { + MockFilePicker.showCallback = fp => { + info("MockFilePicker showCallback"); + + let fileName = fp.defaultString; + let destFile = tempDir.clone(); + destFile.append(fileName); + + if (aDesirableFileName) { + is(fileName, aDesirableFileName, "The default file name is correct."); + } + + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + + MockFilePicker.showCallback = null; + mockTransferCallback = function (downloadSuccess) { + ok(downloadSuccess, "File should have been downloaded successfully"); + mockTransferCallback = () => {}; + resolve(); + }; + }; + }); +} + +function createPromiseForObservingChannel(aURL, aPartitionKey) { + return new Promise(resolve => { + let observer = (aSubject, aTopic) => { + if (aTopic === "http-on-modify-request") { + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + let reqLoadInfo = httpChannel.loadInfo; + + // Make sure this is the request which we want to check. + if (!httpChannel.URI.spec.endsWith(aURL)) { + return; + } + + info(`Checking loadInfo for URI: ${httpChannel.URI.spec}\n`); + is( + reqLoadInfo.cookieJarSettings.partitionKey, + aPartitionKey, + "The loadInfo has the correct partition key" + ); + + Services.obs.removeObserver(observer, "http-on-modify-request"); + resolve(); + } + }; + + Services.obs.addObserver(observer, "http-on-modify-request"); + }); +} + +add_setup(async function () { + info("Setting MockFilePicker."); + mockTransferRegisterer.register(); + + registerCleanupFunction(function () { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + tempDir.remove(true); + }); +}); + +add_task(async function testContextMenuSaveImage() { + let uuidGenerator = Services.uuid; + + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + // We use token to separate the caches. + let token = uuidGenerator.generateUUID().toString(); + const testImageURL = `${TEST_IMAGE_URL}?token=${token}`; + + info(`Open a new tab for testing "Save image as" in context menu.`); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info(`Insert the testing image into the tab.`); + await SpecialPowers.spawn( + tab.linkedBrowser, + [testImageURL], + async url => { + let img = content.document.createElement("img"); + let loaded = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.setAttribute("id", "image1"); + img.src = url; + await loaded; + } + ); + + info("Open the context menu."); + let popupShownPromise = BrowserTestUtils.waitForEvent( + document, + "popupshown" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#image1", + { + type: "contextmenu", + button: 2, + }, + tab.linkedBrowser + ); + + await popupShownPromise; + + let partitionKey = partitionPerSite + ? "(http,example.net)" + : "example.net"; + + let transferCompletePromise = createPromiseForTransferComplete(); + let observerPromise = createPromiseForObservingChannel( + testImageURL, + partitionKey + ); + + let saveElement = document.getElementById("context-saveimage"); + info("Triggering the save process."); + saveElement.doCommand(); + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + + info("Close the context menu."); + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; + + // Check if there will be only one network request. The another one should + // be from cache. + let res = await fetch(`${TEST_IMAGE_URL}?token=${token}&result`); + let res_text = await res.text(); + is(res_text, "1", "The image should be loaded only once."); + + BrowserTestUtils.removeTab(tab); + } + } +}); + +add_task(async function testContextMenuSaveVideo() { + let uuidGenerator = Services.uuid; + + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + // We use token to separate the caches. + let token = uuidGenerator.generateUUID().toString(); + const testVideoURL = `${TEST_VIDEO_URL}?token=${token}`; + + info(`Open a new tab for testing "Save Video as" in context menu.`); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info(`Insert the testing video into the tab.`); + await SpecialPowers.spawn( + tab.linkedBrowser, + [testVideoURL], + async url => { + let video = content.document.createElement("video"); + let loaded = new content.Promise(resolve => { + video.onloadeddata = resolve; + }); + content.document.body.appendChild(video); + video.setAttribute("id", "video1"); + video.src = url; + await loaded; + } + ); + + info("Open the context menu."); + let popupShownPromise = BrowserTestUtils.waitForEvent( + document, + "popupshown" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#video1", + { + type: "contextmenu", + button: 2, + }, + tab.linkedBrowser + ); + + await popupShownPromise; + + let partitionKey = partitionPerSite + ? "(http,example.net)" + : "example.net"; + + // We also check the default file name, see Bug 1679325. + let transferCompletePromise = createPromiseForTransferComplete( + "file_saveAsVideo.webm" + ); + let observerPromise = createPromiseForObservingChannel( + testVideoURL, + partitionKey + ); + + let saveElement = document.getElementById("context-savevideo"); + info("Triggering the save process."); + saveElement.doCommand(); + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + + info("Close the context menu."); + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; + + // Check if there will be only one network request. The another one should + // be from cache. + let res = await fetch(`${TEST_VIDEO_URL}?token=${token}&result`); + let res_text = await res.text(); + is(res_text, "1", "The video should be loaded only once."); + + BrowserTestUtils.removeTab(tab); + } + } +}); + +add_task(async function testSavePageInOfflineMode() { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + let partitionKey = partitionPerSite + ? "(http,example.net)" + : "example.net"; + + info(`Open a new tab which loads an image`); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_IMAGE_URL + ); + + info("Toggle on the offline mode"); + BrowserOffline.toggleOfflineStatus(); + + info("Open file menu and trigger 'Save Page As'"); + let menubar = document.getElementById("main-menubar"); + let filePopup = document.getElementById("menu_FilePopup"); + + // We only use the shortcut keys to open the file menu in Windows and Linux. + // Mac doesn't have a shortcut to only open the file menu. Instead, we directly + // trigger the save in MAC without any UI interactions. + if (Services.appinfo.OS !== "Darwin") { + let menubarActive = BrowserTestUtils.waitForEvent( + menubar, + "DOMMenuBarActive" + ); + EventUtils.synthesizeKey("KEY_F10"); + await menubarActive; + + let popupShownPromise = BrowserTestUtils.waitForEvent( + filePopup, + "popupshown" + ); + // In window, it still needs one extra down key to open the file menu. + if (Services.appinfo.OS === "WINNT") { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + await popupShownPromise; + } + + let transferCompletePromise = createPromiseForTransferComplete(); + let observerPromise = createPromiseForObservingChannel( + TEST_IMAGE_URL, + partitionKey + ); + + info("Triggering the save process."); + let fileSavePageAsElement = document.getElementById("menu_savePage"); + fileSavePageAsElement.doCommand(); + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + + // Close the file menu. + if (Services.appinfo.OS !== "Darwin") { + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + filePopup, + "popuphidden" + ); + filePopup.hidePopup(); + await popupHiddenPromise; + } + + info("Toggle off the offline mode"); + BrowserOffline.toggleOfflineStatus(); + + // Clean up + BrowserTestUtils.removeTab(tab); + + // Clean up the cache count on the server side. + await fetch(`${TEST_IMAGE_URL}?result`); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } + } +}); + +add_task(async function testPageInfoMediaSaveAs() { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + let partitionKey = partitionPerSite + ? "(http,example.net)" + : "example.net"; + + info( + `Open a new tab for testing "Save AS" in the media panel of the page info.` + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGEINFO_URL + ); + + info("Open the media panel of the pageinfo."); + let pageInfo = BrowserPageInfo( + gBrowser.selectedBrowser.currentURI.spec, + "mediaTab" + ); + + await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); + + let imageTree = pageInfo.document.getElementById("imagetree"); + let imageRowsNum = imageTree.view.rowCount; + + is(imageRowsNum, 2, "There should be two media items here."); + + for (let i = 0; i < imageRowsNum; i++) { + imageTree.view.selection.select(i); + imageTree.ensureRowIsVisible(i); + imageTree.focus(); + + // Wait until the preview is loaded. + let preview = pageInfo.document.getElementById("thepreviewimage"); + let mediaType = pageInfo.gImageView.data[i][1]; // COL_IMAGE_TYPE + if (mediaType == "Image") { + await BrowserTestUtils.waitForEvent(preview, "load"); + } else if (mediaType == "Video") { + await BrowserTestUtils.waitForEvent(preview, "canplaythrough"); + } + + let url = pageInfo.gImageView.data[i][0]; // COL_IMAGE_ADDRESS + info(`Start to save the media item with URL: ${url}`); + + let transferCompletePromise = createPromiseForTransferComplete(); + + // Observe the channel and check if it has the correct partitionKey. + let observerPromise = createPromiseForObservingChannel( + url, + partitionKey + ); + + info("Triggering the save process."); + let saveElement = pageInfo.document.getElementById("imagesaveasbutton"); + saveElement.doCommand(); + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + } + + pageInfo.close(); + BrowserTestUtils.removeTab(tab); + } + } +}); + +add_task(async function testPageInfoMediaMultipleSelectedSaveAs() { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + let partitionKey = partitionPerSite + ? "(http,example.net)" + : "example.net"; + + info( + `Open a new tab for testing "Save AS" in the media panel of the page info.` + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGEINFO_URL + ); + + info("Open the media panel of the pageinfo."); + let pageInfo = BrowserPageInfo( + gBrowser.selectedBrowser.currentURI.spec, + "mediaTab" + ); + + await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); + + // Make sure the preview image is loaded in order to avoid interfering + // following tests. + let preview = pageInfo.document.getElementById("thepreviewimage"); + await BrowserTestUtils.waitForCondition(() => { + return preview.complete; + }); + + let imageTree = pageInfo.document.getElementById("imagetree"); + let imageRowsNum = imageTree.view.rowCount; + + is(imageRowsNum, 2, "There should be two media items here."); + + imageTree.view.selection.selectAll(); + imageTree.focus(); + + let url = pageInfo.gImageView.data[0][0]; // COL_IMAGE_ADDRESS + info(`Start to save the media item with URL: ${url}`); + + let transferCompletePromise = createPromiseForTransferComplete(); + let observerPromises = []; + + // Observe all channels and check if they have the correct partitionKey. + for (let i = 0; i < imageRowsNum; ++i) { + let observerPromise = createPromiseForObservingChannel( + url, + partitionKey + ); + observerPromises.push(observerPromise); + } + + info("Triggering the save process."); + let saveElement = pageInfo.document.getElementById("imagesaveasbutton"); + saveElement.doCommand(); + + info("Waiting for the all channels."); + await Promise.all(observerPromises); + + info("Wait until the save is finished."); + await transferCompletePromise; + + pageInfo.close(); + BrowserTestUtils.removeTab(tab); + } + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_tls_session.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_tls_session.js new file mode 100644 index 0000000000..e131f71169 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_tls_session.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +/** + * Tests that we correctly partition TLS sessions by inspecting the + * socket's peerId. The peerId contains the OriginAttributes suffix which + * includes the partitionKey. + */ + +const TEST_ORIGIN_A = "https://example.com"; +const TEST_ORIGIN_B = "https://example.org"; +const TEST_ORIGIN_C = "https://w3c-test.org"; + +const TEST_ENDPOINT = + "/browser/toolkit/components/antitracking/test/browser/empty.js"; + +const TEST_URL_C = TEST_ORIGIN_C + TEST_ENDPOINT; + +/** + * Waits for a load with the given URL to happen and returns the peerId. + * @param {string} url - The URL expected to load. + * @returns {Promise<string>} a promise which resolves on load with the + * associated socket peerId. + */ +async function waitForLoad(url) { + return new Promise(resolve => { + const TOPIC = "http-on-examine-response"; + + function observer(subject, topic, data) { + if (topic != TOPIC) { + return; + } + subject.QueryInterface(Ci.nsIChannel); + if (subject.URI.spec != url) { + return; + } + + Services.obs.removeObserver(observer, TOPIC); + + resolve(subject.securityInfo.peerId); + } + Services.obs.addObserver(observer, TOPIC); + }); +} + +/** + * Loads a url in the given browser and returns the tls socket's peer id + * associated with the load. + * Note: Loads are identified by URI. If multiple loads with the same URI happen + * concurrently, this method may not map them correctly. + * @param {MozBrowser} browser + * @param {string} url + * @returns {Promise<string>} Resolves on load with the peer id associated with + * the load. + */ +function loadURLInFrame(browser, url) { + let loadPromise = waitForLoad(url); + ContentTask.spawn(browser, [url], async testURL => { + let frame = content.document.createElement("iframe"); + frame.src = testURL; + content.document.body.appendChild(frame); + }); + return loadPromise; +} + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.cache.disk.enable", false], + ["browser.cache.memory.enable", false], + ["privacy.partition.network_state", true], + // The test harness acts as a proxy, so we need to make sure to also + // partition for proxies. + ["privacy.partition.network_state.connection_with_proxy", true], + ], + }); + + // C (first party) + let loadPromiseC = waitForLoad(TEST_URL_C); + await BrowserTestUtils.withNewTab(TEST_URL_C, async () => {}); + let peerIdC = await loadPromiseC; + + // C embedded in C (same origin) + let peerIdCC; + await BrowserTestUtils.withNewTab(TEST_ORIGIN_C, async browser => { + peerIdCC = await loadURLInFrame(browser, TEST_URL_C); + }); + + // C embedded in A (third party) + let peerIdAC; + await BrowserTestUtils.withNewTab(TEST_ORIGIN_A, async browser => { + peerIdAC = await loadURLInFrame(browser, TEST_URL_C); + }); + + // C embedded in B (third party) + let peerIdBC; + await BrowserTestUtils.withNewTab(TEST_ORIGIN_B, async browser => { + peerIdBC = await loadURLInFrame(browser, TEST_URL_C); + }); + + info("Test that top level load and same origin frame have the same peerId."); + is(peerIdC, peerIdCC, "Should have the same peerId"); + + info("Test that all partitioned peer ids are distinct."); + isnot(peerIdCC, peerIdAC, "Should have different peerId partitioned under A"); + isnot(peerIdCC, peerIdBC, "Should have different peerId partitioned under B"); + isnot( + peerIdAC, + peerIdBC, + "Should have a different peerId under different first parties." + ); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_websocket.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_websocket.js new file mode 100644 index 0000000000..11f53d69ed --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_websocket.js @@ -0,0 +1,198 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const FIRST_PARTY_A = "http://example.com"; +const FIRST_PARTY_B = "http://example.org"; +const THIRD_PARTY = "http://example.net"; +const WS_ENDPOINT_HOST = "mochi.test:8888"; + +function getWSTestUrlForHost(host) { + return ( + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + `ws://${host}` + ) + `file_ws_handshake_delay` + ); +} + +function connect(browsingContext, host, protocol) { + let url = getWSTestUrlForHost(host); + info("Creating websocket with endpoint " + url); + + // Create websocket connection in third party iframe. + let createPromise = SpecialPowers.spawn( + browsingContext.children[0], + [url, protocol], + (url, protocol) => { + let ws = new content.WebSocket(url, [protocol]); + ws.addEventListener("error", () => { + ws._testError = true; + }); + if (!content.ws) { + content.ws = {}; + } + content.ws[protocol] = ws; + } + ); + + let openPromise = createPromise.then(() => + SpecialPowers.spawn( + browsingContext.children[0], + [protocol], + async protocol => { + let ws = content.ws[protocol]; + if (ws.readyState != 0) { + return !ws._testError; + } + // Still connecting. + let result = await Promise.race([ + ContentTaskUtils.waitForEvent(ws, "open"), + ContentTaskUtils.waitForEvent(ws, "error"), + ]); + return result.type != "error"; + } + ) + ); + + let result = { createPromise, openPromise }; + return result; +} + +// Open 3 websockets which target the same ip/port combination, but have +// different principals. We send a protocol identifier to the server to signal +// how long the request should be delayed. +// +// When partitioning is disabled A blocks B and B blocks C. The timeline will +// look like this: +// A________ +// B____ +// C_ +// +// When partitioning is enabled, connection handshakes for A and B will run +// (semi-) parallel since they have different origin attributes. B and C share +// origin attributes and therefore still run serially. +// A________ +// B____ +// C_ +// +// By observing the order of the handshakes we can ensure that the queue +// partitioning is working correctly. +async function runTest(partitioned) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.network_state", partitioned]], + }); + + let tabA = BrowserTestUtils.addTab(gBrowser, FIRST_PARTY_A); + await BrowserTestUtils.browserLoaded(tabA.linkedBrowser); + let tabB = BrowserTestUtils.addTab(gBrowser, FIRST_PARTY_B); + await BrowserTestUtils.browserLoaded(tabB.linkedBrowser); + + for (let tab of [tabA, tabB]) { + await SpecialPowers.spawn(tab.linkedBrowser, [THIRD_PARTY], async src => { + let frame = content.document.createElement("iframe"); + frame.src = src; + let loadPromise = ContentTaskUtils.waitForEvent(frame, "load"); + content.document.body.appendChild(frame); + await loadPromise; + }); + } + + // First ensure that we can open websocket connections to the test endpoint. + let { openPromise, createPromise } = await connect( + tabA.linkedBrowser.browsingContext, + WS_ENDPOINT_HOST, + false + ); + await createPromise; + let openPromiseResult = await openPromise; + ok(openPromiseResult, "Websocket endpoint accepts connections."); + + let openedA; + let openedB; + let openedC; + + let { createPromise: createPromiseA, openPromise: openPromiseA } = connect( + tabA.linkedBrowser.browsingContext, + WS_ENDPOINT_HOST, + "test-6" + ); + + openPromiseA = openPromiseA.then(opened => { + openedA = opened; + info("Completed WS connection A"); + if (partitioned) { + ok(openedA, "Should have opened A"); + ok(openedB, "Should have opened B"); + } else { + ok(openedA, "Should have opened A"); + ok(openedB == null, "B should be pending"); + } + }); + await createPromiseA; + + // The frame of connection B is embedded in a different first party as A. + let { createPromise: createPromiseB, openPromise: openPromiseB } = connect( + tabB.linkedBrowser.browsingContext, + WS_ENDPOINT_HOST, + "test-3" + ); + openPromiseB = openPromiseB.then(opened => { + openedB = opened; + info("Completed WS connection B"); + if (partitioned) { + ok(openedA == null, "A should be pending"); + ok(openedB, "Should have opened B"); + ok(openedC == null, "C should be pending"); + } else { + ok(openedA, "Should have opened A"); + ok(openedB, "Should have opened B"); + ok(openedC == null, "C should be pending"); + } + }); + await createPromiseB; + + // The frame of connection C is embedded in the same first party as B. + let { createPromise: createPromiseC, openPromise: openPromiseC } = connect( + tabB.linkedBrowser.browsingContext, + WS_ENDPOINT_HOST, + "test-0" + ); + openPromiseC = openPromiseC.then(opened => { + openedC = opened; + info("Completed WS connection C"); + if (partitioned) { + ok(openedB, "Should have opened B"); + ok(openedC, "Should have opened C"); + } else { + ok(opened, "Should have opened B"); + ok(opened, "Should have opened C"); + } + }); + await createPromiseC; + + // Wait for all connections to complete before closing the tabs. + await Promise.all([openPromiseA, openPromiseB, openPromiseC]); + + BrowserTestUtils.removeTab(tabA); + BrowserTestUtils.removeTab(tabB); + + await SpecialPowers.popPrefEnv(); +} + +add_setup(async function () { + // This test relies on a WS connection timeout > 6 seconds. + await SpecialPowers.pushPrefEnv({ + set: [["network.websocket.timeout.open", 20]], + }); +}); + +add_task(async function test_non_partitioned() { + await runTest(false); +}); + +add_task(async function test_partitioned() { + await runTest(true); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessAutograntRequiresUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessAutograntRequiresUserInteraction.js new file mode 100644 index 0000000000..c2d6898313 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessAutograntRequiresUserInteraction.js @@ -0,0 +1,57 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +async function setAutograntPreferences() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", true], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); +} + +add_task(async function testPopupWithUserInteraction() { + await setPreferences(); + await setAutograntPreferences(); + + // Test that requesting storage access initially does not autogrant. + // If the autogrant doesn't occur, we click reject on the door hanger + // and expect the promise returned by requestStorageAccess to reject. + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + // Grant the storageAccessAPI permission to the third-party. + // This signifies that it has been interacted with and should allow autogrants + // among other behaviors. + const uri = Services.io.newURI(TEST_3RD_PARTY_DOMAIN); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + Services.perms.addFromPrincipal( + principal, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + // Test that requesting storage access autogrants here. If a popup occurs, + // expectNoPopup will cause an error in this test. + await openPageAndRunCode( + TEST_TOP_PAGE, + expectNoPopup, + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessDeniedGivesNoUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessDeniedGivesNoUserInteraction.js new file mode 100644 index 0000000000..cf47c43a14 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessDeniedGivesNoUserInteraction.js @@ -0,0 +1,30 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testGrantGivesPermission() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await SpecialPowers.testPermission( + "storageAccessAPI", + SpecialPowers.Services.perms.UNKNOWN_ACTION, + { + url: "https://tracking.example.org/", + } + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js b/toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js new file mode 100644 index 0000000000..c922234ed2 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js @@ -0,0 +1,374 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); + +const BLOCK = 0; +const ALLOW = 1; + +async function testDoorHanger( + choice, + showPrompt, + useEscape, + topPage, + maxConcurrent, + disableWebcompat = false +) { + info( + `Running doorhanger test with choice #${choice}, showPrompt: ${showPrompt} and ` + + `useEscape: ${useEscape}, topPage: ${topPage}, maxConcurrent: ${maxConcurrent}` + ); + + if (!showPrompt) { + is(choice, ALLOW, "When not showing a prompt, we can only auto-grant"); + ok( + !useEscape, + "When not showing a prompt, we should not be trying to use the Esc key" + ); + } + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.antitracking.enableWebcompat", !disableWebcompat], + ["dom.storage_access.auto_grants", true], + ["dom.storage_access.auto_grants.delayed", false], + ["dom.storage_access.enabled", true], + ["dom.storage_access.max_concurrent_auto_grants", maxConcurrent], + ["dom.storage_access.prompt.testing", false], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let tab = BrowserTestUtils.addTab(gBrowser, topPage); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + async function runChecks() { + // We need to repeat this constant here since runChecks is stringified + // and sent to the content process. + const BLOCK = 0; + + await new Promise(resolve => { + addEventListener( + "message", + function onMessage(e) { + if (e.data.startsWith("choice:")) { + window.choice = e.data.split(":")[1]; + window.useEscape = e.data.split(":")[3]; + removeEventListener("message", onMessage); + resolve(); + } + }, + false + ); + parent.postMessage("getchoice", "*"); + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookies for me"); + + await fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + // Let's do it twice. + await fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + + is(document.cookie, "", "Still no cookies for me"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + if (choice == BLOCK) { + // We've said no, so cookies are still blocked + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookies for me"); + } else { + // We've said yes, so cookies are allowed now + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "I have the cookies!"); + } + } + + let permChanged; + // Only create the promise when we're going to click one of the allow buttons. + if (choice != BLOCK) { + permChanged = TestUtils.topicObserved("perm-changed", (subject, data) => { + let result; + if (choice == ALLOW) { + result = + subject && + subject + .QueryInterface(Ci.nsIPermission) + .type.startsWith("3rdPartyFrameStorage^") && + subject.principal.origin == new URL(topPage).origin && + data == "added"; + } + return result; + }); + } + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + shownPromise.then(async _ => { + if (topPage != gBrowser.currentURI.spec) { + return; + } + ok(showPrompt, "We shouldn't show the prompt when we don't intend to"); + let notification = await new Promise(function poll(resolve) { + let notification = PopupNotifications.getNotification( + "storage-access", + browser + ); + if (notification) { + resolve(notification); + return; + } + setTimeout(poll, 10); + }); + Assert.ok(notification, "Should have gotten the notification"); + + if (choice == BLOCK) { + if (useEscape) { + info("hitting escape"); + EventUtils.synthesizeKey("KEY_Escape", {}, window); + } else { + await clickSecondaryAction(); + } + } else if (choice == ALLOW) { + await clickMainAction(); + } + if (choice != BLOCK) { + await permChanged; + } + }); + + let url = TEST_3RD_PARTY_PAGE + "?disableWaitUntilPermission"; + let ct = SpecialPowers.spawn( + browser, + [{ page: url, callback: runChecks.toString(), choice, useEscape }], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.callback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + if (event.data == "getchoice") { + ifr.contentWindow.postMessage( + "choice:" + obj.choice + ":useEscape:" + obj.useEscape, + "*" + ); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + if (showPrompt) { + await Promise.all([ct, shownPromise]); + } else { + await Promise.all([ct, permChanged]); + } + + let permissionPopupPromise = BrowserTestUtils.waitForEvent( + window, + "popupshown", + true, + event => event.target == gPermissionPanel._permissionPopup + ); + gPermissionPanel._identityPermissionBox.click(); + await permissionPopupPromise; + let permissionItem = document.querySelector( + ".permission-popup-permission-item-3rdPartyFrameStorage" + ); + ok(permissionItem, "Permission item exists"); + ok( + BrowserTestUtils.isVisible(permissionItem), + "Permission item visible in the identity panel" + ); + let permissionLearnMoreLink = document.getElementById( + "permission-popup-storage-access-permission-learn-more" + ); + ok(permissionLearnMoreLink, "Permission learn more link exists"); + ok( + BrowserTestUtils.isVisible(permissionLearnMoreLink), + "Permission learn more link is visible in the identity panel" + ); + permissionPopupPromise = BrowserTestUtils.waitForEvent( + gPermissionPanel._permissionPopup, + "popuphidden" + ); + gPermissionPanel._permissionPopup.hidePopup(); + await permissionPopupPromise; + + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +async function preparePermissionsFromOtherSites(topPage) { + info("Faking permissions from other sites"); + let type = "3rdPartyFrameStorage^https://example.org"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + if (topPage == TEST_TOP_PAGE) { + // For the first page, don't do anything + } else if (topPage == TEST_TOP_PAGE_2) { + // For the second page, only add the permission from the first page + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + } else if (topPage == TEST_TOP_PAGE_3) { + // For the third page, add the permissions from the first two pages + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0); + } else if (topPage == TEST_TOP_PAGE_4) { + // For the fourth page, add the permissions from the first three pages + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_3, type, permission, expireType, 0); + } else if (topPage == TEST_TOP_PAGE_5) { + // For the fifth page, add the permissions from the first four pages + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_3, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_4, type, permission, expireType, 0); + } else if (topPage == TEST_TOP_PAGE_6) { + // For the sixth page, add the permissions from the first five pages + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_3, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_4, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_5, type, permission, expireType, 0); + } else { + ok(false, "Unexpected top page: " + topPage); + } +} + +async function cleanUp() { + info("Cleaning up."); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +async function runRound(topPage, showPrompt, maxConcurrent, disableWebcompat) { + info("Starting round"); + if (showPrompt) { + await preparePermissionsFromOtherSites(topPage); + await testDoorHanger( + BLOCK, + showPrompt, + true, + topPage, + maxConcurrent, + disableWebcompat + ); + await cleanUp(); + await preparePermissionsFromOtherSites(topPage); + await testDoorHanger( + BLOCK, + showPrompt, + false, + topPage, + maxConcurrent, + disableWebcompat + ); + await cleanUp(); + await preparePermissionsFromOtherSites(topPage); + await testDoorHanger( + ALLOW, + showPrompt, + false, + topPage, + maxConcurrent, + disableWebcompat + ); + await cleanUp(); + } else { + await preparePermissionsFromOtherSites(topPage); + await testDoorHanger( + ALLOW, + showPrompt, + false, + topPage, + maxConcurrent, + disableWebcompat + ); + } + await cleanUp(); +} + +add_task(async function test_combinations() { + await runRound(TEST_TOP_PAGE, false, 1); + await runRound(TEST_TOP_PAGE_2, true, 1); + await runRound(TEST_TOP_PAGE, false, 5); + await runRound(TEST_TOP_PAGE_2, false, 5); + await runRound(TEST_TOP_PAGE_3, false, 5); + await runRound(TEST_TOP_PAGE_4, false, 5); + await runRound(TEST_TOP_PAGE_5, false, 5); + await runRound(TEST_TOP_PAGE_6, true, 5); +}); + +add_task(async function test_disableWebcompat() { + await runRound(TEST_TOP_PAGE, true, 5, true); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessFrameInteractionGrantsUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessFrameInteractionGrantsUserInteraction.js new file mode 100644 index 0000000000..13b882c0c7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessFrameInteractionGrantsUserInteraction.js @@ -0,0 +1,70 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); +/* import-globals-from storageAccessAPIHelpers.js */ + +async function testEmbeddedPageBehavior() { + // Get the storage access permission + SpecialPowers.wrap(document).notifyUserGestureActivation(); + let p = document.requestStorageAccess(); + try { + await p; + ok(true, "gain storage access."); + } catch { + ok(false, "denied storage access."); + } + SpecialPowers.wrap(document).clearUserGestureActivation(); + + // Wait until we have the permission before we remove it. + waitUntilPermission( + "https://tracking.example.org/", + "storageAccessAPI", + SpecialPowers.Services.perms.ALLOW_ACTION + ); + + // Remove the storageAccessAPI permission + SpecialPowers.removePermission( + "storageAccessAPI", + "https://tracking.example.org/" + ); + + // Wait until the permission is removed + waitUntilPermission( + "https://tracking.example.org/", + "storageAccessAPI", + SpecialPowers.Services.perms.UNKNOWN_ACTION + ); + + // Interact with the third-party iframe and wait for the permission to appear + SpecialPowers.wrap(document).userInteractionForTesting(); + waitUntilPermission( + "https://tracking.example.org/", + "storageAccessAPI", + SpecialPowers.Services.perms.ALLOW_ACTION + ); +} + +// This test verifies that interacting with a third-party iframe with +// storage access gives the storageAccessAPI permission, as if it were a first +// party. This is done by loading a page, then within an iframe in that page +// requesting storage access, ensuring there is no storageAccessAPI permission, +// then interacting with the page and waiting for that storageAccessAPI +// permission to reappear. +add_task(async function testInteractionGivesPermission() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + testEmbeddedPageBehavior + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessGrantedGivesUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessGrantedGivesUserInteraction.js new file mode 100644 index 0000000000..cd70d8c059 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessGrantedGivesUserInteraction.js @@ -0,0 +1,40 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); +/* import-globals-from storageAccessAPIHelpers.js */ + +async function testEmbeddedPageBehavior() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var p = document.requestStorageAccess(); + try { + await p; + ok(true, "gain storage access."); + } catch { + ok(false, "denied storage access."); + } + SpecialPowers.wrap(document).clearUserGestureActivation(); + waitUntilPermission( + "https://tracking.example.org/", + "storageAccessAPI", + SpecialPowers.Services.perms.ALLOW_ACTION + ); +} + +add_task(async function testGrantGivesPermission() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + testEmbeddedPageBehavior + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPrivilegeAPI.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPrivilegeAPI.js new file mode 100644 index 0000000000..9594fdf270 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPrivilegeAPI.js @@ -0,0 +1,617 @@ +// +// Bug 1724376 - Tests for the privilege requestStorageAccessForOrigin API. +// + +/* import-globals-from storageAccessAPIHelpers.js */ + +"use strict"; + +const TEST_ANOTHER_TRACKER_DOMAIN = "https://itisatracker.org/"; +const TEST_ANOTHER_TRACKER_PAGE = + TEST_ANOTHER_TRACKER_DOMAIN + TEST_PATH + "3rdParty.html"; +const TEST_ANOTHER_4TH_PARTY_DOMAIN = "https://test1.example.org/"; +const TEST_ANOTHER_4TH_PARTY_PAGE = + TEST_ANOTHER_4TH_PARTY_DOMAIN + TEST_PATH + "3rdParty.html"; + +// Insert an iframe with the given id into the content. +async function insertSubFrame(browser, url, id) { + return SpecialPowers.spawn(browser, [url, id], async (url, id) => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", id); + + let loaded = ContentTaskUtils.waitForEvent(ifr, "load", false); + content.document.body.appendChild(ifr); + ifr.src = url; + await loaded; + }); +} + +// Run the given script in the iframe with the given id. +function runScriptInSubFrame(browser, id, script) { + return SpecialPowers.spawn( + browser, + [{ callback: script.toString(), id }], + async obj => { + await new content.Promise(resolve => { + let ifr = content.document.getElementById(obj.id); + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + ifr.contentWindow.postMessage(obj.callback, "*"); + }); + } + ); +} + +function waitStoragePermission(trackingOrigin) { + return TestUtils.topicObserved("perm-changed", (aSubject, aData) => { + let permission = aSubject.QueryInterface(Ci.nsIPermission); + let uri = Services.io.newURI(TEST_DOMAIN); + return ( + permission.type == `3rdPartyStorage^${trackingOrigin}` && + permission.principal.equalsURI(uri) + ); + }); +} + +function clearStoragePermission(trackingOrigin) { + return SpecialPowers.removePermission( + `3rdPartyStorage^${trackingOrigin}`, + TEST_TOP_PAGE + ); +} + +function triggerCommand(button) { + let notifications = PopupNotifications.panel.children; + let notification = notifications[0]; + EventUtils.synthesizeMouseAtCenter(notification[button], {}); +} + +function triggerMainCommand() { + triggerCommand("button"); +} + +function triggerSecondaryCommand() { + triggerCommand("secondaryButton"); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", true], + ["dom.storage_access.auto_grants.delayed", false], + ["dom.storage_access.enabled", true], + ["dom.storage_access.prompt.testing", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + registerCleanupFunction(() => { + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + UrlClassifierTestUtils.cleanupTestTrackers(); + }); +}); + +add_task(async function test_api_only_available_in_privilege_scope() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser, [], async _ => { + ok( + content.document.requestStorageAccessForOrigin, + "The privilege API is available in system privilege code." + ); + }); + + // Open an iframe and check if the privilege is not available in content code. + await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test"); + await runScriptInSubFrame(browser, "test", async function check() { + ok( + !document.requestStorageAccessForOrigin, + "The privilege API is not available in content code." + ); + }); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_privilege_api_with_reject_tracker() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test"); + + // Verify that the third party tracker doesn't have storage access at + // beginning. + await runScriptInSubFrame(browser, "test", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "Setting cookie is blocked"); + }); + + let storagePermissionPromise = waitStoragePermission( + "https://tracking.example.org" + ); + + // Verify if the prompt has been shown. + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + // Call the privilege API. + let callAPIPromise = SpecialPowers.spawn(browser, [], async _ => { + // The privilege API requires user activation. So, we set the user + // activation flag before we call the API. + content.document.notifyUserGestureActivation(); + + try { + await content.document.requestStorageAccessForOrigin( + "https://tracking.example.org/" + ); + } catch (e) { + ok(false, "The API shouldn't throw."); + } + + content.document.clearUserGestureActivation(); + }); + + await shownPromise; + + // Accept the prompt + triggerMainCommand(); + + await callAPIPromise; + + // Verify if the storage access permission is set correctly. + await storagePermissionPromise; + + // Verify if the existing third-party tracker iframe gains the storage + // access. + await runScriptInSubFrame(browser, "test", async _ => { + await hasStorageAccessInitially(); + + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "Successfully set cookies."); + }); + + // Insert another third-party tracker iframe and check if it has storage access. + await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test2"); + await runScriptInSubFrame(browser, "test2", async _ => { + await hasStorageAccessInitially(); + + is(document.cookie, "name=value", "Some cookies for me"); + }); + + // Insert another iframe with different third-party tracker and check it has + // no storage access. + await insertSubFrame(browser, TEST_ANOTHER_TRACKER_PAGE, "test3"); + await runScriptInSubFrame(browser, "test3", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "Setting cookie is blocked for another tracker."); + }); + + await clearStoragePermission("https://tracking.example.org"); + Services.cookies.removeAll(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_privilege_api_with_dFPI() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + await insertSubFrame(browser, TEST_4TH_PARTY_PAGE_HTTPS, "test"); + + // Verify that the third-party context doesn't have storage access at + // beginning. + await runScriptInSubFrame(browser, "test", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=partitioned"; + is( + document.cookie, + "name=partitioned", + "Setting cookie in partitioned context." + ); + }); + + let storagePermissionPromise = waitStoragePermission( + "https://not-tracking.example.com" + ); + + // Verify if the prompt has been shown. + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + // Call the privilege API. + let callAPIPromise = SpecialPowers.spawn(browser, [], async _ => { + // The privilege API requires a user gesture. So, we set the user handling + // flag before we call the API. + content.document.notifyUserGestureActivation(); + + try { + await content.document.requestStorageAccessForOrigin( + "https://not-tracking.example.com/" + ); + } catch (e) { + ok(false, "The API shouldn't throw."); + } + + content.document.clearUserGestureActivation(); + }); + + await shownPromise; + + // Accept the prompt + triggerMainCommand(); + + await callAPIPromise; + + // Verify if the storage access permission is set correctly. + await storagePermissionPromise; + + // Verify if the existing third-party iframe gains the storage access. + await runScriptInSubFrame(browser, "test", async _ => { + await hasStorageAccessInitially(); + + is(document.cookie, "", "No unpartitioned cookies"); + document.cookie = "name=unpartitioned"; + is(document.cookie, "name=unpartitioned", "Successfully set cookies."); + }); + + // Insert another third-party content iframe and check if it has storage access. + await insertSubFrame(browser, TEST_4TH_PARTY_PAGE_HTTPS, "test2"); + await runScriptInSubFrame(browser, "test2", async _ => { + await hasStorageAccessInitially(); + + is( + document.cookie, + "name=unpartitioned", + "Some cookies for unpartitioned context" + ); + }); + + // Insert another iframe with different third-party content and check it has + // no storage access. + await insertSubFrame(browser, TEST_ANOTHER_4TH_PARTY_PAGE, "test3"); + await runScriptInSubFrame(browser, "test3", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "Setting cookie to partitioned context."); + }); + + await clearStoragePermission("https://not-tracking.example.com"); + Services.cookies.removeAll(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_prompt() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", false], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ], + }); + + for (const allow of [false, true]) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + // Verify if the prompt has been shown. + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + let hiddenPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + + // Call the privilege API. + let callAPIPromise = SpecialPowers.spawn(browser, [allow], async allow => { + // The privilege API requires a user gesture. So, we set the user handling + // flag before we call the API. + content.document.notifyUserGestureActivation(); + let isThrown = false; + + try { + await content.document.requestStorageAccessForOrigin( + "https://tracking.example.org" + ); + } catch (e) { + isThrown = true; + } + + is(isThrown, !allow, `The API ${allow ? "shouldn't" : "should"} throw.`); + + content.document.clearUserGestureActivation(); + }); + + await shownPromise; + + let notification = await TestUtils.waitForCondition(_ => + PopupNotifications.getNotification("storage-access", browser) + ); + ok(notification, "Should have gotten the notification"); + + // Click the popup button. + if (allow) { + triggerMainCommand(); + } else { + triggerSecondaryCommand(); + } + + // Wait until the popup disappears. + await hiddenPromise; + + // Wait until the API finishes. + await callAPIPromise; + + await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test"); + + if (allow) { + await runScriptInSubFrame(browser, "test", async _ => { + await hasStorageAccessInitially(); + + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "Successfully set cookies."); + }); + } else { + await runScriptInSubFrame(browser, "test", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookie after setting."); + }); + } + + BrowserTestUtils.removeTab(tab); + } + + await clearStoragePermission("https://tracking.example.org"); + Services.cookies.removeAll(); +}); + +// Tests that the priviledged rSA method should show a prompt when auto grants +// are enabled, but we don't have user activation. When requiring user +// activation, rSA should still reject. +add_task(async function test_prompt_no_user_activation() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ], + }); + + for (let requireUserActivation of [false, true]) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + let shownPromise, hiddenPromise; + + // Verify if the prompt has been shown. + if (!requireUserActivation) { + shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + hiddenPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + } + + // Call the privilege API. + let callAPIPromise = SpecialPowers.spawn( + browser, + [requireUserActivation], + async requireUserActivation => { + let isThrown = false; + + try { + await content.document.requestStorageAccessForOrigin( + "https://tracking.example.org", + requireUserActivation + ); + } catch (e) { + isThrown = true; + } + + is( + isThrown, + requireUserActivation, + `The API ${requireUserActivation ? "shouldn't" : "should"} throw.` + ); + } + ); + + if (!requireUserActivation) { + await shownPromise; + + let notification = await TestUtils.waitForCondition(_ => + PopupNotifications.getNotification("storage-access", browser) + ); + ok(notification, "Should have gotten the notification"); + + // Click the popup button. + triggerMainCommand(); + + // Wait until the popup disappears. + await hiddenPromise; + } + + // Wait until the API finishes. + await callAPIPromise; + + await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test"); + + if (!requireUserActivation) { + await runScriptInSubFrame(browser, "test", async _ => { + await hasStorageAccessInitially(); + + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "Successfully set cookies."); + }); + } else { + await runScriptInSubFrame(browser, "test", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookie after setting."); + }); + } + + BrowserTestUtils.removeTab(tab); + await clearStoragePermission("https://tracking.example.org"); + Services.cookies.removeAll(); + } +}); + +add_task(async function test_invalid_input() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser, [], async _ => { + let isThrown = false; + try { + await content.document.requestStorageAccessForOrigin( + "https://tracking.example.org" + ); + } catch (e) { + isThrown = true; + } + ok(isThrown, "The API should throw without user gesture."); + + content.document.notifyUserGestureActivation(); + isThrown = false; + try { + await content.document.requestStorageAccessForOrigin(); + } catch (e) { + isThrown = true; + } + ok(isThrown, "The API should throw with no input."); + + content.document.notifyUserGestureActivation(); + isThrown = false; + try { + await content.document.requestStorageAccessForOrigin(""); + } catch (e) { + isThrown = true; + is(e.name, "NS_ERROR_MALFORMED_URI", "The input is not a valid url"); + } + ok(isThrown, "The API should throw with empty string."); + + content.document.notifyUserGestureActivation(); + isThrown = false; + try { + await content.document.requestStorageAccessForOrigin("invalid url"); + } catch (e) { + isThrown = true; + is(e.name, "NS_ERROR_MALFORMED_URI", "The input is not a valid url"); + } + ok(isThrown, "The API should throw with invalid url."); + + content.document.clearUserGestureActivation(); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction.js new file mode 100644 index 0000000000..835c02e262 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction.js @@ -0,0 +1,36 @@ +AntiTracking.runTest( + "Storage Access API returns promises that do not maintain user activation for calling its reject handler", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(() => { + ok( + !SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, + "Promise reject handler must not have user activation" + ); + }, true); + ok(!threw, "requestStorageAccess should not throw"); + ok(rejected, "requestStorageAccess should not be available"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // expected blocking notifications + false, // private window + "allow-scripts allow-same-origin allow-popups" // iframe sandbox +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js new file mode 100644 index 0000000000..21bf8b7639 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js @@ -0,0 +1,31 @@ +AntiTracking.runTest( + "Storage Access API returns promises that do not maintain user activation for calling its reject handler", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(() => { + ok( + !SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, + "Promise reject handler must not have user activation" + ); + }, true); + ok(!threw, "requestStorageAccess should not throw"); + ok(rejected, "requestStorageAccess should not be available"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications + false, // private window + "allow-scripts allow-same-origin allow-popups" // iframe sandbox +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseResolveHandlerUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseResolveHandlerUserInteraction.js new file mode 100644 index 0000000000..6db7c4c241 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseResolveHandlerUserInteraction.js @@ -0,0 +1,37 @@ +AntiTracking.runTest( + "Storage Access API returns promises that maintain user activation", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(() => { + ok( + SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, + "Promise handler must run as if we're handling user input" + ); + }); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + + await document.hasStorageAccess(); + + ok( + SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, + "Promise handler must run as if we're handling user input" + ); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, // extra prefs + false, // no window open test + false // no user-interaction test +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRejectsInsecureContexts.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRejectsInsecureContexts.js new file mode 100644 index 0000000000..942e4e3bc7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRejectsInsecureContexts.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testInsecureContext() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_HTTPS, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE, + expectNoPopup, + TEST_3RD_PARTY_PAGE_HTTP, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe.js new file mode 100644 index 0000000000..f658dde790 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe.js @@ -0,0 +1,46 @@ +AntiTracking.runTest( + "Storage Access is removed when subframe navigates", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }, + + // non-blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + null, // no iframe sandbox + "navigate-subframe", // access removal type + // after-removal callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + // TODO: this is just a temporarily fixed, we should update the testcase + // in Bug 1649399 + await hasStorageAccessInitially(); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js new file mode 100644 index 0000000000..b4355f2a24 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js @@ -0,0 +1,41 @@ +AntiTracking.runTest( + "Storage Access is removed when subframe navigates", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }, + + // non-blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications + false, // run in normal window + null, // no iframe sandbox + "navigate-subframe", // access removal type + // after-removal callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + // TODO: this is just a temporarily fixed, we should update the testcase + // in Bug 1649399 + await hasStorageAccessInitially(); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe.js new file mode 100644 index 0000000000..fe79a8d935 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe.js @@ -0,0 +1,44 @@ +AntiTracking.runTest( + "Storage Access is removed when topframe navigates", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }, + + // non-blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + null, // no iframe sandbox + "navigate-topframe", // access removal type + // after-removal callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js new file mode 100644 index 0000000000..63a0480e83 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js @@ -0,0 +1,39 @@ +AntiTracking.runTest( + "Storage Access is removed when topframe navigates", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }, + + // non-blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications + false, // run in normal window + null, // no iframe sandbox + "navigate-topframe", // access removal type + // after-removal callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js new file mode 100644 index 0000000000..85c0d60955 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js @@ -0,0 +1,258 @@ +/* import-globals-from storageAccessAPIHelpers.js */ + +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +AntiTracking.runTest( + "Storage Access API called in a sandboxed iframe", + // blocking callback + async _ => { + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(rejected, "requestStorageAccess shouldn't be available"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + // Only clear the user-interaction permissions for the tracker here so that + // the next test has a clean slate. + await new Promise(resolve => { + Services.clearData.deleteDataFromHost( + Services.io.newURI(TEST_3RD_PARTY_DOMAIN).host, + true, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => resolve() + ); + }); + }, + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Exception List can work in a sandboxed iframe", + // blocking callback + async _ => { + await hasStorageAccessInitially(); + + try { + await navigator.serviceWorker.register("empty.js"); + + ok( + true, + "ServiceWorker can be registered in allowlisted sandboxed iframe!" + ); + } catch (e) { + info("Promise rejected: " + e); + ok( + false, + "ServiceWorker should be able to be registered in allowlisted sandboxed iframe" + ); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [ + "privacy.restrict3rdpartystorage.skip_list", + "http://example.net,https://tracking.example.org", + ], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Storage Access API called in a sandboxed iframe with" + + " allow-storage-access-by-user-activation", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation" +); + +AntiTracking.runTest( + "Verify that sandboxed contexts don't get the saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Verify that sandboxed contexts with" + + " allow-storage-access-by-user-activation get the" + + " saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await document.requestStorageAccess(); + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation" +); + +AntiTracking.runTest( + "Verify that private browsing contexts don't get the saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + true, // run in private window + null // iframe sandbox +); + +AntiTracking.runTest( + "Verify that non-sandboxed contexts get the saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + // We request storage access because the permission itself does not cause storage access to be used + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await document.requestStorageAccess(); + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER // expect blocking notifications- the document is initially loaded without storage access +); + +AntiTracking.runTest( + "Storage Access API called in a Permission Policy controlled iframe", + // blocking callback + async _ => { + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(rejected, "requestStorageAccess shouldn't be available"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + // Only clear the user-interaction permissions for the tracker here so that + // the next test has a clean slate. + await new Promise(resolve => { + Services.clearData.deleteDataFromHost( + Services.io.newURI(TEST_3RD_PARTY_DOMAIN).host, + true, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => resolve() + ); + }); + }, + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications + false, // run in normal window + null, + null, + null, + "storage-access ()" // Disable the storage-access feature +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js new file mode 100644 index 0000000000..1b7f2cdf37 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js @@ -0,0 +1,257 @@ +/* import-globals-from storageAccessAPIHelpers.js */ + +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +AntiTracking.runTest( + "Storage Access API called in a sandboxed iframe", + // blocking callback + async _ => { + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(rejected, "requestStorageAccess shouldn't be available"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + // Only clear the user-interaction permissions for the tracker here so that + // the next test has a clean slate. + await new Promise(resolve => { + Services.clearData.deleteDataFromHost( + Services.io.newURI(TEST_3RD_PARTY_DOMAIN).host, + true, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => resolve() + ); + }); + }, + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Exception List can work in a sandboxed iframe", + // blocking callback + async _ => { + await hasStorageAccessInitially(); + + try { + await navigator.serviceWorker.register("empty.js"); + + ok( + true, + "ServiceWorker can be registered in allowlisted sandboxed iframe!" + ); + } catch (e) { + info("Promise rejected: " + e); + ok( + false, + "ServiceWorker should be able to be registered in allowlisted sandboxed iframe" + ); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [ + "privacy.restrict3rdpartystorage.skip_list", + "http://example.net,https://tracking.example.org", + ], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Storage Access API called in a sandboxed iframe with" + + " allow-storage-access-by-user-activation", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation" +); + +AntiTracking.runTest( + "Verify that sandboxed contexts don't get the saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Verify that sandboxed contexts with" + + " allow-storage-access-by-user-activation get the" + + " saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await document.requestStorageAccess(); + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation" +); + +AntiTracking.runTest( + "Verify that private browsing contexts don't get the saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + true, // run in private window + null // iframe sandbox +); + +AntiTracking.runTest( + "Verify that non-sandboxed contexts get the saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await document.requestStorageAccess(); + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER // expect blocking notifications +); + +AntiTracking.runTest( + "Storage Access API called in a Permission Policy controlled iframe", + // blocking callback + async _ => { + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(rejected, "requestStorageAccess shouldn't be available"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + // Only clear the user-interaction permissions for the tracker here so that + // the next test has a clean slate. + await new Promise(resolve => { + Services.clearData.deleteDataFromHost( + Services.io.newURI(TEST_3RD_PARTY_DOMAIN).host, + true, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => resolve() + ); + }); + }, + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications + false, // run in normal window + null, + null, + null, + "storage-access ()" // Disable the storage-access feature +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeDifferentSite.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeDifferentSite.js new file mode 100644 index 0000000000..5b3edec49e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeDifferentSite.js @@ -0,0 +1,64 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testInitialBlock() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testDifferentSitePermission() { + await setPreferences(/*alwaysPartitionStorage*/ false); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testDifferentSitePermissionAPS() { + await setPreferences(/*alwaysPartitionStorage*/ true); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameOrigin.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameOrigin.js new file mode 100644 index 0000000000..f0b1aca0b6 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameOrigin.js @@ -0,0 +1,43 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testInitialBlock() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testSameOriginPermission() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + expectNoPopup, + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteRead.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteRead.js new file mode 100644 index 0000000000..0dd893eda6 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteRead.js @@ -0,0 +1,43 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testInitialBlock() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testSameSitePermission() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE_8, + expectNoPopup, + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteWrite.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteWrite.js new file mode 100644 index 0000000000..4b99cb266e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteWrite.js @@ -0,0 +1,43 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testInitialBlock() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testSameSitePermissionReversed() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_8, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + expectNoPopup, + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js new file mode 100644 index 0000000000..8fd60eb2dc --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js @@ -0,0 +1,123 @@ +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +AntiTracking._createTask({ + name: "Test that after a storage access grant we have full first-party access", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(); + + const TRACKING_PAGE = + "https://another-tracking.example.net/browser/toolkit/components/antitracking/test/browser/trackingPage.html"; + async function runChecks(name) { + let iframe = document.createElement("iframe"); + iframe.src = TRACKING_PAGE; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.onload = resolve; + }); + + await SpecialPowers.spawn(iframe, [name], name => { + content.postMessage(name, "*"); + }); + + await new Promise(resolve => { + onmessage = e => { + if (e.data == "done") { + resolve(); + } + }; + }); + } + + await runChecks("image"); + }, + extraPrefs: [[APS_PREF, false]], + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + errorMessageDomains: [ + "https://tracking.example.org", + "https://tracking.example.org", + "https://tracking.example.org", + ], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we never grant access to cookieBehavior=2", + cookieBehavior: BEHAVIOR_REJECT, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [[APS_PREF, false]], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + errorMessageDomains: [ + "https://tracking.example.org", + "https://tracking.example.org", + ], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we never grant access to cookieBehavior=3", + cookieBehavior: BEHAVIOR_LIMIT_FOREIGN, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [[APS_PREF, false]], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + errorMessageDomains: [ + "https://tracking.example.org", + "https://tracking.example.org", + ], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js new file mode 100644 index 0000000000..bf66b515c4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js @@ -0,0 +1,122 @@ +const allBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL; +const foreignBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN; + +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +AntiTracking._createTask({ + name: "Test that after a storage access grant we have full first-party access", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(); + + const TRACKING_PAGE = + "https://another-tracking.example.net/browser/toolkit/components/antitracking/test/browser/trackingPage.html"; + async function runChecks(name) { + let iframe = document.createElement("iframe"); + iframe.src = TRACKING_PAGE; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.onload = resolve; + }); + + await SpecialPowers.spawn(iframe, [name], name => { + content.postMessage(name, "*"); + }); + + await new Promise(resolve => { + onmessage = e => { + if (e.data == "done") { + resolve(); + } + }; + }); + } + + await runChecks("image"); + }, + extraPrefs: [[APS_PREF, true]], + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + errorMessageDomains: [ + "https://tracking.example.org", + "https://tracking.example.org", + "https://tracking.example.org", + "https://tracking.example.org", + "https://itisatracker.org", + ], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we never grant access to cookieBehavior=2", + cookieBehavior: BEHAVIOR_REJECT, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [[APS_PREF, true]], + expectedBlockingNotifications: allBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + errorMessageDomains: ["http://example.net", "https://tracking.example.org"], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we never grant access to cookieBehavior=3", + cookieBehavior: BEHAVIOR_LIMIT_FOREIGN, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [[APS_PREF, true]], + expectedBlockingNotifications: foreignBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + errorMessageDomains: ["https://tracking.example.org"], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js b/toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js new file mode 100644 index 0000000000..b5e950cc5f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js @@ -0,0 +1,657 @@ +/* vim: set ts=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/. */ + +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "peuService", + "@mozilla.org/partitioning/exception-list-service;1", + "nsIPartitioningExceptionListService" +); + +const TEST_REDIRECT_TOP_PAGE = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "redirect.sjs?" + TEST_TOP_PAGE; +const TEST_REDIRECT_3RD_PARTY_PAGE = + TEST_DOMAIN + TEST_PATH + "redirect.sjs?" + TEST_3RD_PARTY_PARTITIONED_PAGE; +const TEST_REDIRECT_ANOTHER_3RD_PARTY_PAGE = + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS + + TEST_PATH + + "redirect.sjs?" + + TEST_TOP_PAGE_HTTPS; + +const COLLECTION_NAME = "partitioning-exempt-urls"; +const EXCEPTION_LIST_PREF_NAME = "privacy.restrict3rdpartystorage.skip_list"; + +async function cleanup() { + Services.prefs.clearUserPref(EXCEPTION_LIST_PREF_NAME); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_setup(async function () { + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["privacy.restrict3rdpartystorage.heuristic.redirect", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + registerCleanupFunction(cleanup); +}); + +function executeContentScript(browser, callback, options = {}) { + return SpecialPowers.spawn( + browser, + [ + { + callback: callback.toString(), + ...options, + }, + ], + obj => { + return new content.Promise(async resolve => { + if (obj.page) { + // third-party + let ifr = content.document.createElement("iframe"); + ifr.onload = async () => { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage( + { cb: obj.callback, value: obj.value }, + "*" + ); + }; + + content.addEventListener("message", event => resolve(event.data), { + once: true, + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + } else { + // first-party + let runnableStr = `(() => {return (${obj.callback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + resolve(await runnable.call(content, content, obj.value)); + } + }); + } + ); +} + +function readNetworkCookie(win) { + return win + .fetch("cookies.sjs") + .then(r => r.text()) + .then(text => { + return text.substring("cookie:foopy=".length); + }); +} + +async function writeNetworkCookie(win, value) { + await win.fetch("cookies.sjs?" + value).then(r => r.text()); + return true; +} + +function createDataInFirstParty(browser, value) { + return executeContentScript(browser, writeNetworkCookie, { value }); +} +function getDataFromFirstParty(browser) { + return executeContentScript(browser, readNetworkCookie, {}); +} +function createDataInThirdParty(browser, value) { + return executeContentScript(browser, writeNetworkCookie, { + page: TEST_3RD_PARTY_PARTITIONED_PAGE, + value, + }); +} +function getDataFromThirdParty(browser) { + return executeContentScript(browser, readNetworkCookie, { + page: TEST_3RD_PARTY_PARTITIONED_PAGE, + }); +} + +async function redirectWithUserInteraction(browser, url, wait = null) { + await executeContentScript( + browser, + (content, value) => { + content.document.userInteractionForTesting(); + + let link = content.document.createElement("a"); + link.appendChild(content.document.createTextNode("click me!")); + link.href = value; + content.document.body.appendChild(link); + link.click(); + }, + { + value: url, + } + ); + await BrowserTestUtils.browserLoaded(browser, false, wait || url); +} + +async function checkData(browser, options) { + if ("firstParty" in options) { + is( + await getDataFromFirstParty(browser), + options.firstParty, + "correct first-party data" + ); + } + if ("thirdParty" in options) { + is( + await getDataFromThirdParty(browser), + options.thirdParty, + "correct third-party data" + ); + } +} + +async function runTestRedirectHeuristic(disableHeuristics) { + info("Starting Dynamic FPI Redirect Heuristic test..."); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.restrict3rdpartystorage.heuristic.recently_visited", true], + ["privacy.antitracking.enableWebcompat", !disableHeuristics], + ], + }); + + // mark third-party as tracker + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("initializing..."); + await checkData(browser, { firstParty: "", thirdParty: "" }); + + await Promise.all([ + createDataInFirstParty(browser, "firstParty"), + createDataInThirdParty(browser, "thirdParty"), + ]); + + await checkData(browser, { + firstParty: "firstParty", + thirdParty: "", + }); + + info("load third-party content as first-party"); + await redirectWithUserInteraction( + browser, + TEST_REDIRECT_3RD_PARTY_PAGE, + TEST_3RD_PARTY_PARTITIONED_PAGE + ); + + await checkData(browser, { firstParty: "" }); + await createDataInFirstParty(browser, "heuristicFirstParty"); + await checkData(browser, { firstParty: "heuristicFirstParty" }); + + info("redirect back to first-party page"); + await redirectWithUserInteraction( + browser, + TEST_REDIRECT_TOP_PAGE, + TEST_TOP_PAGE + ); + + info("third-party tracker should NOT able to access first-party data"); + await checkData(browser, { + firstParty: "firstParty", + thirdParty: "", + }); + + // remove third-party from tracker + await UrlClassifierTestUtils.cleanupTestTrackers(); + + info("load third-party content as first-party"); + await redirectWithUserInteraction( + browser, + TEST_REDIRECT_3RD_PARTY_PAGE, + TEST_3RD_PARTY_PARTITIONED_PAGE + ); + + await checkData(browser, { + firstParty: "heuristicFirstParty", + }); + + info("redirect back to first-party page"); + await redirectWithUserInteraction( + browser, + TEST_REDIRECT_TOP_PAGE, + TEST_TOP_PAGE + ); + + info( + `third-party page should ${ + disableHeuristics ? "not " : "" + }be able to access first-party data` + ); + await checkData(browser, { + firstParty: "firstParty", + thirdParty: disableHeuristics ? "" : "heuristicFirstParty", + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); + + await cleanup(); +} + +async function runTestRedirectHeuristicWithSameSite() { + info("Starting Dynamic FPI Redirect Between Same Site Heuristic test..."); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE_HTTPS); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info( + `Redirecting from ${TEST_DOMAIN_HTTPS} to ${TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS}` + ); + + await redirectWithUserInteraction( + browser, + TEST_REDIRECT_ANOTHER_3RD_PARTY_PAGE, + TEST_TOP_PAGE_HTTPS + ); + + info("Checking if a permission was set between the redirect"); + + const principal = browser.contentPrincipal; + + is( + Services.perms.testPermissionFromPrincipal( + principal, + `3rdPartyStorage^${TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS.slice(0, -1)}` + ), + Services.perms.UNKNOWN_ACTION, + "No permission was set for same-site redirect" + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + await cleanup(); +} +add_task(async function testRedirectHeuristic() { + await runTestRedirectHeuristic(false); +}); + +add_task(runTestRedirectHeuristicWithSameSite); + +add_task(async function testRedirectHeuristicDisabled() { + await runTestRedirectHeuristic(true); +}); + +class UpdateEvent extends EventTarget {} +function waitForEvent(element, eventName) { + return new Promise(function (resolve) { + element.addEventListener(eventName, e => resolve(e.detail), { once: true }); + }); +} + +// The test URLs have a trailing / which means they're not valid origins. +const TEST_ORIGIN = TEST_DOMAIN.substring(0, TEST_DOMAIN.length - 1); +const TEST_3RD_PARTY_ORIGIN = TEST_3RD_PARTY_DOMAIN.substring( + 0, + TEST_3RD_PARTY_DOMAIN.length - 1 +); + +async function runTestExceptionListPref(disableHeuristics) { + info("Starting Dynamic FPI exception list test pref"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false], + ["privacy.antitracking.enableWebcompat", !disableHeuristics], + ], + }); + + info("Creating new tabs"); + let tabThirdParty = BrowserTestUtils.addTab( + gBrowser, + TEST_3RD_PARTY_PARTITIONED_PAGE + ); + gBrowser.selectedTab = tabThirdParty; + + let browserThirdParty = gBrowser.getBrowserForTab(tabThirdParty); + await BrowserTestUtils.browserLoaded(browserThirdParty); + + let tabFirstParty = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tabFirstParty; + + let browserFirstParty = gBrowser.getBrowserForTab(tabFirstParty); + await BrowserTestUtils.browserLoaded(browserFirstParty); + + info("initializing..."); + await Promise.all([ + checkData(browserFirstParty, { firstParty: "", thirdParty: "" }), + checkData(browserThirdParty, { firstParty: "" }), + ]); + + info("fill default data"); + await Promise.all([ + createDataInFirstParty(browserFirstParty, "firstParty"), + createDataInThirdParty(browserFirstParty, "thirdParty"), + createDataInFirstParty(browserThirdParty, "ExceptionListFirstParty"), + ]); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "thirdParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set exception list pref"); + Services.prefs.setStringPref( + EXCEPTION_LIST_PREF_NAME, + `${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN}` + ); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: disableHeuristics ? "thirdParty" : "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set incomplete exception list pref"); + Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, `${TEST_ORIGIN}`); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "thirdParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set exception list pref, with extra semicolons"); + Services.prefs.setStringPref( + EXCEPTION_LIST_PREF_NAME, + `;${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN};;` + ); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: disableHeuristics ? "thirdParty" : "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set exception list pref, with subdomain wildcard"); + Services.prefs.setStringPref( + EXCEPTION_LIST_PREF_NAME, + `${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN.replace("tracking", "*")}` + ); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: disableHeuristics ? "thirdParty" : "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tabFirstParty); + BrowserTestUtils.removeTab(tabThirdParty); + + await SpecialPowers.popPrefEnv(); + + await cleanup(); +} + +add_task(async function testExceptionListPref() { + await runTestExceptionListPref(false); +}); + +add_task(async function testExceptionListPrefDisabled() { + await runTestExceptionListPref(true); +}); + +add_task(async function testExceptionListRemoteSettings() { + info("Starting Dynamic FPI exception list test (remote settings)"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false], + ], + }); + + // Make sure we have a pref initially, since the exception list service + // requires it. + Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, ""); + + // Add some initial data + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), []); + + // make peuSerivce start working by calling + // registerAndRunExceptionListObserver + let updateEvent = new UpdateEvent(); + let obs = data => { + let event = new CustomEvent("update", { detail: data }); + updateEvent.dispatchEvent(event); + }; + let promise = waitForEvent(updateEvent, "update"); + peuService.registerAndRunExceptionListObserver(obs); + await promise; + + info("Creating new tabs"); + let tabThirdParty = BrowserTestUtils.addTab( + gBrowser, + TEST_3RD_PARTY_PARTITIONED_PAGE + ); + gBrowser.selectedTab = tabThirdParty; + + let browserThirdParty = gBrowser.getBrowserForTab(tabThirdParty); + await BrowserTestUtils.browserLoaded(browserThirdParty); + + let tabFirstParty = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tabFirstParty; + + let browserFirstParty = gBrowser.getBrowserForTab(tabFirstParty); + await BrowserTestUtils.browserLoaded(browserFirstParty); + + info("initializing..."); + await Promise.all([ + checkData(browserFirstParty, { firstParty: "", thirdParty: "" }), + checkData(browserThirdParty, { firstParty: "" }), + ]); + + info("fill default data"); + await Promise.all([ + createDataInFirstParty(browserFirstParty, "firstParty"), + createDataInThirdParty(browserFirstParty, "thirdParty"), + createDataInFirstParty(browserThirdParty, "ExceptionListFirstParty"), + ]); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "thirdParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set exception list remote settings"); + + // set records + promise = waitForEvent(updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: [ + { + id: "1", + last_modified: 1000000000000001, + firstPartyOrigin: TEST_ORIGIN, + thirdPartyOrigin: TEST_3RD_PARTY_ORIGIN, + }, + ], + }, + }); + + let list = await promise; + is( + list, + `${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN}`, + "exception list is correctly set" + ); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tabFirstParty); + BrowserTestUtils.removeTab(tabThirdParty); + + promise = waitForEvent(updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: [], + }, + }); + is(await promise, "", "Exception list is cleared"); + + peuService.unregisterExceptionListObserver(obs); + await cleanup(); +}); + +add_task(async function testWildcardExceptionListPref() { + info("Starting Dynamic FPI wirdcard exception list test pref"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false], + ], + }); + + info("Creating new tabs"); + let tabThirdParty = BrowserTestUtils.addTab( + gBrowser, + TEST_3RD_PARTY_PARTITIONED_PAGE + ); + gBrowser.selectedTab = tabThirdParty; + + let browserThirdParty = gBrowser.getBrowserForTab(tabThirdParty); + await BrowserTestUtils.browserLoaded(browserThirdParty); + + let tabFirstParty = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tabFirstParty; + + let browserFirstParty = gBrowser.getBrowserForTab(tabFirstParty); + await BrowserTestUtils.browserLoaded(browserFirstParty); + + info("initializing..."); + await Promise.all([ + checkData(browserFirstParty, { firstParty: "", thirdParty: "" }), + checkData(browserThirdParty, { firstParty: "" }), + ]); + + info("fill default data"); + await Promise.all([ + createDataInFirstParty(browserFirstParty, "firstParty"), + createDataInThirdParty(browserFirstParty, "thirdParty"), + createDataInFirstParty(browserThirdParty, "ExceptionListFirstParty"), + ]); + + info("check initial data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "thirdParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set wildcard (1st-party) pref"); + Services.prefs.setStringPref( + EXCEPTION_LIST_PREF_NAME, + `*,${TEST_3RD_PARTY_ORIGIN}` + ); + + info("check wildcard (1st-party) data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set invalid exception list pref"); + Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, "*,*"); + + info("check initial data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "thirdParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set wildcard (3rd-party) pref"); + Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, `${TEST_ORIGIN},*`); + + info("check wildcard (3rd-party) data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tabFirstParty); + BrowserTestUtils.removeTab(tabThirdParty); + + await cleanup(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js b/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js new file mode 100644 index 0000000000..5b68975d03 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js @@ -0,0 +1,912 @@ +function waitStoragePermission() { + return new Promise(resolve => { + let id = setInterval(async _ => { + if ( + await SpecialPowers.testPermission( + `3rdPartyStorage^${TEST_3RD_PARTY_DOMAIN.slice(0, -1)}`, + SpecialPowers.Services.perms.ALLOW_ACTION, + TEST_DOMAIN + ) + ) { + clearInterval(id); + resolve(); + } + }, 0); + }); +} + +add_setup(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); +}); + +async function runTestWindowOpenHeuristic(disableHeuristics) { + info( + `Starting window.open() heuristic test with heuristic ${ + disableHeuristics ? "disabled" : "enabled" + }.` + ); + + await SpecialPowers.pushPrefEnv({ + set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]], + }); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_WO, + disableHeuristics, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }).toString(); + + // If the heuristic is disabled, we won't get storage access. + if (obj.disableHeuristics) { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await stillNoStorageAccess(); + }).toString(); + } else { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + } + + info("Checking if storage access is denied"); + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_task(async function testWindowOpenHeuristic() { + await runTestWindowOpenHeuristic(false); +}); + +add_task(async function testWindowOpenHeuristicDisabled() { + await runTestWindowOpenHeuristic(true); +}); + +add_task(async function testDoublyNestedWindowOpenHeuristic() { + info("Starting doubly nested window.open() heuristic test..."); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_WO, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + + info("Checking if storage access is denied"); + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +async function runTestUserInteractionHeuristic(disableHeuristics) { + info( + `Starting user interaction heuristic test with heuristic ${ + disableHeuristics ? "disabled" : "enabled" + }.` + ); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]], + }); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }).toString(); + + info("Checking if storage access is denied"); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info( + "The 3rd party content should not have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + + info("Opening a window from the iframe."); + await SpecialPowers.spawn(ifr, [obj.popup], async popup => { + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + // We need to check the document URI for Fission. It's because the + // 'domwindowclosed' would be triggered twice, one for the + // 'about:blank' page and another for the tracker page. + if ( + aTopic == "domwindowclosed" && + aSubject.document.documentURI == + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html" + ) { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + content.open(popup); + + info("Let's wait for the window to be closed"); + await windowClosed; + }); + + info("The 3rd party content should have access to first party storage."); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + } + ); + + await AntiTracking.interactWithTracker(); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + disableHeuristics, + }, + ], + async obj => { + let msg = {}; + + msg.blockingCallback = (async _ => { + await noStorageAccessInitially(); + }).toString(); + + // If the heuristic is disabled, we won't get storage access. + if (obj.disableHeuristics) { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await stillNoStorageAccess(); + }).toString(); + } else { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + } + + info("Checking if storage access is denied"); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info( + "The 3rd party content should not have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + + info("Opening a window from the iframe."); + await SpecialPowers.spawn(ifr, [obj.popup], async popup => { + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + // We need to check the document URI here as well for the same + // reason above. + if ( + aTopic == "domwindowclosed" && + aSubject.document.documentURI == + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html" + ) { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + content.open(popup); + + info("Let's wait for the window to be closed"); + await windowClosed; + }); + + info("The 3rd party content should have access to first party storage."); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage( + { callback: msg.nonBlockingCallback }, + "*" + ); + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (!disableHeuristics) { + info("Wait until the storage permission is ready before cleaning up."); + await waitStoragePermission(); + } + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + + await SpecialPowers.popPrefEnv(); +} + +add_task(async function testUserInteractionHeuristic() { + await runTestUserInteractionHeuristic(false); +}); + +add_task(async function testUserInteractionHeuristicDisabled() { + await runTestUserInteractionHeuristic(true); +}); + +add_task(async function testDoublyNestedUserInteractionHeuristic() { + info("Starting doubly nested user interaction heuristic test..."); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }).toString(); + + msg.openWindowCallback = (async url => { + open(url); + }).toString(); + + info("Checking if storage access is denied"); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info( + "The 3rd party content should not have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + if (aTopic == "domwindowclosed") { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + info("Opening a window from the iframe."); + ifr.contentWindow.postMessage( + { callback: msg.openWindowCallback, arg: obj.popup }, + "*" + ); + + info("Let's wait for the window to be closed"); + await windowClosed; + + info("The 3rd party content should have access to first party storage."); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + } + ); + + await AntiTracking.interactWithTracker(); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + await noStorageAccessInitially(); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + + msg.openWindowCallback = (async url => { + open(url); + }).toString(); + + info("Checking if storage access is denied"); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info( + "The 3rd party content should not have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + if (aTopic == "domwindowclosed") { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + info("Opening a window from the iframe."); + ifr.contentWindow.postMessage( + { callback: msg.openWindowCallback, arg: obj.popup }, + "*" + ); + + info("Let's wait for the window to be closed"); + await windowClosed; + + info("The 3rd party content should have access to first party storage."); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage( + { callback: msg.nonBlockingCallback }, + "*" + ); + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Wait until the storage permission is ready before cleaning up."); + await waitStoragePermission(); + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +async function runTestFirstPartyWindowOpenHeuristic(disableHeuristics) { + info( + `Starting first-party window.open() heuristic test with heuristic ${ + disableHeuristics ? "disabled" : "enabled" + }.` + ); + + await SpecialPowers.pushPrefEnv({ + set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]], + }); + + // Interact with the tracker first before testing window.open heuristic + await AntiTracking.interactWithTracker(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE, + }, + ], + async obj => { + info("Tracker shouldn't have storage access initially"); + let msg = {}; + msg.blockingCallback = (async _ => { + await noStorageAccessInitially(); + }).toString(); + + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg.blockingCallback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.id = "ifr"; + ifr.src = obj.page; + }); + } + ); + + info("Calling window.open in a first-party iframe"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_IFRAME_PAGE, + popup: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyOpen.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info("Opening a window from the iframe."); + await SpecialPowers.spawn(ifr, [obj.popup], async popup => { + await new content.Promise(resolve => { + content.open(popup); + content.addEventListener("message", function msg(event) { + if (event.data == "hello!") { + resolve(); + } + }); + }); + }); + } + ); + + await SpecialPowers.spawn(browser, [{ disableHeuristics }], async obj => { + info( + "If the heuristic is enabled, the tracker should have storage access now." + ); + let msg = {}; + + // If the heuristic is disabled, we won't get storage access. + if (obj.disableHeuristics) { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await stillNoStorageAccess(); + }).toString(); + } else { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + } + + await new content.Promise(resolve => { + let ifr = content.document.getElementById("ifr"); + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg.nonBlockingCallback, "*"); + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + }); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_task(async function testFirstPartyWindowOpenHeuristic() { + await runTestFirstPartyWindowOpenHeuristic(false); +}); + +add_task(async function testFirstPartyWindowOpenHeuristicDisabled() { + await runTestFirstPartyWindowOpenHeuristic(true); +}); + +add_task(async function () { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Arguments.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Arguments.js new file mode 100644 index 0000000000..a88fc8bcb3 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Arguments.js @@ -0,0 +1,117 @@ +add_task(async function testArgumentInRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_4TH_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async _ => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite("blob://test"); + try { + await p; + ok(false, "Blob URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.requestStorageAccessUnderSite("about:config"); + try { + await p; + ok(false, "about URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.requestStorageAccessUnderSite("qwertyuiop"); + try { + await p; + ok(false, "Non URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.requestStorageAccessUnderSite(""); + try { + await p; + ok(false, "Nullstring must be rejected."); + } catch { + ok(true, "Must reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testArgumentInCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async _ => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = + content.document.completeStorageAccessRequestFromSite("blob://test"); + try { + await p; + ok(false, "Blob URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.completeStorageAccessRequestFromSite("about:config"); + try { + await p; + ok(false, "about URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.completeStorageAccessRequestFromSite("qwertyuiop"); + try { + await p; + ok(false, "Non URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.completeStorageAccessRequestFromSite(""); + try { + await p; + ok(false, "Nullstring must be rejected."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookieBehavior.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookieBehavior.js new file mode 100644 index 0000000000..ea44a34f24 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookieBehavior.js @@ -0,0 +1,412 @@ +add_task(async function testBehaviorAcceptRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testBehaviorAcceptCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.org", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testBehaviorRejectRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testBehaviorRejectCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.org", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task( + async function testBehaviorLimitForeignRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_LIMIT_FOREIGN], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); + } +); + +add_task(async function testBehaviorLimitForeignCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_LIMIT_FOREIGN], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.org", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task( + async function testBehaviorRejectForeignRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_FOREIGN], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testBehaviorRejectForeignCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_FOREIGN], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.org", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testBehaviorRejectTrackerRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_DOMAIN, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testBehaviorRejectTrackerCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testBehaviorRejectTrackerAndPartitionForeignRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_DOMAIN, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testBehaviorRejectTrackerAndPartitionForeignCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + } +); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookiePermission.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookiePermission.js new file mode 100644 index 0000000000..64ead20020 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookiePermission.js @@ -0,0 +1,174 @@ +add_task(async _ => { + PermissionTestUtils.add( + TEST_4TH_PARTY_PAGE, + "cookie", + Services.perms.ALLOW_ACTION + ); +}); + +add_task(async function testCookiePermissionRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_4TH_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPermissions(); +}); + +add_task(async function testCookiePermissionCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: Services.perms.ALLOW_ACTION, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPermissions(); +}); + +add_task(async _ => { + Services.perms.removeAll(); +}); + +add_task(async _ => { + PermissionTestUtils.add( + TEST_4TH_PARTY_PAGE, + "cookie", + Services.perms.DENY_ACTION + ); +}); + +add_task( + async function testCookiePermissionRejectRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_4TH_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPermissions(); + } +); + +add_task( + async function testCookiePermissionRejectCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: Services.perms.ALLOW_ACTION, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPermissions(); + } +); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CrossOriginSameSite.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CrossOriginSameSite.js new file mode 100644 index 0000000000..ca3e47d8e7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CrossOriginSameSite.js @@ -0,0 +1,162 @@ +add_task(async function testIntermediatePreferenceReadSameSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_DOMAIN_7, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject because we don't have the initial request."); + } + }); + + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^https://example.com", + allow: 1, + context: TEST_DOMAIN_7, + }, + ]); + + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject because the permission is cross site."); + } + }); + + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^https://example.org", + allow: 1, + context: TEST_DOMAIN_7, + }, + ]); + + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok( + true, + "Must resolve now that we have the permission from the embedee." + ); + } catch { + ok(false, "Must not reject."); + } + }); + + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^https://example.org", + allow: 1, + context: TEST_DOMAIN_8, + }, + ]); + + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok( + true, + "Must resolve now that we have the permission from the embedee." + ); + } catch { + ok(false, "Must not reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); +}); + +// Note: TEST_DOMAIN_7 and TEST_DOMAIN_8 are Same-Site +add_task(async function testIntermediatePreferenceWriteCrossOrigin() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN_8], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok( + true, + "Must resolve- no funny business here, we just want to set the intermediate pref" + ); + } catch { + ok(false, "Must not reject."); + } + }); + + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + TEST_DOMAIN_8 + ); + // Important to note that this is the site but not origin of TEST_3RD_PARTY_PAGE + var permission = Services.perms.testPermissionFromPrincipal( + principal, + "AllowStorageAccessRequest^https://example.org" + ); + ok(permission == Services.perms.ALLOW_ACTION); + + // Test that checking the permission across site works + principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + TEST_DOMAIN_7 + ); + // Important to note that this is the site but not origin of TEST_3RD_PARTY_PAGE + permission = Services.perms.testPermissionFromPrincipal( + principal, + "AllowStorageAccessRequest^https://example.org" + ); + ok(permission == Services.perms.ALLOW_ACTION); + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Doorhanger.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Doorhanger.js new file mode 100644 index 0000000000..e8d6ce9bb1 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Doorhanger.js @@ -0,0 +1,122 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); + +async function cleanUp() { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_task(async function testDoorhangerRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["dom.storage_access.prompt.testing", false], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_4TH_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + let permChanged = TestUtils.topicObserved("perm-changed", (subject, data) => { + let result = + subject + .QueryInterface(Ci.nsIPermission) + .type.startsWith("AllowStorageAccessRequest^") && + subject.principal.origin == new URL(TEST_TOP_PAGE).origin && + data == "added"; + return result; + }).then(() => { + ok(true, "Permission changed to add intermediate permission"); + }); + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ).then(_ => { + ok(true, "Must display doorhanger from RequestStorageAccessUnderSite"); + return clickMainAction(); + }); + let sp = SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + let p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + + await Promise.all([sp, shownPromise, permChanged]); + await cleanUp(); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testNoDoorhangerCompleteStorageAccessRequestFromSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.prompt.testing", false], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + let popupShown = false; + // This promise is used to the absence of a doorhanger showing up. + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown") + .then(_ => { + // This will be called if a doorhanger is shown. + ok( + false, + "Must not display doorhanger from CompleteStorageAccessRequestFromSite" + ); + popupShown = true; + }) + .catch(_ => { + // This will be called when the test ends if a doorhanger is not shown + ok(true, "It is expected for this popup to not show up."); + }); + await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + let p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + ok(!popupShown, "Must not have shown a popup during this test."); + await cleanUp(); + await BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Embed.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Embed.js new file mode 100644 index 0000000000..86f584763c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Embed.js @@ -0,0 +1,155 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +async function requestStorageAccessUnderSiteAndExpectSuccess() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var p = document.requestStorageAccessUnderSite("http://example.org"); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } +} + +async function requestStorageAccessUnderSiteAndExpectFailure() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var p = document.requestStorageAccessUnderSite("http://example.org"); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } +} + +async function completeStorageAccessRequestFromSiteAndExpectSuccess() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var p = document.completeStorageAccessRequestFromSite("http://example.org"); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } +} + +async function completeStorageAccessRequestFromSiteAndExpectFailure() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var p = document.completeStorageAccessRequestFromSite("http://example.org"); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } +} + +async function setIntermediatePreference() { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.org", + allow: 1, + context: "http://example.com/", + }, + ]); +} + +async function configurePrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); +} + +add_task(async function rSAUS_sameOriginIframe() { + await configurePrefs(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + () => {}, + TEST_DOMAIN_7 + TEST_PATH + "3rdParty.html", + requestStorageAccessUnderSiteAndExpectSuccess + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function rSAUS_sameSiteIframe() { + await configurePrefs(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + () => {}, + TEST_DOMAIN_8 + TEST_PATH + "3rdParty.html", + requestStorageAccessUnderSiteAndExpectSuccess + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function rSAUS_crossSiteIframe() { + await configurePrefs(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + () => {}, + TEST_DOMAIN + TEST_PATH + "3rdParty.html", + requestStorageAccessUnderSiteAndExpectFailure + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function cSAR_sameOriginIframe() { + await configurePrefs(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + setIntermediatePreference, + TEST_DOMAIN_7 + TEST_PATH + "3rdParty.html", + completeStorageAccessRequestFromSiteAndExpectSuccess + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function cSAR_sameSiteIframe() { + await configurePrefs(); + await setIntermediatePreference(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + () => {}, + TEST_DOMAIN_8 + TEST_PATH + "3rdParty.html", + completeStorageAccessRequestFromSiteAndExpectSuccess + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function cSAR_crossSiteIframe() { + await configurePrefs(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + setIntermediatePreference, + TEST_DOMAIN + TEST_PATH + "3rdParty.html", + completeStorageAccessRequestFromSiteAndExpectFailure + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Enable.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Enable.js new file mode 100644 index 0000000000..6ee61cc378 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Enable.js @@ -0,0 +1,86 @@ +add_task(async function testDefaultDisabled() { + let value = Services.prefs.getBoolPref( + "dom.storage_access.forward_declared.enabled" + ); + ok(!value, "dom.storage_access.forward_declared.enabled should be false"); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async _ => { + ok( + content.window.requestStorageAccessUnderSite == undefined, + "API should not be on the window" + ); + ok( + content.window.completeStorageAccessRequestFromSite == undefined, + "API should not be on the window" + ); + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testExplicitlyDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.storage_access.forward_declared.enabled", false]], + }); + let value = Services.prefs.getBoolPref( + "dom.storage_access.forward_declared.enabled" + ); + ok(!value, "dom.storage_access.forward_declared.enabled should be false"); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async _ => { + ok( + content.window.requestStorageAccessUnderSite == undefined, + "API should not be on the window" + ); + ok( + content.window.completeStorageAccessRequestFromSite == undefined, + "API should not be on the window" + ); + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testExplicitlyEnabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ], + }); + let value = Services.prefs.getBoolPref( + "dom.storage_access.forward_declared.enabled" + ); + ok(value, "dom.storage_access.forward_declared.enabled should be true"); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async _ => { + ok( + content.document.requestStorageAccessUnderSite != undefined, + "API should be on the window" + ); + ok( + content.document.completeStorageAccessRequestFromSite != undefined, + "API should be on the window" + ); + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_RequireIntermediatePermission.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_RequireIntermediatePermission.js new file mode 100644 index 0000000000..a5cc6f10b5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_RequireIntermediatePermission.js @@ -0,0 +1,61 @@ +add_task(async function testIntermediatePermissionRequired() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject because we don't have the initial request."); + } + }); + + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^https://example.org", + allow: 1, + context: TEST_TOP_PAGE, + }, + ]); + + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok( + true, + "Must resolve now that we have the permission from the embedee." + ); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_StorageAccessPermission.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_StorageAccessPermission.js new file mode 100644 index 0000000000..0b4f3e7273 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_StorageAccessPermission.js @@ -0,0 +1,94 @@ +add_task( + async function testStorageAccessPermissionRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_4TH_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "3rdPartyStorage^http://not-tracking.example.com", + allow: 1, + context: tp, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testStorageAccessPermissionCompleteStorageAccessRequestFromSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: 1, + context: content.document, + }, + { + type: "3rdPartyStorage^http://not-tracking.example.com", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + } +); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_UserActivation.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_UserActivation.js new file mode 100644 index 0000000000..49dd73fbf7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_UserActivation.js @@ -0,0 +1,63 @@ +add_task(async function testUserActivations() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + // Part 1: open the embedded site as a top level + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + // Part 2: requestStorageAccessUnderSite without activation + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(false, "Must not resolve without user activation."); + } catch { + ok(true, "Must reject without user activation."); + } + // Part 3: requestStorageAccessUnderSite with activation + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve with user activation and autogrant."); + } catch { + ok(false, "Must not reject with user activation."); + } + }); + // Part 4: open the embedding site as a top level + let tab2 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser2 = tab2.linkedBrowser; + await SpecialPowers.spawn(browser2, [TEST_3RD_PARTY_DOMAIN], async tp => { + // Part 5: completeStorageAccessRequestFromSite without activation + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve without user activation."); + } catch { + ok(false, "Must not reject without user activation in this context."); + } + }); + await BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.removeTab(tab2); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_cookies_on_grant.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_cookies_on_grant.js new file mode 100644 index 0000000000..9e2eaf17e8 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_cookies_on_grant.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Inserts an iframe element and resolves once the iframe has loaded. + * @param {*} browser - Browser to insert the iframe into. + * @param {string} url - URL to load in the iframe. + * @returns {Promise<BrowsingContext>} Promise which resolves to the iframe's + * BrowsingContext. + */ +function insertIframeAndWaitForLoad(browser, url) { + return SpecialPowers.spawn(browser, [url], async url => { + let iframe = content.document.createElement("iframe"); + iframe.src = url; + content.document.body.appendChild(iframe); + // Wait for it to load. + await ContentTaskUtils.waitForEvent(iframe, "load"); + + return iframe.browsingContext; + }); +} + +// Tests that first party cookies are available to an iframe with storage +// access, without reload, while the first party context tab is still open. +add_task(async function test_with_first_party_tab_open() { + await BrowserTestUtils.withNewTab("https://example.org", async browser => { + info("Set a first party cookie via `document.cookie`."); + await SpecialPowers.spawn(browser, [], async () => { + content.document.cookie = "foo=bar; Secure; SameSite=None; max-age=3600;"; + }); + + info("Keep example.org's first party tab open."); + await BrowserTestUtils.withNewTab("https://example.com", async browser => { + info("Insert an iframe with example.org."); + let iframeBC = await insertIframeAndWaitForLoad( + browser, + "https://example.org" + ); + + await SpecialPowers.spawn(iframeBC, [], async () => { + ok( + !(await content.document.hasStorageAccess()), + "example.org should not have storage access initially." + ); + + info("Simulate user activation."); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + + info("Request storage access."); + await content.document.requestStorageAccess(); + + ok( + await content.document.hasStorageAccess(), + "example.org should have storage access after request succeeded." + ); + + is( + content.document.cookie, + "foo=bar", + "example.org should have access to the cookie set in the first party context previously." + ); + }); + }); + }); + + info("Cleanup."); + Services.perms.removeAll(); + Services.cookies.removeAll(); +}); + +// Tests that first party cookies are available to an iframe with storage +// access, without reload, after the first party context tab has been closed. +add_task(async function test_all_tabs_closed() { + await BrowserTestUtils.withNewTab("https://example.org", async browser => { + info("Set a first party cookie via `document.cookie`."); + await SpecialPowers.spawn(browser, [], async () => { + content.document.cookie = "foo=bar; Secure; SameSite=None; max-age=3600;"; + }); + }); + info( + "Now that example.org's tab is closed, open a new tab with example.com which embeds example.org." + ); + await BrowserTestUtils.withNewTab("https://example.com", async browser => { + info("Insert an iframe with example.org."); + let iframeBC = await insertIframeAndWaitForLoad( + browser, + "https://example.org" + ); + + await SpecialPowers.spawn(iframeBC, [], async () => { + ok( + !(await content.document.hasStorageAccess()), + "example.org should not have storage access initially." + ); + + content.document.cookie = + "fooPartitioned=bar; Secure; SameSite=None; max-age=3600;"; + + info("Simulate user activation."); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + + info("Request storage access."); + await content.document.requestStorageAccess(); + + ok( + await content.document.hasStorageAccess(), + "example.org should have storage access after request succeeded." + ); + + is( + content.document.cookie, + "foo=bar", + "example.org should have access to the cookie set in the first party context previously." + ); + }); + }); + + info("Cleanup."); + Services.perms.removeAll(); + Services.cookies.removeAll(); +}); + +// Tests that an iframe with storage access receives cookie changes done in +// another tab in the first party context. +add_task(async function test_cookie_updates_broadcasted_to_other_tabs() { + info("Open a new tab with example.com which embeds example.org."); + await BrowserTestUtils.withNewTab("https://example.com", async browser => { + info("Insert an iframe with example.org."); + let iframeBC = await insertIframeAndWaitForLoad( + browser, + "https://example.org" + ); + + await SpecialPowers.spawn(iframeBC, [], async () => { + ok( + !(await content.document.hasStorageAccess()), + "example.org should not have storage access initially." + ); + + info("Simulate user activation."); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + + info("Request storage access."); + await content.document.requestStorageAccess(); + + ok( + await content.document.hasStorageAccess(), + "example.org should have storage access after request succeeded." + ); + }); + + info("Open a new tab with example.org to set a first party cookie."); + await BrowserTestUtils.withNewTab("https://example.org", async browser => { + info("Set a first party cookie via `document.cookie`"); + await SpecialPowers.spawn(browser, [], async () => { + content.document.cookie = + "foo=bar; Secure; SameSite=None; max-age=3600;"; + }); + + info("Make sure the cookie arrived in the example.org iframe."); + await SpecialPowers.spawn(iframeBC, [], async () => { + is( + content.document.cookie, + "foo=bar", + "example.org should have access to the cookie set in the first party context." + ); + }); + }); + + info( + "The first party tab has been closed. Make sure the cookie is still available in the iframe." + ); + await SpecialPowers.spawn(iframeBC, [], async () => { + is( + content.document.cookie, + "foo=bar", + "example.org should have access to the cookie set in the first party context." + ); + }); + }); + + info("Cleanup."); + Services.perms.removeAll(); + Services.cookies.removeAll(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_userActivation.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_userActivation.js new file mode 100644 index 0000000000..1fdc021e28 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_userActivation.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +// Callback function to check if the user activation remained active +async function requestStorageAccessAndExpectUserActivationActive() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + let p = document.requestStorageAccess(); + try { + await p; + ok(true, "gain storage access."); + } catch { + ok(false, "denied storage access."); + } + + is( + SpecialPowers.wrap(document).hasBeenUserGestureActivated, + true, + `check has-been-user-activated on the top-level-document` + ); + is( + SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, + true, + `check has-valid-transient-user-activation on the top-level-document` + ); + + SpecialPowers.wrap(document).clearUserGestureActivation(); +} + +// Callback function to check if the user activation was consumed +async function requestStorageAccessAndExpectUserActivationConsumed() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + const lastUserGesture = SpecialPowers.wrap(document).lastUserGestureTimeStamp; + + let p = document.requestStorageAccess(); + try { + await p; + ok(true, "gain storage access."); + } catch { + ok(false, "denied storage access."); + } + + is( + SpecialPowers.wrap(document).hasBeenUserGestureActivated, + true, + `check has-been-user-activated on the top-level-document` + ); + is( + SpecialPowers.wrap(document).lastUserGestureTimeStamp, + lastUserGesture, + `check has-valid-transient-user-activation on the top-level-document` + ); + + SpecialPowers.wrap(document).clearUserGestureActivation(); +} + +add_task(async function testActiveEvent() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_HTTPS, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectUserActivationActive + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testConsumedEvent() { + await setPreferences(); + SpecialPowers.Services.prefs.setIntPref( + "dom.user_activation.transient.timeout", + 1000 + ); + + const timeout = + SpecialPowers.getIntPref("dom.user_activation.transient.timeout") + 1000; + // Timeout to consume transient activation + await openPageAndRunCode( + TEST_TOP_PAGE_HTTPS, + getExpectPopupAndClickAfterDelay("accept", timeout), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectUserActivationConsumed + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_subResources.js b/toolkit/components/antitracking/test/browser/browser_subResources.js new file mode 100644 index 0000000000..4841527d19 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_subResources.js @@ -0,0 +1,277 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ["privacy.partition.network_state", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts and tracking images"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for images"); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for scripts"); + }); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_WO, + blockingCallback: (async _ => {}).toString(), + nonBlockingCallback: (async _ => {}).toString(), + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Loading tracking scripts and tracking images again"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "1", "One cookie received for images."); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + is(text, "1", "One cookie received received for scripts."); + }); + + let expectTrackerBlocked = (item, blocked) => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + "Correct blocking type reported" + ); + is(item[1], blocked, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectTrackerFound = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectCookiesLoaded = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectTrackerCookiesLoaded = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let log = JSON.parse(await browser.getContentBlockingLog()); + for (let trackerOrigin in log) { + is( + trackerOrigin + "/", + TEST_3RD_PARTY_DOMAIN, + "Correct tracker origin must be reported" + ); + let originLog = log[trackerOrigin]; + is(originLog.length, 5, "We should have 4 entries in the compressed log"); + expectTrackerFound(originLog[0]); + expectCookiesLoaded(originLog[1]); + expectTrackerCookiesLoaded(originLog[2]); + expectTrackerBlocked(originLog[3], true); + expectTrackerBlocked(originLog[4], false); + } + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned.js b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned.js new file mode 100644 index 0000000000..b2de150075 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned.js @@ -0,0 +1,308 @@ +async function runTests(topPage, limitForeignContexts) { + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, topPage); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading scripts and images"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received for images."); + } else { + is(text, "1", "One cookie received for images."); + } + }); + + await fetch( + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received received for scripts."); + } else { + is(text, "1", "One cookie received received for scripts."); + } + }); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_WO, + blockingCallback: (async _ => {}).toString(), + nonBlockingCallback: (async _ => {}).toString(), + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Loading scripts and images again"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received for images."); + } else { + is(text, "1", "One cookie received for images."); + } + }); + + await fetch( + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received received for scripts."); + } else { + is(text, "1", "One cookie received received for scripts."); + } + }); + + let expectTrackerBlocked = (item, blocked) => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + "Correct blocking type reported" + ); + is(item[1], blocked, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectCookiesLoaded = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectCookiesBlockedForeign = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let log = JSON.parse(await browser.getContentBlockingLog()); + for (let trackerOrigin in log) { + let originLog = log[trackerOrigin]; + info(trackerOrigin); + switch (trackerOrigin) { + case "https://example.org": + case "https://example.com": + let numEntries = 1; + if (limitForeignContexts) { + ++numEntries; + } + is( + originLog.length, + numEntries, + `We should have ${numEntries} entries in the compressed log` + ); + expectCookiesLoaded(originLog[0]); + if (limitForeignContexts) { + expectCookiesBlockedForeign(originLog[1]); + } + break; + case "https://tracking.example.org": + is( + originLog.length, + 1, + "We should have 1 entries in the compressed log" + ); + expectTrackerBlocked(originLog[0], false); + break; + } + } + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +} + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], + }); + + for (let limitForeignContexts of [false, true]) { + SpecialPowers.setBoolPref( + "privacy.dynamic_firstparty.limitForeign", + limitForeignContexts + ); + for (let page of [TEST_TOP_PAGE, TEST_TOP_PAGE_2, TEST_TOP_PAGE_3]) { + await runTests(page, limitForeignContexts); + } + } + + SpecialPowers.clearUserPref("privacy.dynamic_firstparty.limitForeign"); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned_alwaysPartition.js new file mode 100644 index 0000000000..53a90854b3 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned_alwaysPartition.js @@ -0,0 +1,313 @@ +async function runTests(topPage, limitForeignContexts) { + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, topPage); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading scripts and images"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received for images."); + } else { + is(text, "1", "One cookie received for images."); + } + }); + + await fetch( + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received received for scripts."); + } else { + is(text, "1", "One cookie received received for scripts."); + } + }); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_WO, + blockingCallback: (async _ => {}).toString(), + nonBlockingCallback: (async _ => {}).toString(), + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Loading scripts and images again"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received for images."); + } else { + is(text, "1", "One cookie received for images."); + } + }); + + await fetch( + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received received for scripts."); + } else { + is(text, "1", "One cookie received received for scripts."); + } + }); + + let expectTrackerBlocked = (item, blocked, type) => { + is(item[0], type, "Correct blocking type reported"); + is(item[1], blocked, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectCookiesLoaded = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectCookiesBlockedForeign = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let log = JSON.parse(await browser.getContentBlockingLog()); + for (let trackerOrigin in log) { + let originLog = log[trackerOrigin]; + info(trackerOrigin); + switch (trackerOrigin) { + case "https://example.org": + case "https://example.com": + let numEntries = 1; + if (limitForeignContexts) { + ++numEntries; + } + is( + originLog.length, + numEntries, + `We should have ${numEntries} entries in the compressed log` + ); + expectCookiesLoaded(originLog[0]); + if (limitForeignContexts) { + expectCookiesBlockedForeign(originLog[1]); + } + break; + case "https://tracking.example.org": + is( + originLog.length, + 2, + "We should have 2 entries in the compressed log" + ); + expectTrackerBlocked( + originLog[0], + true, + Ci.nsIWebProgressListener.STATE_COOKIES_LOADED + ); + expectTrackerBlocked( + originLog[1], + false, + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER + ); + break; + } + } + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +} + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + true, + ], + ], + }); + + for (let limitForeignContexts of [false, true]) { + SpecialPowers.setBoolPref( + "privacy.dynamic_firstparty.limitForeign", + limitForeignContexts + ); + for (let page of [TEST_TOP_PAGE, TEST_TOP_PAGE_2, TEST_TOP_PAGE_3]) { + await runTests(page, limitForeignContexts); + } + } + + SpecialPowers.clearUserPref("privacy.dynamic_firstparty.limitForeign"); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js b/toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js new file mode 100644 index 0000000000..f609c2d5b0 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js @@ -0,0 +1,55 @@ +// This test works by setting up an exception for the tracker domain, which +// disables all the anti-tracking tests. + +add_task(async _ => { + PermissionTestUtils.add( + "http://example.net", + "cookie", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +AntiTracking._createTask({ + name: "Test that we don't store 3P cookies from non-anonymous CORS XHR", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + blockingByContentBlockingRTUI: false, + allowList: false, + thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html", + callback: async _ => { + await new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open( + "GET", + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/cookiesCORS.sjs?some;max-age=999999", + true + ); + xhr.withCredentials = true; + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.onreadystatechange = _ => { + if (4 === xhr.readyState && 200 === xhr.status) { + resolve(); + } + }; + xhr.send(); + }); + }, + extraPrefs: [], + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping.js b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping.js new file mode 100644 index 0000000000..642b5d2cbd --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping.js @@ -0,0 +1,253 @@ +// This test ensures that the URL decoration annotations service works as +// expected, and also we successfully downgrade document.referrer to the +// eTLD+1 URL when tracking identifiers controlled by this service are +// present in the referrer URI. + +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +const COLLECTION_NAME = "anti-tracking-url-decoration"; +const PREF_NAME = "privacy.restrict3rdpartystorage.url_decorations"; +const TOKEN_1 = "fooBar"; +const TOKEN_2 = "foobaz"; +const TOKEN_3 = "fooqux"; +const TOKEN_4 = "bazqux"; + +const token_1 = TOKEN_1.toLowerCase(); + +const DOMAIN = TEST_DOMAIN_3; +const SUB_DOMAIN = "https://sub1.xn--hxajbheg2az3al.xn--jxalpdlp/"; +const TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER = + SUB_DOMAIN + TEST_PATH + "page.html"; +const TOP_PAGE_WITH_TRACKING_IDENTIFIER = + TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER + "?" + TOKEN_1 + "=123"; + +add_task(async _ => { + let uds = Cc["@mozilla.org/tracking-url-decoration-service;1"].getService( + Ci.nsIURLDecorationAnnotationsService + ); + + let records = [ + { + id: "1", + last_modified: 1000000000000001, + schema: Date.now(), + token: TOKEN_1, + }, + ]; + + // Add some initial data + async function emitSync() { + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { current: records }, + }); + } + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), [records[0]]); + await emitSync(); + + await uds.ensureUpdated(); + + let list = Preferences.get(PREF_NAME).split(" "); + ok(list.includes(TOKEN_1), "Token must now be available in " + PREF_NAME); + ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked"); + + async function verifyList(array, not_array) { + await emitSync(); + + await uds.ensureUpdated(); + + list = Preferences.get(PREF_NAME).split(" "); + for (let token of array) { + ok( + list.includes(token), + token + " must now be available in " + PREF_NAME + ); + } + for (let token of not_array) { + ok( + !list.includes(token), + token + " must not be available in " + PREF_NAME + ); + } + ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked"); + } + + records.push( + { + id: "2", + last_modified: 1000000000000002, + schema: Date.now(), + token: TOKEN_2, + }, + { + id: "3", + last_modified: 1000000000000003, + schema: Date.now(), + token: TOKEN_3, + }, + { + id: "4", + last_modified: 1000000000000005, + schema: Date.now(), + token: TOKEN_4, + } + ); + + await verifyList([TOKEN_1, TOKEN_2, TOKEN_3, TOKEN_4], []); + + records.pop(); + + await verifyList([TOKEN_1, TOKEN_2, TOKEN_3], [TOKEN_4]); + + is( + Services.eTLD.getBaseDomain(Services.io.newURI(DOMAIN)), + Services.eTLD.getBaseDomain(Services.io.newURI(SUB_DOMAIN)), + "Sanity check" + ); + + registerCleanupFunction(async _ => { + records = []; + await db.clear(); + await emitSync(); + }); +}); + +AntiTracking._createTask({ + name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + ok(ref.pathname.length > 1, "Path must not be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy. + ["network.http.referer.defaultPolicy.trackers", 3], + [APS_PREF, false], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier even though it gets downgraded to origin only due to the default referrer policy", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy.trackers", 2], + [APS_PREF, false], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we downgrade document.referrer when it contains a tracking identifier", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname should be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy. + ["network.http.referer.defaultPolicy.trackers", 3], + [APS_PREF, false], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we don't downgrade document.referrer when it contains a tracking identifier if it gets downgraded to origin only due to the default referrer policy because the tracking identifier wouldn't be present in the referrer any more", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy.trackers", 2], + [APS_PREF, false], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping_alwaysPartition.js new file mode 100644 index 0000000000..2fbac9811b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping_alwaysPartition.js @@ -0,0 +1,255 @@ +// This test ensures that the URL decoration annotations service works as +// expected, and also we successfully downgrade document.referrer to the +// eTLD+1 URL when tracking identifiers controlled by this service are +// present in the referrer URI. + +"use strict"; + +const trackerBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +const COLLECTION_NAME = "anti-tracking-url-decoration"; +const PREF_NAME = "privacy.restrict3rdpartystorage.url_decorations"; +const TOKEN_1 = "fooBar"; +const TOKEN_2 = "foobaz"; +const TOKEN_3 = "fooqux"; +const TOKEN_4 = "bazqux"; + +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +const token_1 = TOKEN_1.toLowerCase(); + +const DOMAIN = TEST_DOMAIN_3; +const SUB_DOMAIN = "https://sub1.xn--hxajbheg2az3al.xn--jxalpdlp/"; +const TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER = + SUB_DOMAIN + TEST_PATH + "page.html"; +const TOP_PAGE_WITH_TRACKING_IDENTIFIER = + TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER + "?" + TOKEN_1 + "=123"; + +add_task(async _ => { + let uds = Cc["@mozilla.org/tracking-url-decoration-service;1"].getService( + Ci.nsIURLDecorationAnnotationsService + ); + + let records = [ + { + id: "1", + last_modified: 1000000000000001, + schema: Date.now(), + token: TOKEN_1, + }, + ]; + + // Add some initial data + async function emitSync() { + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { current: records }, + }); + } + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), [records[0]]); + await emitSync(); + + await uds.ensureUpdated(); + + let list = Preferences.get(PREF_NAME).split(" "); + ok(list.includes(TOKEN_1), "Token must now be available in " + PREF_NAME); + ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked"); + + async function verifyList(array, not_array) { + await emitSync(); + + await uds.ensureUpdated(); + + list = Preferences.get(PREF_NAME).split(" "); + for (let token of array) { + ok( + list.includes(token), + token + " must now be available in " + PREF_NAME + ); + } + for (let token of not_array) { + ok( + !list.includes(token), + token + " must not be available in " + PREF_NAME + ); + } + ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked"); + } + + records.push( + { + id: "2", + last_modified: 1000000000000002, + schema: Date.now(), + token: TOKEN_2, + }, + { + id: "3", + last_modified: 1000000000000003, + schema: Date.now(), + token: TOKEN_3, + }, + { + id: "4", + last_modified: 1000000000000005, + schema: Date.now(), + token: TOKEN_4, + } + ); + + await verifyList([TOKEN_1, TOKEN_2, TOKEN_3, TOKEN_4], []); + + records.pop(); + + await verifyList([TOKEN_1, TOKEN_2, TOKEN_3], [TOKEN_4]); + + is( + Services.eTLD.getBaseDomain(Services.io.newURI(DOMAIN)), + Services.eTLD.getBaseDomain(Services.io.newURI(SUB_DOMAIN)), + "Sanity check" + ); + + registerCleanupFunction(async _ => { + records = []; + await db.clear(); + await emitSync(); + }); +}); + +AntiTracking._createTask({ + name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + ok(ref.pathname.length > 1, "Path must not be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy. + ["network.http.referer.defaultPolicy.trackers", 3], + [APS_PREF, true], + ], + expectedBlockingNotifications: trackerBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier even though it gets downgraded to origin only due to the default referrer policy", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy.trackers", 2], + [APS_PREF, true], + ], + expectedBlockingNotifications: trackerBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we downgrade document.referrer when it contains a tracking identifier", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname should be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy. + ["network.http.referer.defaultPolicy.trackers", 3], + [APS_PREF, true], + ], + expectedBlockingNotifications: trackerBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we don't downgrade document.referrer when it contains a tracking identifier if it gets downgraded to origin only due to the default referrer policy because the tracking identifier wouldn't be present in the referrer any more", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy.trackers", 2], + [APS_PREF, true], + ], + expectedBlockingNotifications: trackerBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping.js new file mode 100644 index 0000000000..6395110f41 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping.js @@ -0,0 +1,855 @@ +/* vim: set ts=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/. */ + +"use strict"; + +requestLongerTimeout(6); + +const TEST_THIRD_PARTY_DOMAIN = TEST_DOMAIN_2; +const TEST_THIRD_PARTY_SUB_DOMAIN = "http://sub1.xn--exmple-cua.test/"; + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_THIRD_PARTY_URI = + TEST_THIRD_PARTY_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_REDIRECT_URI = TEST_DOMAIN + TEST_PATH + "redirect.sjs"; + +const TEST_CASES = [ + { testQueryString: "paramToStrip1=123", strippedQueryString: "" }, + { + testQueryString: "PARAMTOSTRIP1=123¶mToStrip2=456", + strippedQueryString: "", + }, + { + testQueryString: "paramToStrip1=123¶mToKeep=456", + strippedQueryString: "paramToKeep=456", + }, + { + testQueryString: "paramToStrip1=123¶mToKeep=456¶mToStrip2=abc", + strippedQueryString: "paramToKeep=456", + }, + { + testQueryString: "paramToKeep=123", + strippedQueryString: "paramToKeep=123", + }, + // Test to make sure we don't encode the unstripped parameters. + { + testQueryString: "paramToStrip1=123¶mToKeep=?$!%", + strippedQueryString: "paramToKeep=?$!%", + }, +]; + +let listService; + +function observeChannel(uri, expected) { + return TestUtils.topicObserved("http-on-modify-request", (subject, data) => { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + let channelURI = channel.URI; + + if (channelURI.spec.startsWith(uri)) { + is( + channelURI.query, + expected, + "The loading channel has the expected query string." + ); + return true; + } + + return false; + }); +} + +async function verifyQueryString(browser, expected) { + await SpecialPowers.spawn(browser, [expected], expected => { + // Strip the first question mark. + let search = content.location.search.slice(1); + + is(search, expected, "The query string is correct."); + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_list", "paramToStrip1 paramToStrip2"], + ["privacy.query_stripping.redirect", true], + ["privacy.query_stripping.listService.logLevel", "Debug"], + ["privacy.query_stripping.strip_on_share.enabled", false], + ], + }); + + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + // Here we don't care about the actual enabled state, we just want any init to be done so we get reliable starting conditions. + await listService.testWaitForInit(); +}); + +async function waitForListServiceInit(strippingEnabled) { + info("Waiting for nsIURLQueryStrippingListService to be initialized."); + let isInitialized = await listService.testWaitForInit(); + is( + isInitialized, + strippingEnabled, + "nsIURLQueryStrippingListService should be initialized when the feature is enabled." + ); +} + +add_task(async function doTestsForTabOpen() { + info("Start testing query stripping for tab open."); + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testURI = TEST_URI + "?" + test.testQueryString; + + let expected = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + // Observe the channel and check if the query string is expected. + let networkPromise = observeChannel(TEST_URI, expected); + + // Open a new tab. + await BrowserTestUtils.withNewTab(testURI, async browser => { + // Verify if the query string is expected in the new tab. + await verifyQueryString(browser, expected); + }); + + await networkPromise; + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForWindowOpen() { + info("Start testing query stripping for window.open()."); + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString; + + let originalQueryString = test.testQueryString; + let expectedQueryString = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is intact when open + // a same-origin URI. + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + // Create the promise to wait for the opened tab. + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith(TEST_URI); + }); + + // Call window.open() to open the same-origin URI where the query string + // won't be stripped. + await SpecialPowers.spawn(browser, [testFirstPartyURI], async url => { + content.postMessage({ type: "window-open", url }, "*"); + }); + + await networkPromise; + let newTab = await newTabPromise; + + // Verify if the query string is expected in the new opened tab. + await verifyQueryString(newTab.linkedBrowser, originalQueryString); + + BrowserTestUtils.removeTab(newTab); + + // Observe the channel and check if the query string is expected for + // cross-origin URI. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith(TEST_THIRD_PARTY_URI); + }); + + // Call window.open() to open the cross-site URI where the query string + // could be stripped. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "window-open", url }, "*"); + }); + + await networkPromise; + newTab = await newTabPromise; + + // Verify if the query string is expected in the new opened tab. + await verifyQueryString(newTab.linkedBrowser, expectedQueryString); + + BrowserTestUtils.removeTab(newTab); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForLinkClick() { + info("Start testing query stripping for link navigation."); + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString; + + let originalQueryString = test.testQueryString; + let expectedQueryString = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is intact when + // click a same-origin link. + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testFirstPartyURI + ); + + // Create a link and click it to navigate. + await SpecialPowers.spawn(browser, [testFirstPartyURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, originalQueryString); + + // Second, create a link to a cross-origin site to see if the query + // string is stripped as expected. + + // Observe the channel and check if the query string is expected when + // click a cross-origin link. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + let targetURI = expectedQueryString + ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}` + : TEST_THIRD_PARTY_URI; + // Create the promise to wait for the location change. + locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + targetURI + ); + + // Create a cross-origin link and click it to navigate. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, expectedQueryString); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForLinkClickInIframe() { + info("Start testing query stripping for link navigation in iframe."); + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString; + + let originalQueryString = test.testQueryString; + let expectedQueryString = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create an iframe and wait until it has been loaded. + let iframeBC = await SpecialPowers.spawn( + browser, + [TEST_URI], + async url => { + let frame = content.document.createElement("iframe"); + content.document.body.appendChild(frame); + + await new Promise(done => { + frame.addEventListener( + "load", + function () { + done(); + }, + { capture: true, once: true } + ); + + frame.setAttribute("src", url); + }); + + return frame.browsingContext; + } + ); + + // Observe the channel and check if the query string is intact when + // click a same-origin link. + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + // Create the promise to wait for the new tab. + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + testFirstPartyURI + ); + + // Create a same-site link which has '_blank' as target in the iframe + // and click it to navigate. + await SpecialPowers.spawn(iframeBC, [testFirstPartyURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.setAttribute("target", "_blank"); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + let newOpenedTab = await newTabPromise; + + // Verify the query string in the content window. + await verifyQueryString( + newOpenedTab.linkedBrowser, + originalQueryString + ); + BrowserTestUtils.removeTab(newOpenedTab); + + // Second, create a link to a cross-origin site in the iframe to see if + // the query string is stripped as expected. + + // Observe the channel and check if the query string is expected when + // click a cross-origin link. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + let targetURI = expectedQueryString + ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}` + : TEST_THIRD_PARTY_URI; + // Create the promise to wait for the new tab. + newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, targetURI); + + // Create a cross-origin link which has '_blank' as target in the iframe + // and click it to navigate. + await SpecialPowers.spawn(iframeBC, [testThirdPartyURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.setAttribute("target", "_blank"); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + newOpenedTab = await newTabPromise; + + // Verify the query string in the content window. + await verifyQueryString( + newOpenedTab.linkedBrowser, + expectedQueryString + ); + BrowserTestUtils.removeTab(newOpenedTab); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForScriptNavigation() { + info("Start testing query stripping for script navigation."); + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString; + + let originalQueryString = test.testQueryString; + let expectedQueryString = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is intact when + // navigating to a same-origin URI via script. + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testFirstPartyURI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testFirstPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, originalQueryString); + + // Second, trigger a cross-origin navigation through script to see if + // the query string is stripped as expected. + + let targetURI = expectedQueryString + ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}` + : TEST_THIRD_PARTY_URI; + + // Observe the channel and check if the query string is expected. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + targetURI + ); + + // Trigger the cross-origin navigation by script. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, expectedQueryString); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForNoStrippingForIframeNavigation() { + info("Start testing no query stripping for iframe navigation."); + + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString; + + // There should be no query stripping for the iframe navigation. + let originalQueryString = test.testQueryString; + let expectedQueryString = test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create an iframe and wait until it has been loaded. + let iframeBC = await SpecialPowers.spawn( + browser, + [TEST_URI], + async url => { + let frame = content.document.createElement("iframe"); + content.document.body.appendChild(frame); + + await new Promise(done => { + frame.addEventListener( + "load", + function () { + done(); + }, + { capture: true, once: true } + ); + + frame.setAttribute("src", url); + }); + + return frame.browsingContext; + } + ); + + // Observe the channel and check if the query string is intact when + // navigating an iframe. + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testFirstPartyURI + ); + + // Trigger the iframe navigation by script. + await SpecialPowers.spawn(iframeBC, [testFirstPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the iframe. + await verifyQueryString(iframeBC, originalQueryString); + + // Second, trigger a cross-origin navigation through script to see if + // the query string is still the same. + + let targetURI = expectedQueryString + ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}` + : TEST_THIRD_PARTY_URI; + + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + targetURI + ); + + // Trigger the cross-origin iframe navigation by script. + await SpecialPowers.spawn(iframeBC, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(iframeBC, expectedQueryString); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForRedirect() { + info("Start testing query stripping for redirects."); + + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = + TEST_REDIRECT_URI + "?" + TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?${test.testQueryString}`; + + let originalQueryString = test.testQueryString; + let expectedQueryString = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is intact when + // redirecting to a same-origin URI . + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + let targetURI = `${TEST_URI}?${originalQueryString}`; + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + targetURI + ); + + // Trigger the redirect. + await SpecialPowers.spawn(browser, [testFirstPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, originalQueryString); + + // Second, trigger a redirect to a cross-origin site where the query + // string should be stripped. + + targetURI = expectedQueryString + ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}` + : TEST_THIRD_PARTY_URI; + + // Observe the channel and check if the query string is expected. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + targetURI + ); + + // Trigger the cross-origin redirect. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, expectedQueryString); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestForAllowList() { + info("Start testing query stripping allow list."); + + // Enable the query stripping and set the allow list. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.enabled", true], + ["privacy.query_stripping.allow_list", "xn--exmple-cua.test"], + ], + }); + await waitForListServiceInit(true); + + const expected = "paramToStrip1=123"; + + // Make sure the allow list works for sites, so we will test both the domain + // and the sub domain. + for (const domain of [TEST_THIRD_PARTY_DOMAIN, TEST_THIRD_PARTY_SUB_DOMAIN]) { + let testURI = `${domain}${TEST_PATH}file_stripping.html`; + let testURIWithQueryString = `${testURI}?${expected}`; + + // 1. Test the allow list for tab open. + info("Run tab open test."); + + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + await BrowserTestUtils.withNewTab(testURIWithQueryString, async browser => { + // Verify if the query string is not stripped in the new tab. + await verifyQueryString(browser, expected); + }); + + await networkPromise; + + // 2. Test the allow list for window open + info("Run window open test."); + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith(testURI); + }); + + await SpecialPowers.spawn( + browser, + [testURIWithQueryString], + async url => { + content.postMessage({ type: "window-open", url }, "*"); + } + ); + + await networkPromise; + let newTab = await newTabPromise; + + // Verify if the query string is not stripped in the new opened tab. + await verifyQueryString(newTab.linkedBrowser, expected); + + BrowserTestUtils.removeTab(newTab); + }); + + // 3. Test the allow list for link click + info("Run link click test."); + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testURIWithQueryString + ); + + await SpecialPowers.spawn( + browser, + [testURIWithQueryString], + async url => { + let link = content.document.createElement("a"); + link.setAttribute("href", url); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + } + ); + + await networkPromise; + await locationChangePromise; + + // Verify the query string is not stripped in the content window. + await verifyQueryString(browser, expected); + }); + + // 4. Test the allow list for clicking link in an iframe. + info("Run link click in iframe test."); + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create an iframe and wait until it has been loaded. + let iframeBC = await SpecialPowers.spawn( + browser, + [TEST_URI], + async url => { + let frame = content.document.createElement("iframe"); + content.document.body.appendChild(frame); + + await new Promise(done => { + frame.addEventListener( + "load", + function () { + done(); + }, + { capture: true, once: true } + ); + + frame.setAttribute("src", url); + }); + + return frame.browsingContext; + } + ); + + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + // Create the promise to wait for the new tab. + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + testURIWithQueryString + ); + + // Create a same-site link which has '_blank' as target in the iframe + // and click it to navigate. + await SpecialPowers.spawn( + iframeBC, + [testURIWithQueryString], + async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.setAttribute("target", "_blank"); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + } + ); + + await networkPromise; + let newOpenedTab = await newTabPromise; + + // Verify the query string is not stripped in the content window. + await verifyQueryString(newOpenedTab.linkedBrowser, expected); + BrowserTestUtils.removeTab(newOpenedTab); + + // 5. Test the allow list for script navigation. + info("Run script navigation test."); + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testURIWithQueryString + ); + + await SpecialPowers.spawn( + browser, + [testURIWithQueryString], + async url => { + content.postMessage({ type: "script", url }, "*"); + } + ); + + await networkPromise; + await locationChangePromise; + + // Verify the query string is not stripped in the content window. + await verifyQueryString(browser, expected); + }); + + // 6. Test the allow list for redirect. + info("Run redirect test."); + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testURIWithQueryString + ); + + let testRedirectURI = `${TEST_REDIRECT_URI}?${testURI}?${expected}`; + + // Trigger the redirect. + await SpecialPowers.spawn(browser, [testRedirectURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, expected); + }); + }); + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_allowList.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_allowList.js new file mode 100644 index 0000000000..6dee6cede0 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_allowList.js @@ -0,0 +1,442 @@ +/* vim: set ts=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/. */ + +"use strict"; + +const TEST_THIRD_PARTY_DOMAIN = TEST_DOMAIN_2; + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_THIRD_PARTY_URI = + TEST_THIRD_PARTY_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_REDIRECT_URI = TEST_DOMAIN + TEST_PATH + "redirect.sjs"; + +const TEST_QUERY_STRING = "paramToStrip=1"; + +function observeChannel(uri, expected) { + return TestUtils.topicObserved("http-on-before-connect", (subject, data) => { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + let channelURI = channel.URI; + + if (channelURI.spec.startsWith(uri)) { + is( + channelURI.query, + expected, + "The loading channel has the expected query string." + ); + return true; + } + + return false; + }); +} + +async function verifyQueryString(browser, expected) { + await SpecialPowers.spawn(browser, [expected], expected => { + // Strip the first question mark. + let search = content.location.search.slice(1); + + is(search, expected, "The query string is correct."); + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_list", "paramToStrip"], + ["privacy.query_stripping.redirect", true], + ["privacy.query_stripping.enabled", true], + ], + }); + + let listService = Cc[ + "@mozilla.org/query-stripping-list-service;1" + ].getService(Ci.nsIURLQueryStrippingListService); + await listService.testWaitForInit(); +}); + +add_task(async function doTestsForTabOpen() { + let testURI = TEST_URI + "?" + TEST_QUERY_STRING; + + // Observe the channel and check if the query string is stripped. + let networkPromise = observeChannel(TEST_URI, ""); + + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURI); + + // Verify if the query string is stripped. + await verifyQueryString(tab.linkedBrowser, ""); + await networkPromise; + + // Toggle ETP off and verify if the query string is restored. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + testURI + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_URI, TEST_QUERY_STRING); + + gProtectionsHandler.disableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + await verifyQueryString(tab.linkedBrowser, TEST_QUERY_STRING); + + BrowserTestUtils.removeTab(tab); + + // Open the tab again and check if the query string is not stripped. + networkPromise = observeChannel(TEST_URI, TEST_QUERY_STRING); + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURI); + await networkPromise; + + // Verify if the query string is not stripped because it's in the content + // blocking allow list. + await verifyQueryString(tab.linkedBrowser, TEST_QUERY_STRING); + + // Toggle ETP on and verify if the query string is stripped again. + networkPromise = observeChannel(TEST_URI, ""); + browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + TEST_URI + ); + gProtectionsHandler.enableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + await verifyQueryString(tab.linkedBrowser, ""); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function doTestsForWindowOpen() { + let testURI = TEST_THIRD_PARTY_URI + "?" + TEST_QUERY_STRING; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is stripped. + let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + // Create the promise to wait for the opened tab. + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith(TEST_THIRD_PARTY_URI); + }); + + // Call window.open() to open the third-party URI. + await SpecialPowers.spawn(browser, [testURI], async url => { + content.postMessage({ type: "window-open", url }, "*"); + }); + + await networkPromise; + let newTab = await newTabPromise; + + // Verify if the query string is stripped in the new opened tab. + await verifyQueryString(newTab.linkedBrowser, ""); + + // Toggle ETP off and verify if the query string is restored. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + newTab.linkedBrowser, + false, + testURI + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + + gProtectionsHandler.disableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + await verifyQueryString(newTab.linkedBrowser, TEST_QUERY_STRING); + + BrowserTestUtils.removeTab(newTab); + + // Call window.open() again to check if the query string is not stripped if + // it's in the content blocking allow list. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith(TEST_THIRD_PARTY_URI); + }); + + await SpecialPowers.spawn(browser, [testURI], async url => { + content.postMessage({ type: "window-open", url }, "*"); + }); + + await networkPromise; + newTab = await newTabPromise; + + // Verify if the query string is not stripped in the new opened tab. + await verifyQueryString(newTab.linkedBrowser, TEST_QUERY_STRING); + + // Toggle ETP on and verify if the query string is stripped again. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + browserLoadedPromise = BrowserTestUtils.browserLoaded( + newTab.linkedBrowser, + false, + TEST_THIRD_PARTY_URI + ); + gProtectionsHandler.enableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + await verifyQueryString(newTab.linkedBrowser, ""); + BrowserTestUtils.removeTab(newTab); + }); +}); + +add_task(async function doTestsForLinkClick() { + let testURI = TEST_THIRD_PARTY_URI + "?" + TEST_QUERY_STRING; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is stripped. + let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_THIRD_PARTY_URI + ); + + // Create a link and click it to navigate. + await SpecialPowers.spawn(browser, [testURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, ""); + + // Toggle ETP off and verify if the query string is restored. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + testURI + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + + gProtectionsHandler.disableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, TEST_QUERY_STRING); + }); + + // Repeat the test again to see if the query string is not stripped if it's in + // the content blocking allow list. + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + TEST_QUERY_STRING + ); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testURI + ); + + // Create a link and click it to navigate. + await SpecialPowers.spawn(browser, [testURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, TEST_QUERY_STRING); + + // Toggle ETP on and verify if the query string is stripped again. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + TEST_THIRD_PARTY_URI + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + gProtectionsHandler.enableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, ""); + }); +}); + +add_task(async function doTestsForScriptNavigation() { + let testURI = TEST_THIRD_PARTY_URI + "?" + TEST_QUERY_STRING; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is stripped. + let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_THIRD_PARTY_URI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, ""); + + // Toggle ETP off and verify if the query string is restored. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + testURI + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + + gProtectionsHandler.disableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, TEST_QUERY_STRING); + }); + + // Repeat the test again to see if the query string is not stripped if it's in + // the content blocking allow list. + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + TEST_QUERY_STRING + ); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testURI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, TEST_QUERY_STRING); + + // Toggle ETP on and verify if the query string is stripped again. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + TEST_THIRD_PARTY_URI + ); + // Observe the channel and check if the query string is stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + gProtectionsHandler.enableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, ""); + }); +}); + +add_task(async function doTestsForRedirect() { + let testURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?${TEST_QUERY_STRING}`; + let resultURI = TEST_THIRD_PARTY_URI; + let resultURIWithQuery = `${TEST_THIRD_PARTY_URI}?${TEST_QUERY_STRING}`; + + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI); + + // Observe the channel and check if the query string is stripped. + let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + resultURI + ); + + // Trigger the redirect. + await SpecialPowers.spawn(tab.linkedBrowser, [testURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(tab.linkedBrowser, ""); + + // Toggle ETP off and verify if the query string is restored. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + resultURIWithQuery + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + + gProtectionsHandler.disableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + BrowserTestUtils.removeTab(tab); + + // Open the tab again to check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI); + + locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + resultURIWithQuery + ); + + // Trigger the redirect. + await SpecialPowers.spawn(tab.linkedBrowser, [testURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(tab.linkedBrowser, TEST_QUERY_STRING); + + // Toggle ETP on and verify if the query string is stripped again. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + resultURI + ); + gProtectionsHandler.enableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + await verifyQueryString(tab.linkedBrowser, ""); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_nimbus.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_nimbus.js new file mode 100644 index 0000000000..e5f256b870 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_nimbus.js @@ -0,0 +1,145 @@ +/* vim: set ts=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/. */ + +"use strict"; + +/** + * Simplified version of browser_urlQueryStripping.js to test that the Nimbus + * integration works correctly in both normal and private browsing. + */ + +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_QUERY_STRING = "paramToStrip1=123¶mToKeep=456"; +const TEST_QUERY_STRING_STRIPPED = "paramToKeep=456"; +const TEST_URI_WITH_QUERY = TEST_URI + "?" + TEST_QUERY_STRING; + +let listService; + +async function waitForListServiceInit(strippingEnabled) { + info("Waiting for nsIURLQueryStrippingListService to be initialized."); + let isInitialized = await listService.testWaitForInit(); + is( + isInitialized, + strippingEnabled, + "nsIURLQueryStrippingListService should be initialized when the feature is enabled." + ); +} + +/** + * Set a list of prefs on the default branch and restore the original values on test end. + * @param {*} prefs - Key value pairs in an array. + */ +function setDefaultPrefs(prefs) { + let originalValues = new Map(); + let defaultPrefs = Services.prefs.getDefaultBranch(""); + + let prefValueToSetter = prefValue => { + let type = typeof prefValue; + if (type == "string") { + return defaultPrefs.setStringPref; + } + if (type == "boolean") { + return defaultPrefs.setBoolPref; + } + throw new Error("unexpected pref type"); + }; + + prefs.forEach(([key, value]) => { + prefValueToSetter(value)(key, value); + originalValues.set(key, value); + }); + + registerCleanupFunction(function () { + prefs.forEach(([key, value]) => { + prefValueToSetter(value)(key, originalValues.get(key)); + }); + }); +} + +add_setup(async function () { + // Disable the feature via the default pref. This is required so we can set + // user values via Nimbus. + setDefaultPrefs([ + ["privacy.query_stripping.enabled", false], + ["privacy.query_stripping.enabled.pbmode", false], + ["privacy.query_stripping.strip_list", ""], + ["privacy.query_stripping.strip_on_share.enabled", false], + ]); + + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.listService.logLevel", "Debug"]], + }); + + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + // Here we don't care about the actual enabled state, we just want any init to be done so we get reliable starting conditions. + await listService.testWaitForInit(); +}); + +add_task(async function test() { + let [normalWindow, pbWindow] = await Promise.all([ + BrowserTestUtils.openNewBrowserWindow(), + BrowserTestUtils.openNewBrowserWindow({ private: true }), + ]); + + for (let enableStripPBM of [false, true]) { + for (let enableStrip of [false, true]) { + let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "queryStripping", + value: { + enabledNormalBrowsing: enableStrip, + enabledPrivateBrowsing: enableStripPBM, + stripList: "paramToStrip1 paramToStrip2", + }, + }); + + for (let testPBM of [false, true]) { + let shouldStrip = + (testPBM && enableStripPBM) || (!testPBM && enableStrip); + let expectedQueryString = shouldStrip + ? TEST_QUERY_STRING_STRIPPED + : TEST_QUERY_STRING; + + info( + "Test stripping " + + JSON.stringify({ + enableStripPBM, + enableStrip, + testPBM, + expectedQueryString, + }) + ); + + await waitForListServiceInit(enableStripPBM || enableStrip); + + let tabBrowser = testPBM ? pbWindow.gBrowser : normalWindow.gBrowser; + await BrowserTestUtils.withNewTab( + { gBrowser: tabBrowser, url: TEST_URI_WITH_QUERY }, + async browser => { + is( + browser.currentURI.query, + expectedQueryString, + "Correct query string" + ); + } + ); + } + + await doExperimentCleanup(); + } + } + + // Cleanup + await Promise.all([ + BrowserTestUtils.closeWindow(normalWindow), + BrowserTestUtils.closeWindow(pbWindow), + ]); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_pbmode.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_pbmode.js new file mode 100644 index 0000000000..fd37f94765 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_pbmode.js @@ -0,0 +1,105 @@ +/* vim: set ts=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/. */ + +"use strict"; + +/** + * Simplified version of browser_urlQueryStripping.js to test that the feature + * prefs work correctly in both normal and private browsing. + */ + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_QUERY_STRING = "paramToStrip1=123¶mToKeep=456"; +const TEST_QUERY_STRING_STRIPPED = "paramToKeep=456"; +const TEST_URI_WITH_QUERY = TEST_URI + "?" + TEST_QUERY_STRING; + +let listService; + +async function waitForListServiceInit(strippingEnabled) { + info("Waiting for nsIURLQueryStrippingListService to be initialized."); + let isInitialized = await listService.testWaitForInit(); + is( + isInitialized, + strippingEnabled, + "nsIURLQueryStrippingListService should be initialized when the feature is enabled." + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_list", "paramToStrip1 paramToStrip2"], + ["privacy.query_stripping.listService.logLevel", "Debug"], + ["privacy.query_stripping.strip_on_share.enabled", false], + ], + }); + + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + // Here we don't care about the actual enabled state, we just want any init to be done so we get reliable starting conditions. + await listService.testWaitForInit(); +}); + +add_task(async function test() { + let [normalWindow, pbWindow] = await Promise.all([ + BrowserTestUtils.openNewBrowserWindow(), + BrowserTestUtils.openNewBrowserWindow({ private: true }), + ]); + + for (let enableStripPBM of [false, true]) { + Services.prefs.setBoolPref( + "privacy.query_stripping.enabled.pbmode", + enableStripPBM + ); + for (let enableStrip of [false, true]) { + Services.prefs.setBoolPref( + "privacy.query_stripping.enabled", + enableStrip + ); + for (let testPBM of [false, true]) { + let shouldStrip = + (testPBM && enableStripPBM) || (!testPBM && enableStrip); + let expectedQueryString = shouldStrip + ? TEST_QUERY_STRING_STRIPPED + : TEST_QUERY_STRING; + + info( + "Test stripping " + + JSON.stringify({ + enableStripPBM, + enableStrip, + testPBM, + expectedQueryString, + }) + ); + + await waitForListServiceInit(enableStripPBM || enableStrip); + + let tabBrowser = testPBM ? pbWindow.gBrowser : normalWindow.gBrowser; + await BrowserTestUtils.withNewTab( + { gBrowser: tabBrowser, url: TEST_URI_WITH_QUERY }, + async browser => { + is( + browser.currentURI.query, + expectedQueryString, + "Correct query string" + ); + } + ); + } + } + } + + // Cleanup + await Promise.all([ + BrowserTestUtils.closeWindow(normalWindow), + BrowserTestUtils.closeWindow(pbWindow), + ]); + + Services.prefs.clearUserPref("privacy.query_stripping.enabled"); + Services.prefs.clearUserPref("privacy.query_stripping.enabled.pbmode"); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry.js new file mode 100644 index 0000000000..20c830113c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry.js @@ -0,0 +1,355 @@ +/** + * Bug 1706616 - Testing the URL query string stripping telemetry. + */ + +"use strict"; + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_THIRD_PARTY_URI = TEST_DOMAIN_2 + TEST_PATH + "file_stripping.html"; +const TEST_REDIRECT_URI = TEST_DOMAIN + TEST_PATH + "redirect.sjs"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const LABEL_NAVIGATION = 0; +const LABEL_REDIRECT = 1; +const LABEL_STRIP_FOR_NAVIGATION = 2; +const LABEL_STRIP_FOR_REDIRECT = 3; + +const QUERY_STRIPPING_COUNT = "QUERY_STRIPPING_COUNT"; +const QUERY_STRIPPING_PARAM_COUNT = "QUERY_STRIPPING_PARAM_COUNT"; + +async function clearTelemetry() { + // There's an arbitrary interval of 2 seconds in which the content + // processes sync their data with the parent process, we wait + // this out to ensure that we clear everything. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry.getHistogramById(QUERY_STRIPPING_COUNT).clear(); + Services.telemetry.getHistogramById(QUERY_STRIPPING_PARAM_COUNT).clear(); + + let isCleared = () => { + let histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).content; + + return ( + !histograms || + (!histograms[QUERY_STRIPPING_COUNT] && + !histograms[QUERY_STRIPPING_PARAM_COUNT]) + ); + }; + + // Check that the telemetry probes have been cleared properly. Do this check + // sync first to avoid any race conditions where telemetry arrives after + // clearing. + if (!isCleared()) { + await TestUtils.waitForCondition( + isCleared, + "waiting for query stripping probes to be cleared" + ); + } + + ok(true, "Telemetry has been cleared."); +} + +async function verifyQueryString(browser, expected) { + await SpecialPowers.spawn(browser, [expected], expected => { + // Strip the first question mark. + let search = content.location.search.slice(1); + + is(search, expected, "The query string is correct."); + }); +} + +async function getTelemetryProbe(key, label, checkCntFn) { + let histogram; + + // Wait until the telemetry probe appears. + await TestUtils.waitForCondition(() => { + let histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).parent; + histogram = histograms[key]; + + let checkRes = false; + + if (histogram) { + checkRes = checkCntFn ? checkCntFn(histogram.values[label]) : true; + } + + return checkRes; + }, `waiting for telemetry probe (key=${key}, label=${label}) to appear`); + + return histogram.values[label]; +} + +async function checkTelemetryProbe(key, expectedCnt, label) { + let cnt = await getTelemetryProbe(key, label, cnt => cnt == expectedCnt); + + is(cnt, expectedCnt, "There should be expected count in telemetry."); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.enabled", true], + [ + "privacy.query_stripping.strip_list", + "paramToStrip paramToStripB paramToStripC paramToStripD", + ], + ], + }); + + // Clear Telemetry probes before testing. + await clearTelemetry(); +}); + +add_task(async function testQueryStrippingNavigationInParent() { + let testURI = TEST_URI + "?paramToStrip=value"; + + // Open a new tab and trigger the query stripping. + await BrowserTestUtils.withNewTab(testURI, async browser => { + // Verify if the query string was happened. + await verifyQueryString(browser, ""); + }); + + // Verify the telemetry probe. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + 1, + LABEL_STRIP_FOR_NAVIGATION + ); + await checkTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, 1, "1"); + + // Because there would be some loading happening during the test and they + // could interfere the count here. So, we only verify if the counter is + // increased, but not the exact count. + let newNavigationCnt = await getTelemetryProbe( + QUERY_STRIPPING_COUNT, + LABEL_NAVIGATION, + cnt => cnt > 0 + ); + ok(newNavigationCnt > 0, "There is navigation count added."); + + await clearTelemetry(); +}); + +add_task(async function testQueryStrippingNavigationInContent() { + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?paramToStrip=value"; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_THIRD_PARTY_URI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await locationChangePromise; + + // Verify if the query string was happened. + await verifyQueryString(browser, ""); + }); + + // Verify the telemetry probe. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + 1, + LABEL_STRIP_FOR_NAVIGATION + ); + await checkTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, 1, "1"); + + // Check if the navigation count is increased. + let newNavigationCnt = await getTelemetryProbe( + QUERY_STRIPPING_COUNT, + LABEL_NAVIGATION, + cnt => cnt > 0 + ); + ok(newNavigationCnt > 0, "There is navigation count added."); + + await clearTelemetry(); +}); + +add_task(async function testQueryStrippingNavigationInContentQueryCount() { + let testThirdPartyURI = + TEST_THIRD_PARTY_URI + + "?paramToStrip=value¶mToStripB=valueB¶mToStripC=valueC¶mToStripD=valueD"; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_THIRD_PARTY_URI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await locationChangePromise; + + // Verify if the query string was happened. + await verifyQueryString(browser, ""); + }); + + // Verify the telemetry probe. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + 1, + LABEL_STRIP_FOR_NAVIGATION + ); + + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "0", cnt => !cnt); + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "1", cnt => !cnt); + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "2", cnt => !cnt); + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "3", cnt => !cnt); + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "4", cnt => cnt == 1); + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "5", cnt => !cnt); + + // Check if the navigation count is increased. + let newNavigationCnt = await getTelemetryProbe( + QUERY_STRIPPING_COUNT, + LABEL_NAVIGATION, + cnt => cnt > 0 + ); + ok(newNavigationCnt > 0, "There is navigation count added."); + + await clearTelemetry(); +}); + +add_task(async function testQueryStrippingRedirect() { + let testThirdPartyURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?paramToStrip=value`; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_THIRD_PARTY_URI + ); + + // Trigger the redirect. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await locationChangePromise; + + // Verify if the query string was happened. + await verifyQueryString(browser, ""); + }); + + // Verify the telemetry probe in parent process. Note that there is no + // non-test loading is using redirect. So, we can check the exact count here. + await checkTelemetryProbe(QUERY_STRIPPING_COUNT, 1, LABEL_STRIP_FOR_REDIRECT); + await checkTelemetryProbe(QUERY_STRIPPING_COUNT, 1, LABEL_REDIRECT); + await checkTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, 1, "1"); + + await clearTelemetry(); +}); + +add_task(async function testQueryStrippingDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", false]], + }); + + // First, test the navigation in parent process. + let testURI = TEST_URI + "?paramToStrip=value"; + + // Open a new tab and trigger the query stripping. + await BrowserTestUtils.withNewTab(testURI, async browser => { + // Verify if the query string was not happened. + await verifyQueryString(browser, "paramToStrip=value"); + }); + + // Verify the telemetry probe. There should be no stripped navigation count. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + undefined, + LABEL_STRIP_FOR_NAVIGATION + ); + // Check if the navigation count is increased. + let newNavigationCnt = await getTelemetryProbe( + QUERY_STRIPPING_COUNT, + LABEL_NAVIGATION, + cnt => cnt > 0 + ); + ok(newNavigationCnt > 0, "There is navigation count added."); + + // Second, test the navigation in content. + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?paramToStrip=value"; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testThirdPartyURI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await locationChangePromise; + + // Verify if the query string was happened. + await verifyQueryString(browser, "paramToStrip=value"); + }); + + // Verify the telemetry probe in content process. There should be no stripped + // navigation count. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + undefined, + LABEL_STRIP_FOR_NAVIGATION + ); + // Check if the navigation count is increased. + newNavigationCnt = await getTelemetryProbe( + QUERY_STRIPPING_COUNT, + LABEL_NAVIGATION, + cnt => cnt > 0 + ); + ok(newNavigationCnt > 0, "There is navigation count added."); + + // Third, test the redirect. + testThirdPartyURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?paramToStrip=value`; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + `${TEST_THIRD_PARTY_URI}?paramToStrip=value` + ); + + // Trigger the redirect. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await locationChangePromise; + + // Verify if the query string was happened. + await verifyQueryString(browser, "paramToStrip=value"); + }); + + // Verify the telemetry probe. The stripped redirect count should not exist. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + undefined, + LABEL_STRIP_FOR_REDIRECT + ); + await checkTelemetryProbe(QUERY_STRIPPING_COUNT, 1, LABEL_REDIRECT); + + await clearTelemetry(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js new file mode 100644 index 0000000000..9f7c6146f1 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js @@ -0,0 +1,160 @@ +"use strict"; + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; + +const QUERY_STRIPPING_COUNT = "QUERY_STRIPPING_COUNT"; +const QUERY_STRIPPING_PARAM_COUNT = "QUERY_STRIPPING_PARAM_COUNT"; +const QUERY_STRIPPING_COUNT_BY_PARAM = "QUERY_STRIPPING_COUNT_BY_PARAM"; + +const histogramLabels = + Services.telemetry.getCategoricalLabels().QUERY_STRIPPING_COUNT_BY_PARAM; + +async function clearTelemetry() { + // There's an arbitrary interval of 2 seconds in which the content + // processes sync their data with the parent process, we wait + // this out to ensure that we clear everything. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry.getHistogramById(QUERY_STRIPPING_COUNT).clear(); + Services.telemetry.getHistogramById(QUERY_STRIPPING_PARAM_COUNT).clear(); + Services.telemetry.getHistogramById(QUERY_STRIPPING_COUNT_BY_PARAM).clear(); + + let isCleared = () => { + let histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).content; + + return ( + !histograms || + (!histograms[QUERY_STRIPPING_COUNT] && + !histograms[QUERY_STRIPPING_PARAM_COUNT] && + !histograms.QUERY_STRIPPING_COUNT_BY_PARAM) + ); + }; + + // Check that the telemetry probes have been cleared properly. Do this check + // sync first to avoid any race conditions where telemetry arrives after + // clearing. + if (!isCleared()) { + await TestUtils.waitForCondition(isCleared); + } + + ok(true, "Telemetry has been cleared."); +} + +async function verifyQueryString(browser, expected) { + await SpecialPowers.spawn(browser, [expected], expected => { + // Strip the first question mark. + let search = content.location.search.slice(1); + + is(search, expected, "The query string is correct."); + }); +} + +function testTelemetry(queryParamToCount) { + const histogram = Services.telemetry.getHistogramById( + QUERY_STRIPPING_COUNT_BY_PARAM + ); + + let snapshot = histogram.snapshot(); + + let indexToCount = {}; + Object.entries(queryParamToCount).forEach(([key, value]) => { + let index = histogramLabels.indexOf(`param_${key}`); + + // In debug builds we perform additional stripping for testing, which + // results in telemetry being recorded twice. This does not impact + // production builds. + if (SpecialPowers.isDebugBuild) { + indexToCount[index] = value * 2; + } else { + indexToCount[index] = value; + } + }); + + for (let [i, val] of Object.entries(snapshot.values)) { + let expectedCount = indexToCount[i] || 0; + + is( + val, + expectedCount, + `Histogram ${QUERY_STRIPPING_COUNT_BY_PARAM} should have expected value for label ${histogramLabels[i]}.` + ); + } +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.enabled", true], + [ + "privacy.query_stripping.strip_list", + "foo mc_eid oly_anon_id oly_enc_id __s vero_id _hsenc mkt_tok fbclid ysclid", + ], + ], + }); + + // Clear Telemetry probes before testing. + await clearTelemetry(); +}); + +/** + * Tests the QUERY_STRIPPING_COUNT_BY_PARAM histogram telemetry which counts how + * often query params from a predefined lists are stripped. + */ +add_task(async function test_queryParamCountTelemetry() { + info("Test with a query params to be stripped and recoded in telemetry."); + let url = new URL(TEST_URI); + url.searchParams.set("mc_eid", "myValue"); + + // Open a new tab and trigger the query stripping. + await BrowserTestUtils.withNewTab(url.href, async browser => { + // Verify that the tracking query param has been stripped. + await verifyQueryString(browser, ""); + }); + + testTelemetry({ mc_eid: 1 }); + + // Repeat this with the same query parameter, the respective histogram bucket + // should be incremented. + await BrowserTestUtils.withNewTab(url.href, async browser => { + await verifyQueryString(browser, ""); + }); + + testTelemetry({ mc_eid: 2 }); + + url = new URL(TEST_URI); + url.searchParams.set("fbclid", "myValue2"); + url.searchParams.set("mkt_tok", "myValue3"); + url.searchParams.set("bar", "foo"); + + info("Test with multiple query params to be stripped."); + await BrowserTestUtils.withNewTab(url.href, async browser => { + await verifyQueryString(browser, "bar=foo"); + }); + testTelemetry({ mc_eid: 2, fbclid: 1, mkt_tok: 1 }); + + info( + "Test with query param on the strip-list, which should not be recoded in telemetry." + ); + url = new URL(TEST_URI); + url.searchParams.set("foo", "bar"); + url.searchParams.set("__s", "myValue4"); + url.searchParams.set("ysclid", "myValue5"); + await BrowserTestUtils.withNewTab(url.href, async browser => { + await verifyQueryString(browser, ""); + }); + testTelemetry({ mc_eid: 2, fbclid: 1, mkt_tok: 1, __s: 1, ysclid: 1 }); + + url = new URL(TEST_URI); + url.searchParams.set("foo", "bar"); + await BrowserTestUtils.withNewTab(url.href, async browser => { + await verifyQueryString(browser, ""); + }); + testTelemetry({ mc_eid: 2, fbclid: 1, mkt_tok: 1, __s: 1, ysclid: 1 }); + + await clearTelemetry(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js new file mode 100644 index 0000000000..ee791e3cea --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js @@ -0,0 +1,253 @@ +/* vim: set ts=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/. */ + +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "urlQueryStrippingListService", + "@mozilla.org/query-stripping-list-service;1", + "nsIURLQueryStrippingListService" +); + +const COLLECTION_NAME = "query-stripping"; + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "empty.html"; +const TEST_THIRD_PARTY_URI = TEST_DOMAIN_2 + TEST_PATH + "empty.html"; + +// The Update Event here is used to listen the observer from the +// URLQueryStrippingListService. We need to use the event here so that the same +// observer can be called multiple times. +class UpdateEvent extends EventTarget {} + +// The Observer that is registered needs to implement onQueryStrippingListUpdate +// like a nsIURLQueryStrippingListObserver does +class ListObserver { + updateEvent = new UpdateEvent(); + + onQueryStrippingListUpdate(stripList, allowList) { + let event = new CustomEvent("update", { detail: { stripList, allowList } }); + this.updateEvent.dispatchEvent(event); + } +} + +function waitForEvent(element, eventName) { + return BrowserTestUtils.waitForEvent(element, eventName).then(e => e.detail); +} + +async function verifyQueryString(browser, expected) { + await SpecialPowers.spawn(browser, [expected], expected => { + // Strip the first question mark. + let search = content.location.search.slice(1); + + is(search, expected, "The query string is correct."); + }); +} + +async function check(query, expected) { + // Open a tab with the query string. + let testURI = TEST_URI + "?" + query; + + // Test for stripping in parent process. + await BrowserTestUtils.withNewTab(testURI, async browser => { + // Verify if the query string is expected in the new tab. + await verifyQueryString(browser, expected); + }); + + testURI = TEST_URI + "?" + query; + let expectedURI; + if (expected != "") { + expectedURI = TEST_URI + "?" + expected; + } else { + expectedURI = TEST_URI; + } + + // Test for stripping in content processes. This will first open a third-party + // page and create a link to the test uri. And then, click the link to + // navigate the page, which will trigger the stripping in content processes. + await BrowserTestUtils.withNewTab(TEST_THIRD_PARTY_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + expectedURI + ); + + // Create a link and click it to navigate. + await SpecialPowers.spawn(browser, [testURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, expected); + }); +} + +registerCleanupFunction(() => { + Cc["@mozilla.org/query-stripping-list-service;1"] + .getService(Ci.nsIURLQueryStrippingListService) + .clearLists(); +}); + +add_task(async function testPrefSettings() { + // Enable query stripping and clear the prefs at the beginning. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.enabled", true], + ["privacy.query_stripping.strip_list", ""], + ["privacy.query_stripping.allow_list", ""], + ["privacy.query_stripping.testing", true], + ], + }); + + // Test if the observer been called when adding to the service. + let obs = new ListObserver(); + let promise = waitForEvent(obs.updateEvent, "update"); + urlQueryStrippingListService.registerAndRunObserver(obs); + let lists = await promise; + is(lists.stripList, "", "No strip list at the beginning."); + is(lists.allowList, "", "No allow list at the beginning."); + + // Verify that no query stripping happens. + await check("pref_query1=123", "pref_query1=123"); + await check("pref_query2=456", "pref_query2=456"); + + // Set pref for strip list + promise = waitForEvent(obs.updateEvent, "update"); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.strip_list", "pref_query1 pref_query2"]], + }); + lists = await promise; + + is( + lists.stripList, + "pref_query1 pref_query2", + "There should be strip list entries." + ); + is(lists.allowList, "", "There should be no allow list entries."); + + // The query string should be stripped. + await check("pref_query1=123", ""); + await check("pref_query2=456", ""); + + // Set the pref for allow list. + promise = waitForEvent(obs.updateEvent, "update"); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.allow_list", "example.net"]], + }); + lists = await promise; + + is( + lists.stripList, + "pref_query1 pref_query2", + "There should be strip list entires." + ); + is(lists.allowList, "example.net", "There should be one allow list entry."); + + // The query string shouldn't be stripped because this host is in allow list. + await check("pref_query1=123", "pref_query1=123"); + await check("pref_query2=123", "pref_query2=123"); + + urlQueryStrippingListService.unregisterObserver(obs); + + // Clear prefs. + SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testRemoteSettings() { + // Enable query stripping and clear the prefs at the beginning. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.enabled", true], + ["privacy.query_stripping.strip_list", ""], + ["privacy.query_stripping.allow_list", ""], + ["privacy.query_stripping.testing", true], + ], + }); + + // Add initial empty record. + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), []); + + // Test if the observer been called when adding to the service. + let obs = new ListObserver(); + let promise = waitForEvent(obs.updateEvent, "update"); + urlQueryStrippingListService.registerAndRunObserver(obs); + let lists = await promise; + is(lists.stripList, "", "No strip list at the beginning."); + is(lists.allowList, "", "No allow list at the beginning."); + + // Verify that no query stripping happens. + await check("remote_query1=123", "remote_query1=123"); + await check("remote_query2=456", "remote_query2=456"); + + // Set record for strip list. + promise = waitForEvent(obs.updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: [ + { + id: "1", + last_modified: 1000000000000001, + stripList: ["remote_query1", "remote_query2"], + allowList: [], + }, + ], + }, + }); + lists = await promise; + + is( + lists.stripList, + "remote_query1 remote_query2", + "There should be strip list entries." + ); + is(lists.allowList, "", "There should be no allow list entries."); + + // The query string should be stripped. + await check("remote_query1=123", ""); + await check("remote_query2=456", ""); + + // Set record for strip list and allow list. + promise = waitForEvent(obs.updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: [ + { + id: "2", + last_modified: 1000000000000002, + stripList: ["remote_query1", "remote_query2"], + allowList: ["example.net"], + }, + ], + }, + }); + lists = await promise; + + is( + lists.stripList, + "remote_query1 remote_query2", + "There should be strip list entries." + ); + is(lists.allowList, "example.net", "There should be one allow list entry."); + + // The query string shouldn't be stripped because this host is in allow list. + await check("remote_query1=123", "remote_query1=123"); + await check("remote_query2=123", "remote_query2=123"); + + urlQueryStrippingListService.unregisterObserver(obs); + + // Clear the remote settings. + await db.clear(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_userInteraction.js b/toolkit/components/antitracking/test/browser/browser_userInteraction.js new file mode 100644 index 0000000000..d343a56731 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_userInteraction.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.userInteraction.document.interval", 1], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let uri = Services.io.newURI(TEST_DOMAIN); + is( + PermissionTestUtils.testPermission(uri, "storageAccessAPI"), + Services.perms.UNKNOWN_ACTION, + "Before user-interaction we don't have a permission" + ); + + let promise = TestUtils.topicObserved("perm-changed", (aSubject, aData) => { + let permission = aSubject.QueryInterface(Ci.nsIPermission); + return ( + permission.type == "storageAccessAPI" && + permission.principal.equalsURI(uri) + ); + }); + + info("Simulating user-interaction."); + await SpecialPowers.spawn(browser, [], async function () { + content.document.userInteractionForTesting(); + }); + + info("Waiting to have a permissions set."); + await promise; + + // Let's see if the document is able to update the permission correctly. + for (var i = 0; i < 3; ++i) { + // Another perm-changed event should be triggered by the timer. + promise = TestUtils.topicObserved("perm-changed", (aSubject, aData) => { + let permission = aSubject.QueryInterface(Ci.nsIPermission); + return ( + permission.type == "storageAccessAPI" && + permission.principal.equalsURI(uri) + ); + }); + + info("Simulating another user-interaction."); + await SpecialPowers.spawn(browser, [], async function () { + content.document.userInteractionForTesting(); + }); + + info("Waiting to have a permissions set."); + await promise; + } + + // Let's disable the document.interval. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userInteraction.document.interval", 0]], + }); + + promise = new Promise(resolve => { + let id; + + function observer(subject, topic, data) { + ok(false, "Notification received!"); + Services.obs.removeObserver(observer, "perm-changed"); + clearTimeout(id); + resolve(); + } + + Services.obs.addObserver(observer, "perm-changed"); + + id = setTimeout(() => { + ok(true, "No notification received!"); + Services.obs.removeObserver(observer, "perm-changed"); + resolve(); + }, 2000); + }); + + info("Simulating another user-interaction."); + await SpecialPowers.spawn(browser, [], async function () { + content.document.userInteractionForTesting(); + }); + + info("Waiting to have a permissions set."); + await promise; + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_workerPropagation.js b/toolkit/components/antitracking/test/browser/browser_workerPropagation.js new file mode 100644 index 0000000000..54ec0c1bf6 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_workerPropagation.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", true], + ["dom.storage_access.auto_grants.delayed", false], + ["dom.storage_access.enabled", true], + ["dom.storage_access.prompt.testing", false], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Let's create an iframe and run the test there. + let page = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "workerIframe.html"; + await SpecialPowers.spawn(browser, [page], async function (page) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.id = "test"; + + content.addEventListener("message", e => { + if (e.data.type == "finish") { + resolve(); + return; + } + + if (e.data.type == "info") { + info(e.data.msg); + return; + } + + if (e.data.type == "ok") { + ok(e.data.what, e.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = page; + }); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/clearSiteData.sjs b/toolkit/components/antitracking/test/browser/clearSiteData.sjs new file mode 100644 index 0000000000..374f03a474 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/clearSiteData.sjs @@ -0,0 +1,6 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + aResponse.setHeader("Clear-Site-Data", '"*"'); + aResponse.setHeader("Content-Type", "text/plain"); + aResponse.write("Clear-Site-Data"); +} diff --git a/toolkit/components/antitracking/test/browser/container.html b/toolkit/components/antitracking/test/browser/container.html new file mode 100644 index 0000000000..24daa80113 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/container.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<iframe src="embedder.html"></iframe> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/container2.html b/toolkit/components/antitracking/test/browser/container2.html new file mode 100644 index 0000000000..c2591ad7fc --- /dev/null +++ b/toolkit/components/antitracking/test/browser/container2.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<body> +<script> + onmessage = function(e) { + parent.postMessage(e.data, "*"); + }; +</script> +<iframe src="embedder2.html"></iframe> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/cookies.sjs b/toolkit/components/antitracking/test/browser/cookies.sjs new file mode 100644 index 0000000000..1267a69d8c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/cookies.sjs @@ -0,0 +1,12 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + let cookie = ""; + if (aRequest.hasHeader("Cookie")) { + cookie = aRequest.getHeader("Cookie"); + } + aResponse.write("cookie:" + cookie); + + if (aRequest.queryString) { + aResponse.setHeader("Set-Cookie", "foopy=" + aRequest.queryString); + } +} diff --git a/toolkit/components/antitracking/test/browser/cookiesCORS.sjs b/toolkit/components/antitracking/test/browser/cookiesCORS.sjs new file mode 100644 index 0000000000..2cfdca2700 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/cookiesCORS.sjs @@ -0,0 +1,9 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + aResponse.setHeader("Access-Control-Allow-Origin", "http://example.net"); + aResponse.setHeader("Access-Control-Allow-Credentials", "true"); + + if (aRequest.queryString) { + aResponse.setHeader("Set-Cookie", "foopy=" + aRequest.queryString); + } +} diff --git a/toolkit/components/antitracking/test/browser/dedicatedWorker.js b/toolkit/components/antitracking/test/browser/dedicatedWorker.js new file mode 100644 index 0000000000..72fd4ad850 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/dedicatedWorker.js @@ -0,0 +1,3 @@ +self.onmessage = msg => { + self.postMessage(msg.data); +}; diff --git a/toolkit/components/antitracking/test/browser/dynamicfpi_head.js b/toolkit/components/antitracking/test/browser/dynamicfpi_head.js new file mode 100644 index 0000000000..6eaa620508 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/dynamicfpi_head.js @@ -0,0 +1,180 @@ +/* vim: set ts=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/. */ + +/* import-globals-from head.js */ + +"use strict"; + +this.DynamicFPIHelper = { + getTestPageConfig(runInSecureContext) { + if (runInSecureContext) { + return { + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_4TH_PARTY_STORAGE_PAGE_HTTPS, + partitionKey: "(https,example.net)", + }; + } + return { + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_4TH_PARTY_STORAGE_PAGE, + partitionKey: "(http,example.net)", + }; + }, + + runTest( + name, + callback, + cleanupFunction, + extraPrefs, + runInPrivateWindow, + { runInSecureContext = false } = {} + ) { + add_task(async _ => { + info( + "Starting test `" + + name + + "' with dynamic FPI running in a " + + (runInPrivateWindow ? "private" : "normal") + + " window..." + ); + + await SpecialPowers.flushPrefEnv(); + await setCookieBehaviorPref( + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + runInPrivateWindow + ); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + true, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ["privacy.dynamic_firstparty.use_site", true], + ["dom.security.https_first_pbm", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "not-tracking.example.com", + ], + ], + }); + + if (extraPrefs && Array.isArray(extraPrefs) && extraPrefs.length) { + await SpecialPowers.pushPrefEnv({ set: extraPrefs }); + } + + let win = window; + if (runInPrivateWindow) { + win = OpenBrowserWindow({ private: true }); + await TestUtils.topicObserved("browser-delayed-startup-finished"); + } + + const { topPage, thirdPartyPage, partitionKey } = + this.getTestPageConfig(runInSecureContext); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(win.gBrowser, topPage); + win.gBrowser.selectedTab = tab; + + let browser = win.gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Check the cookieJarSettings of the browser object"); + ok( + browser.cookieJarSettings, + "The browser object has the cookieJarSettings." + ); + is( + browser.cookieJarSettings.cookieBehavior, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + "The cookieJarSettings has the correct cookieBehavior" + ); + is( + browser.cookieJarSettings.partitionKey, + partitionKey, + "The cookieJarSettings has the correct partitionKey" + ); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: thirdPartyPage, + callback: callback.toString(), + partitionKey, + }, + ], + async obj => { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = async _ => { + await SpecialPowers.spawn(ifr, [obj], async obj => { + is( + content.document.nodePrincipal.originAttributes.partitionKey, + "", + "We don't have first-party set on nodePrincipal" + ); + is( + content.document.effectiveStoragePrincipal.originAttributes + .partitionKey, + obj.partitionKey, + "We have first-party set on storagePrincipal" + ); + }); + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.callback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (runInPrivateWindow) { + win.close(); + } + }); + + add_task(async _ => { + info("Cleaning up."); + if (cleanupFunction) { + await cleanupFunction(); + } + + // While running these tests we typically do not have enough idle time to do + // GC reliably, so force it here. + /* import-globals-from antitracking_head.js */ + forceGC(); + }); + }, +}; diff --git a/toolkit/components/antitracking/test/browser/embedder.html b/toolkit/components/antitracking/test/browser/embedder.html new file mode 100644 index 0000000000..1a517079e0 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/embedder.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js"></script> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect"></script> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect2"></script> diff --git a/toolkit/components/antitracking/test/browser/embedder2.html b/toolkit/components/antitracking/test/browser/embedder2.html new file mode 100644 index 0000000000..b1441894a4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/embedder2.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js"></script> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect"></script> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect2"></script> +</head> +<body onload="parent.postMessage({data:document.querySelectorAll('script').length}, '*');"></body> +</html> diff --git a/toolkit/components/antitracking/test/browser/empty-altsvc.js b/toolkit/components/antitracking/test/browser/empty-altsvc.js new file mode 100644 index 0000000000..3053583c76 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/empty-altsvc.js @@ -0,0 +1 @@ +/* nothing here */ diff --git a/toolkit/components/antitracking/test/browser/empty-altsvc.js^headers^ b/toolkit/components/antitracking/test/browser/empty-altsvc.js^headers^ new file mode 100644 index 0000000000..70592d2f93 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/empty-altsvc.js^headers^ @@ -0,0 +1 @@ +Alt-Svc: h2=":12345"; ma=60 diff --git a/toolkit/components/antitracking/test/browser/empty.html b/toolkit/components/antitracking/test/browser/empty.html new file mode 100644 index 0000000000..e20d67db57 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/empty.html @@ -0,0 +1 @@ +<h1>Empty</h1> diff --git a/toolkit/components/antitracking/test/browser/empty.js b/toolkit/components/antitracking/test/browser/empty.js new file mode 100644 index 0000000000..3053583c76 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/empty.js @@ -0,0 +1 @@ +/* nothing here */ diff --git a/toolkit/components/antitracking/test/browser/file_iframe_document_open.html b/toolkit/components/antitracking/test/browser/file_iframe_document_open.html new file mode 100644 index 0000000000..fd2969f270 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_iframe_document_open.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<script> +function run() { + let ifr = document.createElement("iframe"); + document.body.appendChild(ifr); + + let doc = ifr.contentWindow.document; + doc.open(); + doc.write(`<script>document.cookie = "foo=bar"<\/script>`); + doc.close(); +} +</script> +</head> +<body onLoad="run();"> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/file_localStorage.html b/toolkit/components/antitracking/test/browser/file_localStorage.html new file mode 100644 index 0000000000..54bad94bc9 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_localStorage.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> + <title>Bug 1663192 - Accessing localStorage in a file urls</title> +</head> +<script> + window.addEventListener("DOMContentLoaded", () => { + let result = document.getElementById("result"); + + try { + window.localStorage.setItem("foo", "bar"); + result.textContent = "PASS"; + } catch (e) { + result.textContent = "FAIL"; + } + }, { once: true }); +</script> +<body> +<a id="result"></a> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/file_saveAsImage.sjs b/toolkit/components/antitracking/test/browser/file_saveAsImage.sjs new file mode 100644 index 0000000000..2afb7d435f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_saveAsImage.sjs @@ -0,0 +1,20 @@ +// small red image +const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + if (aRequest.queryString.includes("result")) { + aResponse.write(getState("hints") || 0); + setState("hints", "0"); + } else { + let hints = parseInt(getState("hints") || 0) + 1; + setState("hints", hints.toString()); + + aResponse.setHeader("Content-Type", "image/png", false); + aResponse.write(IMAGE); + } +} diff --git a/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html b/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html new file mode 100644 index 0000000000..aa3de2a555 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html @@ -0,0 +1,6 @@ +<html> +<body> + <img src="http://example.net/browser/toolkit/components/antitracking/test/browser/raptor.jpg" id="image1"> + <video src="http://example.net/browser/toolkit/components/antitracking/test/browser/file_video.ogv" id="video1"> </video> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/file_saveAsVideo.sjs b/toolkit/components/antitracking/test/browser/file_saveAsVideo.sjs new file mode 100644 index 0000000000..10bf246f62 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_saveAsVideo.sjs @@ -0,0 +1,38 @@ +const VIDEO = atob( + "GkXfo49CgoR3ZWJtQoeBAkKFgQIYU4BnI0nOEU2bdKpNu" + + "4tTq4QVSalmU6yBL027i1OrhBZUrmtTrIGmTbuLU6uEHF" + + "O7a1OsgdEVSalm8k2ApWxpYmVibWwyIHYwLjkuNyArIGx" + + "pYm1hdHJvc2thMiB2MC45LjhXQZ5ta2NsZWFuIDAuMi41" + + "IGZyb20gTGF2ZjUyLjU3LjFzpJC/CoyJjSbckmOytS9Se" + + "y3cRImIQK9AAAAAAABEYYgEHiZDUAHwABZUrmumrqTXgQ" + + "FzxYEBnIEAIrWcg3VuZIaFVl9WUDiDgQHgh7CCAUC6gfA" + + "cU7trzbuMs4EAt4f3gQHxggEju42zggMgt4f3gQHxgrZd" + + "u46zggZAt4j3gQHxgwFba7uOs4IJYLeI94EB8YMCAR+7j" + + "rOCDIC3iPeBAfGDAqggH0O2dSBibueBAKNbiYEAAIDQdw" + + "CdASpAAfAAAAcIhYWIhYSIAIIb347n/c/Z0BPBfjv7f+I" + + "/6df275Wbh/XPuZ+qv9k58KrftA9tvkP+efiN/ovmd/if" + + "9L/ef2z+Xv+H/rv+39xD/N/zL+nfid71363e9X+pf6/+q" + + "+x7+W/0P/H/233Zf6n/Wv696APuDf0b+YdcL+2HsUfxr+" + + "gfP/91P+F/yH9K+H79Z/7j/Xvhj/VjVsvwHXt/n/qz9U/" + + "w749+c/g=" +); + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + if (aRequest.queryString.includes("result")) { + aResponse.write(getState("hints") || 0); + setState("hints", "0"); + } else { + let hints = parseInt(getState("hints") || 0) + 1; + setState("hints", hints.toString()); + + aResponse.setHeader("Content-Type", "video/webm", false); + aResponse.setHeader( + "Cache-Control", + "public, max-age=604800, immutable", + false + ); + aResponse.write(VIDEO); + } +} diff --git a/toolkit/components/antitracking/test/browser/file_stripping.html b/toolkit/components/antitracking/test/browser/file_stripping.html new file mode 100644 index 0000000000..60d9840a0f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_stripping.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf8"> +<script> + onmessage = event => { + switch (event.data.type) { + case "window-open": + window.open(event.data.url); + break; + case "script": + window.location.href = event.data.url; + break + } + }; +</script> +</head> +<body> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/file_video.ogv b/toolkit/components/antitracking/test/browser/file_video.ogv Binary files differnew file mode 100644 index 0000000000..68dee3cf2b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_video.ogv diff --git a/toolkit/components/antitracking/test/browser/file_ws_handshake_delay_wsh.py b/toolkit/components/antitracking/test/browser/file_ws_handshake_delay_wsh.py new file mode 100644 index 0000000000..412ac2e24c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_ws_handshake_delay_wsh.py @@ -0,0 +1,28 @@ +import time + +from mod_pywebsocket import msgutil + + +def web_socket_do_extra_handshake(request): + # # must set request.ws_protocol to the selected version from ws_requested_protocols + for x in request.ws_requested_protocols: + if x != "test-does-not-exist": + request.ws_protocol = x + break + + if request.ws_protocol == "test-3": + time.sleep(3) + elif request.ws_protocol == "test-6": + time.sleep(6) + else: + pass + + +def web_socket_passive_closing_handshake(request): + if request.ws_close_code == 1005: + return None, None + return request.ws_close_code, request.ws_close_reason + + +def web_socket_transfer_data(request): + msgutil.close_connection(request) diff --git a/toolkit/components/antitracking/test/browser/head.js b/toolkit/components/antitracking/test/browser/head.js new file mode 100644 index 0000000000..d7721b7cac --- /dev/null +++ b/toolkit/components/antitracking/test/browser/head.js @@ -0,0 +1,152 @@ +/* vim: set ts=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/. */ + +"use strict"; + +const TEST_DOMAIN = "http://example.net/"; +const TEST_DOMAIN_HTTPS = "https://example.net/"; +const TEST_DOMAIN_2 = "http://xn--exmple-cua.test/"; +const TEST_DOMAIN_3 = "https://xn--hxajbheg2az3al.xn--jxalpdlp/"; +const TEST_DOMAIN_4 = "http://prefixexample.com/"; +const TEST_DOMAIN_5 = "http://test/"; +const TEST_DOMAIN_6 = "http://mochi.test:8888/"; +const TEST_DOMAIN_7 = "http://example.com/"; +const TEST_DOMAIN_8 = "http://www.example.com/"; +const TEST_3RD_PARTY_DOMAIN = "https://tracking.example.org/"; +const TEST_3RD_PARTY_DOMAIN_HTTP = "http://tracking.example.org/"; +const TEST_3RD_PARTY_DOMAIN_TP = "https://tracking.example.com/"; +const TEST_3RD_PARTY_DOMAIN_STP = "https://social-tracking.example.org/"; +const TEST_4TH_PARTY_DOMAIN = "http://not-tracking.example.com/"; +const TEST_4TH_PARTY_DOMAIN_HTTPS = "https://not-tracking.example.com/"; +const TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTP = + "http://another-tracking.example.net/"; +const TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS = + "https://another-tracking.example.net/"; +const TEST_ANOTHER_3RD_PARTY_DOMAIN = SpecialPowers.useRemoteSubframes + ? TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTP + : TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS; +const TEST_EMAIL_TRACKER_DOMAIN = "http://email-tracking.example.org/"; + +const TEST_PATH = "browser/toolkit/components/antitracking/test/browser/"; + +const TEST_TOP_PAGE = TEST_DOMAIN + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_HTTPS = TEST_DOMAIN_HTTPS + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_2 = TEST_DOMAIN_2 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_3 = TEST_DOMAIN_3 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_4 = TEST_DOMAIN_4 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_5 = TEST_DOMAIN_5 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_6 = TEST_DOMAIN_6 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_7 = TEST_DOMAIN_7 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_8 = TEST_DOMAIN_8 + TEST_PATH + "page.html"; +const TEST_EMBEDDER_PAGE = TEST_DOMAIN + TEST_PATH + "embedder.html"; +const TEST_POPUP_PAGE = TEST_DOMAIN + TEST_PATH + "popup.html"; +const TEST_IFRAME_PAGE = TEST_DOMAIN + TEST_PATH + "iframe.html"; +const TEST_3RD_PARTY_PAGE = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdParty.html"; +const TEST_3RD_PARTY_PAGE_HTTP = + TEST_3RD_PARTY_DOMAIN_HTTP + TEST_PATH + "3rdParty.html"; +const TEST_3RD_PARTY_PAGE_WO = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyWO.html"; +const TEST_3RD_PARTY_PAGE_UI = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyUI.html"; +const TEST_3RD_PARTY_PAGE_WITH_SVG = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartySVG.html"; +const TEST_3RD_PARTY_PAGE_RELAY = + TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdPartyRelay.html"; +const TEST_4TH_PARTY_PAGE = TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdParty.html"; +const TEST_4TH_PARTY_PAGE_HTTPS = + TEST_4TH_PARTY_DOMAIN_HTTPS + TEST_PATH + "3rdParty.html"; +const TEST_ANOTHER_3RD_PARTY_PAGE = + TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "3rdParty.html"; +const TEST_ANOTHER_3RD_PARTY_PAGE_HTTPS = + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS + TEST_PATH + "3rdParty.html"; +const TEST_3RD_PARTY_STORAGE_PAGE = + TEST_3RD_PARTY_DOMAIN_HTTP + TEST_PATH + "3rdPartyStorage.html"; +const TEST_3RD_PARTY_PAGE_WORKER = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyWorker.html"; +const TEST_3RD_PARTY_PARTITIONED_PAGE = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyPartitioned.html"; +const TEST_4TH_PARTY_STORAGE_PAGE = + TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdPartyStorage.html"; +const TEST_4TH_PARTY_STORAGE_PAGE_HTTPS = + TEST_4TH_PARTY_DOMAIN_HTTPS + TEST_PATH + "3rdPartyStorage.html"; +const TEST_4TH_PARTY_PARTITIONED_PAGE = + TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdPartyPartitioned.html"; +const TEST_4TH_PARTY_PARTITIONED_PAGE_HTTPS = + TEST_4TH_PARTY_DOMAIN_HTTPS + TEST_PATH + "3rdPartyPartitioned.html"; +const BEHAVIOR_ACCEPT = Ci.nsICookieService.BEHAVIOR_ACCEPT; +const BEHAVIOR_REJECT = Ci.nsICookieService.BEHAVIOR_REJECT; +const BEHAVIOR_LIMIT_FOREIGN = Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN; +const BEHAVIOR_REJECT_FOREIGN = Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN; +const BEHAVIOR_REJECT_TRACKER = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER; +const BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN = + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + +let originalRequestLongerTimeout = requestLongerTimeout; +// eslint-disable-next-line no-global-assign +requestLongerTimeout = function AntiTrackingRequestLongerTimeout(factor) { + let ccovMultiplier = AppConstants.MOZ_CODE_COVERAGE ? 2 : 1; + let fissionMultiplier = SpecialPowers.useRemoteSubframes ? 2 : 1; + originalRequestLongerTimeout(ccovMultiplier * fissionMultiplier * factor); +}; + +requestLongerTimeout(3); + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/antitracking_head.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/partitionedstorage_head.js", + this +); + +function setCookieBehaviorPref(cookieBehavior, runInPrivateWindow) { + let cbRegular; + let cbPrivate; + + // Set different cookieBehaviors to regular mode and private mode so that we + // can make sure these two prefs don't interfere with each other for all + // tests. + if (runInPrivateWindow) { + cbPrivate = cookieBehavior; + + let defaultPrefBranch = Services.prefs.getDefaultBranch(""); + // In order to test the default private cookieBehavior pref, we need to set + // the regular pref to the default value because we don't want the private + // pref to mirror the regular pref in this case. + // + // Note that the private pref will mirror the regular pref if the private + // pref is in default value and the regular pref is not in default value. + if ( + cookieBehavior == + defaultPrefBranch.getIntPref("network.cookie.cookieBehavior.pbmode") + ) { + cbRegular = defaultPrefBranch.getIntPref("network.cookie.cookieBehavior"); + } else { + cbRegular = + cookieBehavior == BEHAVIOR_ACCEPT ? BEHAVIOR_REJECT : BEHAVIOR_ACCEPT; + } + } else { + cbRegular = cookieBehavior; + cbPrivate = + cookieBehavior == BEHAVIOR_ACCEPT ? BEHAVIOR_REJECT : BEHAVIOR_ACCEPT; + } + + return SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", cbRegular], + ["network.cookie.cookieBehavior.pbmode", cbPrivate], + ], + }); +} diff --git a/toolkit/components/antitracking/test/browser/iframe.html b/toolkit/components/antitracking/test/browser/iframe.html new file mode 100644 index 0000000000..85d37ed7fa --- /dev/null +++ b/toolkit/components/antitracking/test/browser/iframe.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>Just a first-level iframe</title> +</head> +<body> + <h1>This is the first-level iframe</h1> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/image.sjs b/toolkit/components/antitracking/test/browser/image.sjs new file mode 100644 index 0000000000..145ff1f0a4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/image.sjs @@ -0,0 +1,22 @@ +// A 1x1 PNG image. +// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" +); + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + if (aRequest.queryString.includes("result")) { + aResponse.write(getState("hints") || 0); + setState("hints", "0"); + } else { + let hints = parseInt(getState("hints") || 0) + 1; + setState("hints", hints.toString()); + + aResponse.setHeader("Set-Cookie", "foopy=1"); + aResponse.setHeader("Content-Type", "image/png", false); + aResponse.write(IMAGE); + } +} diff --git a/toolkit/components/antitracking/test/browser/imageCacheWorker.js b/toolkit/components/antitracking/test/browser/imageCacheWorker.js new file mode 100644 index 0000000000..d11221112c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/imageCacheWorker.js @@ -0,0 +1,78 @@ +/* import-globals-from head.js */ +/* import-globals-from antitracking_head.js */ +/* import-globals-from browser_imageCache4.js */ + +AntiTracking.runTest( + "Image cache - should load the image three times.", + // blocking callback + async _ => { + // Let's load the image twice here. + let img = document.createElement("img"); + document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs"; + await new Promise(resolve => { + img.onload = resolve; + }); + ok(true, "Image 1 loaded"); + + img = document.createElement("img"); + document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs"; + await new Promise(resolve => { + img.onload = resolve; + }); + ok(true, "Image 2 loaded"); + }, + + // non-blocking callback + { + runExtraTests: false, + cookieBehavior, + blockingByAllowList, + expectedBlockingNotifications, + callback: async _ => { + // Let's load the image twice here as well. + let img = document.createElement("img"); + document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs"; + await new Promise(resolve => { + img.onload = resolve; + }); + ok(true, "Image 3 loaded"); + + img = document.createElement("img"); + document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs"; + await new Promise(resolve => { + img.onload = resolve; + }); + ok(true, "Image 4 loaded"); + }, + }, + null, // cleanup function + null, // no extra prefs + false, // no window open test + false, // no user-interaction test + expectedBlockingNotifications +); + +// We still want to see just expected requests. +add_task(async _ => { + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs?result" + ) + .then(r => r.text()) + .then(text => { + is(text, "2", "The image should be loaded correctly."); + }); + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/localStorage.html b/toolkit/components/antitracking/test/browser/localStorage.html new file mode 100644 index 0000000000..e08c25f2c4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/localStorage.html @@ -0,0 +1,68 @@ +<h1>Here a tracker!</h1> +<script> + +if (window.opener) { + SpecialPowers.wrap(document).userInteractionForTesting(); + localStorage.foo = "opener" + Math.random(); + // Don't call window.close immediatelly. It can happen that adding the + // "storage" event listener below takes more time than usual (it may need to + // synchronously subscribe in the parent process to receive storage + // notifications). Spending more time in the initial script can prevent + // the "load" event from being fired for the window opened by "open and test". + setTimeout(() => { + window.close(); + }, 0); +} + +if (parent) { + window.onmessage = e => { + if (e.data == "test") { + let status; + try { + localStorage.foo = "value" + Math.random(); + status = true; + } catch (e) { + status = false; + } + + parent.postMessage({type: "test", status }, "*"); + return; + } + + if (e.data == "open") { + window.open("localStorage.html"); + return; + } + + if (e.data == "open and test") { + let w = window.open("localStorage.html"); + w.addEventListener("load", _ => { + let status; + try { + localStorage.foo = "value" + Math.random(); + status = true; + } catch (e) { + status = false; + } + + parent.postMessage({type: "test", status }, "*"); + }, {once: true}); + } + }; + + window.addEventListener("storage", e => { + let fromOpener = localStorage.foo.startsWith("opener"); + + let status; + try { + localStorage.foo = "value" + Math.random(); + status = true; + } catch (e) { + status = false; + } + + parent.postMessage({type: "test", status: status && fromOpener }, "*"); + }); +} + +</script> diff --git a/toolkit/components/antitracking/test/browser/localStorageEvents.html b/toolkit/components/antitracking/test/browser/localStorageEvents.html new file mode 100644 index 0000000000..737d1e0cab --- /dev/null +++ b/toolkit/components/antitracking/test/browser/localStorageEvents.html @@ -0,0 +1,30 @@ +<script> + +let eventCounter = 0; + +onmessage = e => { + if (e.data == "getValue") { + parent.postMessage(localStorage.foo, "*"); + return; + } + + if (e.data == "setValue") { + localStorage.foo = "tracker-" + Math.random(); + return; + } + + if (e.data == "getEvents") { + parent.postMessage(eventCounter, "*"); + return; + } + + if (e.data == "reload") { + window.location.reload(); + } +}; + +addEventListener("storage", _ => { + ++eventCounter; +}); + +</script> diff --git a/toolkit/components/antitracking/test/browser/matchAll.js b/toolkit/components/antitracking/test/browser/matchAll.js new file mode 100644 index 0000000000..8f112b0804 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/matchAll.js @@ -0,0 +1,16 @@ +self.addEventListener("message", async e => { + let clients = await self.clients.matchAll({ + type: "window", + includeUncontrolled: true, + }); + + let hasWindow = false; + for (let client of clients) { + if (e.data == client.url) { + hasWindow = true; + break; + } + } + + e.source.postMessage(hasWindow); +}); diff --git a/toolkit/components/antitracking/test/browser/page.html b/toolkit/components/antitracking/test/browser/page.html new file mode 100644 index 0000000000..a99e8be179 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/page.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>Just a top-level page</title> +</head> +<body> + <h1>This is the top-level page</h1> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/partitionedSharedWorker.js b/toolkit/components/antitracking/test/browser/partitionedSharedWorker.js new file mode 100644 index 0000000000..5ac4ec9f27 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/partitionedSharedWorker.js @@ -0,0 +1,17 @@ +let value = ""; +self.onconnect = e => { + e.ports[0].onmessage = event => { + if (event.data.what === "get") { + e.ports[0].postMessage(value); + return; + } + + if (event.data.what === "put") { + value = event.data.value; + return; + } + + // Error. + e.ports[0].postMessage(-1); + }; +}; diff --git a/toolkit/components/antitracking/test/browser/partitionedstorage_head.js b/toolkit/components/antitracking/test/browser/partitionedstorage_head.js new file mode 100644 index 0000000000..d37b8177c4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/partitionedstorage_head.js @@ -0,0 +1,463 @@ +/* vim: set ts=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/. */ + +/* import-globals-from head.js */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/dynamicfpi_head.js", + this +); + +this.PartitionedStorageHelper = { + runTestInNormalAndPrivateMode( + name, + callback, + cleanupFunction, + extraPrefs, + runInSecure = true + ) { + // Normal mode + this.runTest(name, callback, cleanupFunction, extraPrefs, { + runInSecureContext: runInSecure, + runInPrivateWindow: false, + }); + + // Private mode + this.runTest(name, callback, cleanupFunction, extraPrefs, { + runInSecureContext: runInSecure, + runInPrivateWindow: true, + }); + }, + + runTest( + name, + callback, + cleanupFunction, + extraPrefs, + { runInPrivateWindow = false, runInSecureContext = true } = {} + ) { + DynamicFPIHelper.runTest( + name, + callback, + cleanupFunction, + extraPrefs, + runInPrivateWindow, + { runInSecureContext } + ); + }, + + runPartitioningTestInNormalAndPrivateMode( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + expectUnpartition = false + ) { + // Normal mode + this.runPartitioningTest( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + expectUnpartition, + false + ); + + // Private mode + this.runPartitioningTest( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + expectUnpartition, + true + ); + }, + + runPartitioningTest( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + expectUnpartition, + runInPrivateWindow = false + ) { + for (let variant of ["normal", "initial-aboutblank"]) { + for (let limitForeignContexts of [false, true]) { + this.runPartitioningTestInner( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + variant, + runInPrivateWindow, + limitForeignContexts, + expectUnpartition + ); + } + } + }, + + runPartitioningTestInner( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + variant, + runInPrivateWindow, + limitForeignContexts, + expectUnpartition + ) { + add_task(async _ => { + info( + "Starting test `" + + name + + "' testCategory `" + + testCategory + + "' variant `" + + variant + + "' in a " + + (runInPrivateWindow ? "private" : "normal") + + " window " + + (limitForeignContexts ? "with" : "without") + + " limitForeignContexts to check that 2 tabs are correctly partititioned" + ); + + await SpecialPowers.flushPrefEnv(); + await setCookieBehaviorPref( + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + runInPrivateWindow + ); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + true, + ], + ["privacy.dynamic_firstparty.limitForeign", limitForeignContexts], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ["dom.security.https_first_pbm", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "not-tracking.example.com", + ], + ], + }); + + let win = window; + if (runInPrivateWindow) { + win = OpenBrowserWindow({ private: true }); + await TestUtils.topicObserved("browser-delayed-startup-finished"); + } + + info("Creating the first tab"); + let tab1 = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE); + win.gBrowser.selectedTab = tab1; + + let browser1 = win.gBrowser.getBrowserForTab(tab1); + await BrowserTestUtils.browserLoaded(browser1); + + info("Creating the second tab"); + let tab2 = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE_6); + win.gBrowser.selectedTab = tab2; + + let browser2 = win.gBrowser.getBrowserForTab(tab2); + await BrowserTestUtils.browserLoaded(browser2); + + info("Creating the third tab"); + let tab3 = BrowserTestUtils.addTab( + win.gBrowser, + TEST_4TH_PARTY_PARTITIONED_PAGE + ); + win.gBrowser.selectedTab = tab3; + + let browser3 = win.gBrowser.getBrowserForTab(tab3); + await BrowserTestUtils.browserLoaded(browser3); + + // Use the same URL as first tab to check partitioned data + info("Creating the forth tab"); + let tab4 = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE); + win.gBrowser.selectedTab = tab4; + + let browser4 = win.gBrowser.getBrowserForTab(tab4); + await BrowserTestUtils.browserLoaded(browser4); + + async function getDataFromThirdParty(browser, result) { + // Overwrite the special case here since third party cookies are not + // avilable when `limitForeignContexts` is enabled. + if (testCategory === "cookies" && limitForeignContexts) { + info("overwrite result to empty"); + result = ""; + } + + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_4TH_PARTY_PARTITIONED_PAGE + "?variant=" + variant, + getDataCallback: getDataCallback.toString(), + result, + }, + ], + async obj => { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = __ => { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage({ cb: obj.getDataCallback }, "*"); + }; + + content.addEventListener( + "message", + function msg(event) { + is( + event.data, + obj.result, + "Partitioned cookie jar has value: " + obj.result + ); + resolve(); + }, + { once: true } + ); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + } + + async function getDataFromFirstParty(browser, result) { + await SpecialPowers.spawn( + browser, + [ + { + getDataCallback: getDataCallback.toString(), + result, + variant, + }, + ], + async obj => { + let runnableStr = `(() => {return (${obj.getDataCallback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + let win = content; + if (obj.variant == "initial-aboutblank") { + let i = win.document.createElement("iframe"); + i.src = "about:blank"; + win.document.body.appendChild(i); + // override win to make it point to the initial about:blank window + win = i.contentWindow; + } + + let result = await runnable.call(content, win); + is( + result, + obj.result, + "Partitioned cookie jar is empty: " + obj.result + ); + } + ); + } + + info("Checking 3rd party has an empty cookie jar in first tab"); + await getDataFromThirdParty(browser1, ""); + + info("Checking 3rd party has an empty cookie jar in second tab"); + await getDataFromThirdParty(browser2, ""); + + info("Checking first party has an empty cookie jar in third tab"); + await getDataFromFirstParty(browser3, ""); + + info("Checking 3rd party has an empty cookie jar in forth tab"); + await getDataFromThirdParty(browser4, ""); + + async function createDataInThirdParty(browser, value) { + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_4TH_PARTY_PARTITIONED_PAGE + "?variant=" + variant, + addDataCallback: addDataCallback.toString(), + value, + }, + ], + async obj => { + await new content.Promise(resolve => { + let ifr = content.document.getElementsByTagName("iframe")[0]; + content.addEventListener( + "message", + function msg(event) { + ok(event.data, "Data created"); + resolve(); + }, + { once: true } + ); + + ifr.contentWindow.postMessage( + { + cb: obj.addDataCallback, + value: obj.value, + }, + "*" + ); + }); + } + ); + } + + async function createDataInFirstParty(browser, value) { + await SpecialPowers.spawn( + browser, + [ + { + addDataCallback: addDataCallback.toString(), + value, + variant, + }, + ], + async obj => { + let runnableStr = `(() => {return (${obj.addDataCallback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + let win = content; + if (obj.variant == "initial-aboutblank") { + let i = win.document.createElement("iframe"); + i.src = "about:blank"; + win.document.body.appendChild(i); + // override win to make it point to the initial about:blank window + win = i.contentWindow; + } + + let result = await runnable.call(content, win, obj.value); + ok(result, "Data created"); + } + ); + } + + info("Creating data in the first tab"); + await createDataInThirdParty(browser1, "A"); + + info("Creating data in the second tab"); + await createDataInThirdParty(browser2, "B"); + + // Before writing browser4, check data written by browser1 + info("First tab should still have just 'A'"); + await getDataFromThirdParty(browser1, "A"); + info("Forth tab should still have just 'A'"); + await getDataFromThirdParty(browser4, "A"); + + // Ensure to create data in the forth tab before the third tab, + // otherwise cookie will be written successfully due to prior cookie + // of the base domain exists. + info("Creating data in the forth tab"); + await createDataInThirdParty(browser4, "D"); + + info("Creating data in the third tab"); + await createDataInFirstParty(browser3, "C"); + + // read all tabs + info("First tab should be changed to 'D'"); + await getDataFromThirdParty(browser1, "D"); + + info("Second tab should still have just 'B'"); + await getDataFromThirdParty(browser2, "B"); + + info("Third tab should still have just 'C'"); + await getDataFromFirstParty(browser3, "C"); + + info("Forth tab should still have just 'D'"); + await getDataFromThirdParty(browser4, "D"); + + async function setStorageAccessForThirdParty(browser) { + info(`Setting permission for ${browser.currentURI.spec}`); + let type = "3rdPartyStorage^http://not-tracking.example.com"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + Services.perms.addFromPrincipal( + browser.contentPrincipal, + type, + permission, + expireType, + 0 + ); + // Wait for permission to be set successfully + let originAttributes = runInPrivateWindow + ? { privateBrowsingId: 1 } + : {}; + await new Promise(resolve => { + let id = setInterval(async _ => { + if ( + await SpecialPowers.testPermission(type, permission, { + url: browser.currentURI.spec, + originAttributes, + }) + ) { + clearInterval(id); + resolve(); + } + }, 0); + }); + } + + if (!expectUnpartition) { + info("Setting Storage access for third parties"); + + await setStorageAccessForThirdParty(browser1); + await setStorageAccessForThirdParty(browser2); + await setStorageAccessForThirdParty(browser3); + await setStorageAccessForThirdParty(browser4); + + info("Done setting Storage access for third parties"); + + // read all tabs + info("First tab should still have just 'D'"); + await getDataFromThirdParty(browser1, "D"); + + info("Second tab should still have just 'B'"); + await getDataFromThirdParty(browser2, "B"); + + info("Third tab should still have just 'C'"); + await getDataFromFirstParty(browser3, "C"); + + info("Forth tab should still have just 'D'"); + await getDataFromThirdParty(browser4, "D"); + } + + info("Done checking departitioned state"); + + info("Removing the tabs"); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + + if (runInPrivateWindow) { + win.close(); + } + }); + + add_task(async _ => { + info("Cleaning up."); + if (cleanupFunction) { + await cleanupFunction(); + } + + // While running these tests we typically do not have enough idle time to do + // GC reliably, so force it here. + /* import-globals-from antitracking_head.js */ + forceGC(); + }); + }, +}; diff --git a/toolkit/components/antitracking/test/browser/popup.html b/toolkit/components/antitracking/test/browser/popup.html new file mode 100644 index 0000000000..f195add788 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/popup.html @@ -0,0 +1,11 @@ +<html> +<head> + <title>Just a popup that does a redirect</title> +</head> +<body> + <h1>Just a popup that does a redirect</h1> + <script> + window.location = "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html"; + </script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/raptor.jpg b/toolkit/components/antitracking/test/browser/raptor.jpg Binary files differnew file mode 100644 index 0000000000..243ba9e2d4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/raptor.jpg diff --git a/toolkit/components/antitracking/test/browser/redirect.sjs b/toolkit/components/antitracking/test/browser/redirect.sjs new file mode 100644 index 0000000000..4645aedca5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/redirect.sjs @@ -0,0 +1,11 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 302); + + let query = aRequest.queryString; + let locations = query.split("|"); + let nextLocation = locations.shift(); + if (locations.length) { + nextLocation += "?" + locations.join("|"); + } + aResponse.setHeader("Location", nextLocation); +} diff --git a/toolkit/components/antitracking/test/browser/referrer.sjs b/toolkit/components/antitracking/test/browser/referrer.sjs new file mode 100644 index 0000000000..8e80eb7fa3 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/referrer.sjs @@ -0,0 +1,49 @@ +// A 1x1 PNG image. +// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" +); + +const IFRAME = + "<!DOCTYPE html>\n" + + "<script>\n" + + "onmessage = event => {\n" + + "parent.postMessage(document.referrer, '*');\n" + + "};\n" + + "</script>"; + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + let key; + if (aRequest.queryString.includes("what=script")) { + key = "script"; + } else if (aRequest.queryString.includes("what=image")) { + key = "image"; + } else { + key = "iframe"; + } + + if (aRequest.queryString.includes("result")) { + aResponse.write(getState(key)); + setState(key, ""); + return; + } + + if (aRequest.hasHeader("Referer")) { + let referrer = aRequest.getHeader("Referer"); + setState(key, referrer); + } + + if (key == "script") { + aResponse.setHeader("Content-Type", "text/javascript", false); + aResponse.write("42;"); + } else if (key == "image") { + aResponse.setHeader("Content-Type", "image/png", false); + aResponse.write(IMAGE); + } else { + aResponse.setHeader("Content-Type", "text/html", false); + aResponse.write(IFRAME); + } +} diff --git a/toolkit/components/antitracking/test/browser/sandboxed.html b/toolkit/components/antitracking/test/browser/sandboxed.html new file mode 100644 index 0000000000..a359ae0aa3 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/sandboxed.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> + <body> + <p>Hello, World!</p> + </body> +</html> diff --git a/toolkit/components/antitracking/test/browser/sandboxed.html^headers^ b/toolkit/components/antitracking/test/browser/sandboxed.html^headers^ new file mode 100644 index 0000000000..4705ce9ded --- /dev/null +++ b/toolkit/components/antitracking/test/browser/sandboxed.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts; diff --git a/toolkit/components/antitracking/test/browser/server.sjs b/toolkit/components/antitracking/test/browser/server.sjs new file mode 100644 index 0000000000..4d1b2ef01a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/server.sjs @@ -0,0 +1,20 @@ +function handleRequest(aRequest, aResponse) { + if (aRequest.queryString.includes("redirect")) { + aResponse.setStatusLine(aRequest.httpVersion, 302); + if (aRequest.queryString.includes("redirect-checkonly")) { + aResponse.setHeader("Location", "server.sjs?checkonly"); + } else { + aResponse.setHeader("Location", "server.sjs"); + } + return; + } + aResponse.setStatusLine(aRequest.httpVersion, 200); + if (aRequest.hasHeader("Cookie")) { + aResponse.write("cookie-present"); + } else { + if (!aRequest.queryString.includes("checkonly")) { + aResponse.setHeader("Set-Cookie", "foopy=1"); + } + aResponse.write("cookie-not-present"); + } +} diff --git a/toolkit/components/antitracking/test/browser/serviceWorker.js b/toolkit/components/antitracking/test/browser/serviceWorker.js new file mode 100644 index 0000000000..71c88c721a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/serviceWorker.js @@ -0,0 +1,103 @@ +let value = ""; +let fetch_url = ""; + +self.onfetch = function (e) { + fetch_url = e.request.url; +}; + +// Call clients.claim() to enable fetch event. +self.addEventListener("activate", e => { + e.waitUntil(self.clients.claim()); +}); + +self.addEventListener("message", async e => { + let res = {}; + + switch (e.data.type) { + case "GetHWConcurrency": + res.result = "OK"; + res.value = navigator.hardwareConcurrency; + break; + + case "GetScriptValue": + res.result = "OK"; + res.value = value; + break; + + case "SetScriptValue": + res.result = "OK"; + value = e.data.value; + break; + + case "HasCache": + // Return if the cache storage exists or not. + try { + res.value = await caches.has(e.data.value); + res.result = "OK"; + } catch (e) { + res.result = "ERROR"; + } + break; + + case "SetCache": + // Open a cache storage with the given name. + try { + let cache = await caches.open(e.data.value); + await cache.add("empty.js"); + res.result = "OK"; + } catch (e) { + res.result = "ERROR"; + } + break; + + case "SetIndexedDB": + await new Promise(resolve => { + let idxDB = indexedDB.open("test", 1); + + idxDB.onupgradeneeded = evt => { + let db = evt.target.result; + db.createObjectStore("foobar", { keyPath: "id" }); + }; + + idxDB.onsuccess = evt => { + let db = evt.target.result; + db + .transaction("foobar", "readwrite") + .objectStore("foobar") + .put({ id: 1, value: e.data.value }).onsuccess = _ => { + resolve(); + }; + }; + }); + res.result = "OK"; + break; + + case "GetIndexedDB": + res.value = await new Promise(resolve => { + let idxDB = indexedDB.open("test", 1); + + idxDB.onsuccess = evt => { + let db = evt.target.result; + db.transaction("foobar").objectStore("foobar").get(1).onsuccess = + ee => { + resolve( + ee.target.result === undefined ? "" : ee.target.result.value + ); + }; + }; + }); + res.result = "OK"; + break; + + case "GetFetchURL": + res.value = fetch_url; + fetch_url = ""; + res.result = "OK"; + break; + + default: + res.result = "ERROR"; + } + + e.source.postMessage(res); +}); diff --git a/toolkit/components/antitracking/test/browser/sharedWorker.js b/toolkit/components/antitracking/test/browser/sharedWorker.js new file mode 100644 index 0000000000..01188ed10a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/sharedWorker.js @@ -0,0 +1,18 @@ +let ports = 0; +self.onconnect = e => { + ++ports; + e.ports[0].onmessage = event => { + if (event.data === "count") { + e.ports[0].postMessage(ports); + return; + } + + if (event.data === "close") { + self.close(); + return; + } + + // Error. + e.ports[0].postMessage(-1); + }; +}; diff --git a/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js b/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js new file mode 100644 index 0000000000..7cf379d61d --- /dev/null +++ b/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js @@ -0,0 +1,238 @@ +/* global allowListed */ + +async function hasStorageAccessInitially() { + let hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Has storage access"); +} + +async function noStorageAccessInitially() { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); +} + +async function stillNoStorageAccess() { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Still doesn't have storage access"); +} + +async function callRequestStorageAccess(callback, expectFail) { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + + let origin = new URL(location.href).origin; + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref("network.cookie.cookieBehavior"); + + let success = true; + // We only grant storage exceptions when the reject tracker behavior is enabled. + let rejectTrackers = + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + SpecialPowers.Ci.nsICookieService + .BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ].includes(effectiveCookieBehavior) && !isOnContentBlockingAllowList(); + const TEST_ANOTHER_3RD_PARTY_ORIGIN = SpecialPowers.useRemoteSubframes + ? "http://another-tracking.example.net" + : "https://another-tracking.example.net"; + // With another-tracking.example.net, we're same-eTLD+1, so the first try succeeds. + if (origin != TEST_ANOTHER_3RD_PARTY_ORIGIN) { + if (rejectTrackers) { + let p; + let threw = false; + try { + p = document.requestStorageAccess(); + } catch (e) { + threw = true; + } + ok(!threw, "requestStorageAccess should not throw"); + try { + if (callback) { + if (expectFail) { + await p.catch(_ => callback()); + success = false; + } else { + await p.then(_ => callback()); + } + } else { + await p; + } + } catch (e) { + success = false; + } finally { + SpecialPowers.wrap(document).clearUserGestureActivation(); + } + ok(!success, "Should not have worked without user interaction"); + + await noStorageAccessInitially(); + + await interactWithTracker(); + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + } + if ( + effectiveCookieBehavior == + SpecialPowers.Ci.nsICookieService.BEHAVIOR_ACCEPT && + !isOnContentBlockingAllowList() + ) { + try { + if (callback) { + if (expectFail) { + await document.requestStorageAccess().catch(_ => callback()); + success = false; + } else { + await document.requestStorageAccess().then(_ => callback()); + } + } else { + await document.requestStorageAccess(); + } + } catch (e) { + success = false; + } finally { + SpecialPowers.wrap(document).clearUserGestureActivation(); + } + ok(success, "Should not have thrown"); + + await hasStorageAccessInitially(); + + await interactWithTracker(); + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + } + } + + let p; + let threw = false; + try { + p = document.requestStorageAccess(); + } catch (e) { + threw = true; + } + let rejected = false; + try { + if (callback) { + if (expectFail) { + await p.catch(_ => callback()); + rejected = true; + } else { + await p.then(_ => callback()); + } + } else { + await p; + } + } catch (e) { + rejected = true; + } finally { + SpecialPowers.wrap(document).clearUserGestureActivation(); + } + + success = !threw && !rejected; + let hasAccess = await document.hasStorageAccess(); + is( + hasAccess, + success, + "Should " + (success ? "" : "not ") + "have storage access now" + ); + if ( + success && + rejectTrackers && + window.location.search != "?disableWaitUntilPermission" && + origin != TEST_ANOTHER_3RD_PARTY_ORIGIN + ) { + let protocol = isSecureContext ? "https" : "http"; + // Wait until the permission is visible in parent process to avoid race + // conditions. We don't need to wait the permission to be visible in content + // processes since the content process doesn't rely on the permission to + // know the storage access is updated. + let originURI = SpecialPowers.Services.io.newURI(window.origin); + let site = SpecialPowers.Services.eTLD.getSite(originURI); + await waitUntilPermission( + `${protocol}://example.net/browser/toolkit/components/antitracking/test/browser/page.html`, + "3rdPartyFrameStorage^" + site + ); + } + + return [threw, rejected]; +} + +async function waitUntilPermission( + url, + name, + value = SpecialPowers.Services.perms.ALLOW_ACTION +) { + let originAttributes = SpecialPowers.isContentWindowPrivate(window) + ? { privateBrowsingId: 1 } + : {}; + await new Promise(resolve => { + let id = setInterval(async _ => { + if ( + await SpecialPowers.testPermission(name, value, { + url, + originAttributes, + }) + ) { + clearInterval(id); + resolve(); + } + }, 0); + }); +} + +async function interactWithTracker() { + await new Promise(resolve => { + let orionmessage = onmessage; + onmessage = _ => { + onmessage = orionmessage; + resolve(); + }; + + info("Let's interact with the tracker"); + window.open( + "/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html?messageme" + ); + }); + + // Wait until the user interaction permission becomes visible in our process + await waitUntilPermission(window.origin, "storageAccessAPI"); +} + +function isOnContentBlockingAllowList() { + // We directly check the window.allowListed here instead of checking the + // permission. The allow list permission might not be available since it is + // not in the preload list. + + return window.allowListed; +} + +async function registerServiceWorker(win, url) { + let reg = await win.navigator.serviceWorker.register(url); + if (reg.installing.state !== "activated") { + await new Promise(resolve => { + let w = reg.installing; + w.addEventListener("statechange", function onStateChange() { + if (w.state === "activated") { + w.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + } + + return reg.active; +} + +function sendAndWaitWorkerMessage(target, worker, message) { + return new Promise(resolve => { + worker.addEventListener( + "message", + msg => { + resolve(msg.data); + }, + { once: true } + ); + + target.postMessage(message); + }); +} diff --git a/toolkit/components/antitracking/test/browser/storage_access_head.js b/toolkit/components/antitracking/test/browser/storage_access_head.js new file mode 100644 index 0000000000..ea4f67b4fe --- /dev/null +++ b/toolkit/components/antitracking/test/browser/storage_access_head.js @@ -0,0 +1,281 @@ +/* import-globals-from ../../../../../browser/modules/test/browser/head.js */ +/* import-globals-from antitracking_head.js */ + +async function openPageAndRunCode( + topPage, + topPageCallback, + embeddedPage, + embeddedPageCallback +) { + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: topPage, + waitForLoad: true, + }); + let browser = gBrowser.getBrowserForTab(tab); + + await topPageCallback(); + await SpecialPowers.spawn( + browser, + [{ page: embeddedPage, callback: embeddedPageCallback.toString() }], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + ifr.contentWindow.postMessage(obj.callback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + await BrowserTestUtils.removeTab(tab); +} + +// This function returns a function that spawns an asynchronous task to handle +// the popup and click on the appropriate values. If that task is never executed +// the catch case is reached and we fail the test. If for some reason that catch +// case isn't reached, having an extra event listener at the end of the test +// will cause the test to fail anyway. +// Note: this means that tests that use this callback should probably be in +// their own test file. +function getExpectPopupAndClick(accept) { + return function () { + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + shownPromise + .then(async _ => { + // This occurs when the promise resolves on the test finishing + let popupNotifications = PopupNotifications.panel.childNodes; + if (!popupNotifications.length) { + ok(false, "Prompt did not show up"); + } else if (accept == "accept") { + ok(true, "Prompt shows up, clicking accept."); + await clickMainAction(); + } else if (accept == "reject") { + ok(true, "Prompt shows up, clicking reject."); + await clickSecondaryAction(); + } else { + ok(false, "Unknown accept value for test: " + accept); + info("Clicking accept so that the test can finish."); + await clickMainAction(); + } + }) + .catch(() => { + ok(false, "Prompt did not show up"); + }); + }; +} + +// Click popup after a delay of {timeout} ms +function getExpectPopupAndClickAfterDelay(accept, timeout) { + return function () { + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + shownPromise + .then( + setTimeout(async _ => { + // This occurs when the promise resolves on the test finishing + let popupNotifications = PopupNotifications.panel.childNodes; + if (!popupNotifications.length) { + ok(false, "Prompt did not show up"); + } else if (accept == "accept") { + ok(true, "Prompt shows up, clicking accept."); + await clickMainAction(); + } else if (accept == "reject") { + ok(true, "Prompt shows up, clicking reject."); + await clickSecondaryAction(); + } else { + ok(false, "Unknown accept value for test: " + accept); + info("Clicking accept so that the test can finish."); + await clickMainAction(); + } + }, timeout) + ) + .catch(() => { + ok(false, "Prompt did not show up"); + }); + }; +} + +// This function spawns an asynchronous task that fails the test if a popup +// appears. If that never happens, the catch case is executed on the test +// cleanup. +// Note: this means that tests that use this callback should probably be in +// their own test file. +function expectNoPopup() { + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + shownPromise + .then(async _ => { + // This occurs when the promise resolves on the test finishing + let popupNotifications = PopupNotifications.panel.childNodes; + if (!popupNotifications.length) { + ok(true, "Prompt did not show up"); + } else { + ok(false, "Prompt shows up"); + info(PopupNotifications.panel); + await clickSecondaryAction(); + } + }) + .catch(() => { + ok(true, "Prompt did not show up"); + }); +} + +async function requestStorageAccessAndExpectSuccess() { + const aps = SpecialPowers.Services.prefs.getBoolPref( + "privacy.partition.always_partition_third_party_non_cookie_storage" + ); + + // When always partitioning storage, we do not clear non-cookie storage + // after a requestStorageAccess is accepted by the user. So here we test + // that indexedDB is cleared when the pref is off, but not when it is on. + await new Promise((resolve, reject) => { + const db = window.indexedDB.open("rSATest", 1); + db.onupgradeneeded = resolve; + db.success = resolve; + db.onerror = reject; + }); + + const hadAccessAlready = await document.hasStorageAccess(); + const shouldClearIDB = !aps && !hadAccessAlready; + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + let p = document.requestStorageAccess(); + try { + await p; + ok(true, "gain storage access."); + } catch { + ok(false, "denied storage access."); + } + + await new Promise((resolve, reject) => { + const req = window.indexedDB.open("rSATest", 1); + req.onerror = reject; + req.onupgradeneeded = () => { + ok(shouldClearIDB, "iDB was cleared"); + req.onsuccess = undefined; + resolve(); + }; + req.onsuccess = () => { + ok(!shouldClearIDB, "iDB was not cleared"); + resolve(); + }; + }); + + await new Promise(resolve => { + const req = window.indexedDB.deleteDatabase("rSATest"); + req.onsuccess = resolve; + req.onerror = resolve; + }); + + SpecialPowers.wrap(document).clearUserGestureActivation(); +} + +async function requestStorageAccessAndExpectFailure() { + // When always partitioning storage, we do not clear non-cookie storage + // after a requestStorageAccess is accepted by the user. So here we test + // that indexedDB is cleared when the pref is off, but not when it is on. + await new Promise((resolve, reject) => { + const db = window.indexedDB.open("rSATest", 1); + db.onupgradeneeded = resolve; + db.success = resolve; + db.onerror = reject; + }); + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + let p = document.requestStorageAccess(); + try { + await p; + ok(false, "gain storage access."); + } catch { + ok(true, "denied storage access."); + } + + await new Promise((resolve, reject) => { + const req = window.indexedDB.open("rSATest", 1); + req.onerror = reject; + req.onupgradeneeded = () => { + ok(false, "iDB was cleared"); + req.onsuccess = undefined; + resolve(); + }; + req.onsuccess = () => { + ok(true, "iDB was not cleared"); + resolve(); + }; + }); + + await new Promise(resolve => { + const req = window.indexedDB.deleteDatabase("rSATest"); + req.onsuccess = resolve; + req.onerror = resolve; + }); + + SpecialPowers.wrap(document).clearUserGestureActivation(); +} + +async function cleanUpData() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + ok(true, "Deleted all data."); +} + +async function setPreferences(alwaysPartitionStorage = true) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", true], + ["dom.storage_access.auto_grants.delayed", false], + ["dom.storage_access.enabled", true], + ["dom.storage_access.max_concurrent_auto_grants", 0], + ["dom.storage_access.prompt.testing", false], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + alwaysPartitionStorage, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); +} diff --git a/toolkit/components/antitracking/test/browser/subResources.sjs b/toolkit/components/antitracking/test/browser/subResources.sjs new file mode 100644 index 0000000000..f40d5cac97 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/subResources.sjs @@ -0,0 +1,33 @@ +// A 1x1 PNG image. +// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" +); + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + let key = aRequest.queryString.includes("what=script") ? "script" : "image"; + + if (aRequest.queryString.includes("result")) { + aResponse.write(getState(key) || 0); + setState(key, "0"); + return; + } + + if (aRequest.hasHeader("Cookie")) { + let hints = parseInt(getState(key) || 0) + 1; + setState(key, hints.toString()); + } + + aResponse.setHeader("Set-Cookie", "foopy=1"); + + if (key == "script") { + aResponse.setHeader("Content-Type", "text/javascript", false); + aResponse.write("42;"); + } else { + aResponse.setHeader("Content-Type", "image/png", false); + aResponse.write(IMAGE); + } +} diff --git a/toolkit/components/antitracking/test/browser/tracker.js b/toolkit/components/antitracking/test/browser/tracker.js new file mode 100644 index 0000000000..85e943f7c4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/tracker.js @@ -0,0 +1,7 @@ +window.addEventListener("message", e => { + let bc = new BroadcastChannel("a"); + bc.postMessage("ready!"); +}); +window.open( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpen.html" +); diff --git a/toolkit/components/antitracking/test/browser/trackingAPI.js b/toolkit/components/antitracking/test/browser/trackingAPI.js new file mode 100644 index 0000000000..496ae3db08 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/trackingAPI.js @@ -0,0 +1,77 @@ +function createIframe(src) { + let ifr = document.createElement("iframe"); + ifr.src = src; + document.body.appendChild(ifr); +} + +function createImage(src) { + let img = document.createElement("img"); + img.src = src; + img.onload = () => { + parent.postMessage("done", "*"); + }; + document.body.appendChild(img); +} + +onmessage = event => { + switch (event.data) { + case "tracking": + createIframe("https://trackertest.org/"); + break; + case "socialtracking": + createIframe( + "https://social-tracking.example.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs" + ); + break; + case "cryptomining": + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + createIframe("http://cryptomining.example.com/"); + break; + case "fingerprinting": + createIframe("https://fingerprinting.example.com/"); + break; + case "more-tracking": + createIframe("https://itisatracker.org/"); + break; + case "more-tracking-2": + createIframe("https://tracking.example.com/"); + break; + case "cookie": + createIframe( + "https://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs" + ); + break; + case "first-party-cookie": + // Since the content blocking log doesn't seem to get updated for + // top-level cookies right now, we just create an iframe with the + // first party domain... + createIframe( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://not-tracking.example.com/browser/browser/base/content/test/protectionsUI/cookieServer.sjs" + ); + break; + case "third-party-cookie": + createIframe( + "https://test1.example.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs" + ); + break; + case "image": + createImage( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "https://itisatracker.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs?type=image-no-cookie" + ); + break; + case "window-open": + window.win = window.open( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://trackertest.org/browser/browser/base/content/test/protectionsUI/cookieServer.sjs", + "_blank", + "width=100,height=100" + ); + break; + case "window-close": + window.win.close(); + window.win = null; + break; + } +}; diff --git a/toolkit/components/antitracking/test/browser/trackingPage.html b/toolkit/components/antitracking/test/browser/trackingPage.html new file mode 100644 index 0000000000..0dbb4ca274 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/trackingPage.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<!-- 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/. --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + <script src="trackingAPI.js" type="text/javascript"></script> + </head> + <body> + <iframe src="https://itisatracker.org/"></iframe> + </body> +</html> diff --git a/toolkit/components/antitracking/test/browser/workerIframe.html b/toolkit/components/antitracking/test/browser/workerIframe.html new file mode 100644 index 0000000000..37aa5d7c0d --- /dev/null +++ b/toolkit/components/antitracking/test/browser/workerIframe.html @@ -0,0 +1,71 @@ +<html> +<head> + <title>3rd party content!</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Here the 3rd party content!</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +async function runTest() { + function workerCode() { + onmessage = e => { + try { + indexedDB.open("test", "1"); + postMessage(true); + } catch (e) { + postMessage(false); + } + }; + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + info("Initialized"); + + let blob = new Blob([workerCode.toString() + "; workerCode();"]); + let blobURL = URL.createObjectURL(blob); + info("Blob created"); + + let w = new Worker(blobURL); + info("Worker created"); + + await new Promise(resolve => { + w.addEventListener("message", e => { + ok(!e.data, "IDB is disabled"); + resolve(); + }, { once: true }); + w.postMessage("go"); + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + await new Promise(resolve => { + w.addEventListener("message", e => { + ok(e.data, "IDB is enabled"); + resolve(); + }, { once: true }); + w.postMessage("go"); + }); + + parent.postMessage({ type: "finish" }, "*"); +} + +runTest(); + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/gtest/TestPartitioningExceptionList.cpp b/toolkit/components/antitracking/test/gtest/TestPartitioningExceptionList.cpp new file mode 100644 index 0000000000..1aa0614622 --- /dev/null +++ b/toolkit/components/antitracking/test/gtest/TestPartitioningExceptionList.cpp @@ -0,0 +1,184 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "gtest/gtest.h" + +#include "mozilla/Preferences.h" +#include "mozilla/PartitioningExceptionList.h" + +using namespace mozilla; + +static const char kPrefPartitioningExceptionList[] = + "privacy.restrict3rdpartystorage.skip_list"; + +static const char kPrefEnableWebcompat[] = + "privacy.antitracking.enableWebcompat"; + +TEST(TestPartitioningExceptionList, TestPrefBasic) +{ + nsAutoCString oldPartitioningExceptionList; + Preferences::GetCString(kPrefEnableWebcompat, oldPartitioningExceptionList); + bool oldEnableWebcompat = Preferences::GetBool(kPrefEnableWebcompat); + + for (uint32_t populateList = 0; populateList <= 1; populateList++) { + for (uint32_t enableWebcompat = 0; enableWebcompat <= 1; + enableWebcompat++) { + if (populateList) { + Preferences::SetCString(kPrefPartitioningExceptionList, + "https://example.com,https://example.net"); + } else { + Preferences::SetCString(kPrefPartitioningExceptionList, ""); + } + + Preferences::SetBool(kPrefEnableWebcompat, enableWebcompat); + + EXPECT_FALSE( + PartitioningExceptionList::Check(""_ns, "https://example.net"_ns)); + EXPECT_FALSE( + PartitioningExceptionList::Check("https://example.com"_ns, ""_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check(""_ns, ""_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.net"_ns, + "https://example.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.org"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("http://example.com"_ns, + "http://example.net"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "http://example.net"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com."_ns, + "https://example.net"_ns)); + + bool result = PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns); + EXPECT_TRUE(result == (populateList && enableWebcompat)); + } + } + + Preferences::SetCString(kPrefPartitioningExceptionList, + oldPartitioningExceptionList); + Preferences::SetBool(kPrefEnableWebcompat, oldEnableWebcompat); +} + +TEST(TestPartitioningExceptionList, TestPrefWildcard) +{ + nsAutoCString oldPartitioningExceptionList; + Preferences::GetCString(kPrefEnableWebcompat, oldPartitioningExceptionList); + bool oldEnableWebcompat = Preferences::GetBool(kPrefEnableWebcompat); + + Preferences::SetCString(kPrefPartitioningExceptionList, + "https://example.com,https://example.net;" + "https://*.foo.com,https://bar.com;" + "https://*.foo.com,https://foobar.net;" + "https://test.net,https://*.example.com;" + "https://test.com,https://*.example.com;" + "https://*.test2.org,*;" + "*,http://notatracker.org"); + + Preferences::SetBool(kPrefEnableWebcompat, true); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://two.foo.com"_ns, + "https://bar.com"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check("https://another.foo.com"_ns, + "https://bar.com"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check("https://three.two.foo.com"_ns, + "https://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://two.foo.com"_ns, + "https://example.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://foo.com"_ns, + "https://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://two.foo.com"_ns, + "http://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("http://two.foo.com"_ns, + "https://bar.com"_ns)); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://a.foo.com"_ns, + "https://foobar.net"_ns)); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://test.net"_ns, + "https://test.example.com"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check( + "https://test.net"_ns, "https://foo.bar.example.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://test.com"_ns, + "https://foo.test.net"_ns)); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://one.test2.org"_ns, + "https://example.net"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check("https://two.test2.org"_ns, + "https://foo.example.net"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check("https://three.test2.org"_ns, + "http://example.net"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check("https://four.sub.test2.org"_ns, + "https://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://four.sub.test2.com"_ns, + "https://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("http://four.sub.test2.org"_ns, + "https://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check( + "https://four.sub.test2.org."_ns, "https://bar.com"_ns)); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://example.com"_ns, + "http://notatracker.org"_ns)); + + Preferences::SetCString(kPrefPartitioningExceptionList, + oldPartitioningExceptionList); + Preferences::SetBool(kPrefEnableWebcompat, oldEnableWebcompat); +} + +TEST(TestPartitioningExceptionList, TestInvalidEntries) +{ + nsAutoCString oldPartitioningExceptionList; + Preferences::GetCString(kPrefEnableWebcompat, oldPartitioningExceptionList); + bool oldEnableWebcompat = Preferences::GetBool(kPrefEnableWebcompat); + + Preferences::SetBool(kPrefEnableWebcompat, true); + + // Empty entries. + Preferences::SetCString(kPrefPartitioningExceptionList, ";;;,;"); + + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + + // Schemeless entries. + Preferences::SetCString(kPrefPartitioningExceptionList, + "example.com,example.net"); + + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + + // Invalid entry should be skipped and not break other entries. + Preferences::SetCString(kPrefPartitioningExceptionList, + "*,*;" + "https://example.com,https://example.net;" + "http://example.org,"); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://foo.com"_ns, + "https://bar.net"_ns)); + + // Unsupported schemes should not be accepted. + Preferences::SetCString(kPrefPartitioningExceptionList, + "ftp://example.com,ftp://example.net;"); + + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("ftp://example.com"_ns, + "ftp://example.net"_ns)); + + // Test invalid origins with trailing '/'. + Preferences::SetCString(kPrefPartitioningExceptionList, + "https://example.com/,https://example.net/"); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + + Preferences::SetCString(kPrefPartitioningExceptionList, + oldPartitioningExceptionList); + Preferences::SetBool(kPrefEnableWebcompat, oldEnableWebcompat); +} diff --git a/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp b/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp new file mode 100644 index 0000000000..44959e4dc8 --- /dev/null +++ b/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp @@ -0,0 +1,215 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "gtest/gtest.h" + +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIContentPolicy.h" +#include "nsICookieJarSettings.h" +#include "nsILoadInfo.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsStringFwd.h" + +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/Preferences.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StoragePrincipalHelper.h" + +using mozilla::Preferences; +using namespace mozilla; + +/** + * Creates a test channel with CookieJarSettings which have a partitionKey set. + */ +nsresult CreateMockChannel(nsIPrincipal* aPrincipal, bool isThirdParty, + const nsACString& aPartitionKey, + nsIChannel** aChannel, + nsICookieJarSettings** aCookieJarSettings) { + nsCOMPtr<nsIURI> mockUri; + nsresult rv = NS_NewURI(getter_AddRefs(mockUri), "http://example.com"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsIIOService> service = do_GetIOService(&rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = service->NewChannelFromURI(mockUri, nullptr, aPrincipal, aPrincipal, 0, + nsContentPolicyType::TYPE_OTHER, + getter_AddRefs(mockChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> mockLoadInfo = mockChannel->LoadInfo(); + rv = mockLoadInfo->SetIsThirdPartyContextToTopWindow(isThirdParty); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsICookieJarSettings> cjs; + rv = mockLoadInfo->GetCookieJarSettings(getter_AddRefs(cjs)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> partitionKeyUri; + rv = NS_NewURI(getter_AddRefs(partitionKeyUri), aPartitionKey); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cjs->InitWithURI(partitionKeyUri, false); + NS_ENSURE_SUCCESS(rv, rv); + + cjs.forget(aCookieJarSettings); + mockChannel.forget(aChannel); + return NS_OK; +} + +TEST(TestStoragePrincipalHelper, TestCreateContentPrincipal) +{ + nsCOMPtr<nsIPrincipal> contentPrincipal = + BasePrincipal::CreateContentPrincipal("https://example.com"_ns); + EXPECT_TRUE(contentPrincipal); + + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = CreateMockChannel( + contentPrincipal, false, "https://example.org"_ns, + getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings)); + ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel"; + + nsCOMPtr<nsIPrincipal> storagePrincipal; + rv = StoragePrincipalHelper::Create(mockChannel, contentPrincipal, true, + getter_AddRefs(storagePrincipal)); + ASSERT_EQ(rv, NS_OK) << "Should not fail for ContentPrincipal"; + EXPECT_TRUE(storagePrincipal); + + nsCOMPtr<nsIPrincipal> storagePrincipalSW; + rv = StoragePrincipalHelper::CreatePartitionedPrincipalForServiceWorker( + contentPrincipal, cookieJarSettings, getter_AddRefs(storagePrincipalSW)); + ASSERT_EQ(rv, NS_OK) << "Should not fail for ContentPrincipal"; + EXPECT_TRUE(storagePrincipalSW); +} + +TEST(TestStoragePrincipalHelper, TestCreateNullPrincipal) +{ + RefPtr<NullPrincipal> nullPrincipal = + NullPrincipal::CreateWithoutOriginAttributes(); + EXPECT_TRUE(nullPrincipal); + + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = CreateMockChannel( + nullPrincipal, false, "https://example.org"_ns, + getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings)); + ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel"; + + nsCOMPtr<nsIPrincipal> storagePrincipal; + rv = StoragePrincipalHelper::Create(mockChannel, nullPrincipal, true, + getter_AddRefs(storagePrincipal)); + EXPECT_NS_FAILED(rv) << "Should fail for NullPrincipal"; + EXPECT_FALSE(storagePrincipal); + + nsCOMPtr<nsIPrincipal> storagePrincipalSW; + rv = StoragePrincipalHelper::CreatePartitionedPrincipalForServiceWorker( + nullPrincipal, cookieJarSettings, getter_AddRefs(storagePrincipalSW)); + EXPECT_NS_FAILED(rv) << "Should fail for NullPrincipal"; + EXPECT_FALSE(storagePrincipal); +} + +TEST(TestStoragePrincipalHelper, TestGetPrincipalCookieBehavior4) +{ + Preferences::SetInt("network.cookie.cookieBehavior", 4); + + nsCOMPtr<nsIPrincipal> contentPrincipal = + BasePrincipal::CreateContentPrincipal("https://example.com"_ns); + EXPECT_TRUE(contentPrincipal); + + for (auto isThirdParty : {false, true}) { + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = CreateMockChannel( + contentPrincipal, isThirdParty, "https://example.org"_ns, + getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings)); + ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel"; + + nsCOMPtr<nsIPrincipal> testPrincipal; + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::eRegularPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get regular principal"; + EXPECT_TRUE(testPrincipal); + EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()); + + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::ePartitionedPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get partitioned principal"; + EXPECT_TRUE(testPrincipal); + EXPECT_TRUE( + testPrincipal->OriginAttributesRef().mPartitionKey.EqualsLiteral( + "(https,example.org)")); + + // We should always get regular principal if the dFPI is disabled. + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::eForeignPartitionedPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get foreign partitioned principal"; + EXPECT_TRUE(testPrincipal); + EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()); + + // Note that we don't test eStorageAccessPrincipal here because it's hard to + // setup the right state for the storage access in gTest. + } +} + +TEST(TestStoragePrincipalHelper, TestGetPrincipalCookieBehavior5) +{ + Preferences::SetInt("network.cookie.cookieBehavior", 5); + + nsCOMPtr<nsIPrincipal> contentPrincipal = + BasePrincipal::CreateContentPrincipal("https://example.com"_ns); + EXPECT_TRUE(contentPrincipal); + + for (auto isThirdParty : {false, true}) { + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = CreateMockChannel( + contentPrincipal, isThirdParty, "https://example.org"_ns, + getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings)); + ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel"; + + nsCOMPtr<nsIPrincipal> testPrincipal; + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::eRegularPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get regular principal"; + EXPECT_TRUE(testPrincipal); + EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()); + + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::ePartitionedPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get partitioned principal"; + EXPECT_TRUE(testPrincipal); + EXPECT_TRUE( + testPrincipal->OriginAttributesRef().mPartitionKey.EqualsLiteral( + "(https,example.org)")); + + // We should always get regular principal if the dFPI is disabled. + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::eForeignPartitionedPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get foreign partitioned principal"; + EXPECT_TRUE(testPrincipal); + if (isThirdParty) { + EXPECT_TRUE( + testPrincipal->OriginAttributesRef().mPartitionKey.EqualsLiteral( + "(https,example.org)")); + } else { + EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()); + } + + // Note that we don't test eStorageAccessPrincipal here because it's hard to + // setup the right state for the storage access in gTest. + } +} diff --git a/toolkit/components/antitracking/test/gtest/TestURLQueryStringStripper.cpp b/toolkit/components/antitracking/test/gtest/TestURLQueryStringStripper.cpp new file mode 100644 index 0000000000..3f77479f74 --- /dev/null +++ b/toolkit/components/antitracking/test/gtest/TestURLQueryStringStripper.cpp @@ -0,0 +1,176 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "gtest/gtest.h" + +#include "mozilla/Components.h" +#include "nsIURLQueryStringStripper.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsStringFwd.h" + +#include "mozilla/Preferences.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/URLQueryStringStripper.h" + +using namespace mozilla; + +static const char kPrefQueryStrippingEnabled[] = + "privacy.query_stripping.enabled"; +static const char kPrefQueryStrippingEnabledPBM[] = + "privacy.query_stripping.enabled.pbmode"; +static const char kPrefQueryStrippingList[] = + "privacy.query_stripping.strip_list"; + +/** + * Waits for the strip list in the URLQueryStringStripper to match aExpected. + */ +void waitForStripListChange(const nsACString& aExpected) { + nsresult rv; + nsCOMPtr<nsIURLQueryStringStripper> queryStripper = + components::URLQueryStringStripper::Service(&rv); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "TestURLQueryStringStripper waitForStripListChange"_ns, [&]() -> bool { + nsAutoCString stripList; + rv = queryStripper->TestGetStripList(stripList); + return NS_SUCCEEDED(rv) && stripList.Equals(aExpected); + })); +} + +void DoTest(const nsACString& aTestURL, const bool aIsPBM, + const nsACString& aExpectedURL, uint32_t aExpectedResult) { + nsCOMPtr<nsIURI> testURI; + + NS_NewURI(getter_AddRefs(testURI), aTestURL); + + nsresult rv; + nsCOMPtr<nsIURLQueryStringStripper> queryStripper = + components::URLQueryStringStripper::Service(&rv); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIURI> strippedURI; + uint32_t numStripped; + rv = queryStripper->Strip(testURI, aIsPBM, getter_AddRefs(strippedURI), + &numStripped); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + EXPECT_TRUE(numStripped == aExpectedResult); + + if (!numStripped) { + EXPECT_TRUE(!strippedURI); + } else { + EXPECT_TRUE(strippedURI->GetSpecOrDefault().Equals(aExpectedURL)); + } +} + +TEST(TestURLQueryStringStripper, TestPrefDisabled) +{ + // Disable the query string stripping by the pref and make sure the stripping + // is disabled. + // Note that we don't need to run a dummy test to create the + // URLQueryStringStripper here because the stripper will never be created if + // the query stripping is disabled. + Preferences::SetCString(kPrefQueryStrippingList, "fooBar foobaz"); + Preferences::SetBool(kPrefQueryStrippingEnabled, false); + Preferences::SetBool(kPrefQueryStrippingEnabledPBM, false); + + for (bool isPBM : {false, true}) { + DoTest("https://example.com/"_ns, isPBM, ""_ns, 0); + DoTest("https://example.com/?Barfoo=123"_ns, isPBM, ""_ns, 0); + DoTest("https://example.com/?fooBar=123&foobaz"_ns, isPBM, ""_ns, 0); + } +} + +TEST(TestURLQueryStringStripper, TestEmptyStripList) +{ + // Make sure there is no error if the strip list is empty. + Preferences::SetBool(kPrefQueryStrippingEnabled, true); + Preferences::SetBool(kPrefQueryStrippingEnabledPBM, true); + + // To create the URLQueryStringStripper, we need to run a dummy test after + // the query stripping is enabled. By doing this, the stripper will be + // initiated and we are good to test. + DoTest("https://example.com/"_ns, false, ""_ns, 0); + + // Set the strip list to empty and wait until the pref setting is set to the + // stripper. + Preferences::SetCString(kPrefQueryStrippingList, ""); + + waitForStripListChange(""_ns); + + for (bool isPBM : {false, true}) { + DoTest("https://example.com/"_ns, isPBM, ""_ns, 0); + DoTest("https://example.com/?Barfoo=123"_ns, isPBM, ""_ns, 0); + DoTest("https://example.com/?fooBar=123&foobaz"_ns, isPBM, ""_ns, 0); + } +} + +TEST(TestURLQueryStringStripper, TestStripping) +{ + Preferences::SetBool(kPrefQueryStrippingEnabled, true); + Preferences::SetBool(kPrefQueryStrippingEnabledPBM, true); + DoTest("https://example.com/"_ns, false, ""_ns, 0); + + Preferences::SetCString(kPrefQueryStrippingList, "fooBar foobaz"); + waitForStripListChange("foobar foobaz"_ns); + + // Test all pref combinations. + for (bool pref : {false, true}) { + for (bool prefPBM : {false, true}) { + Preferences::SetBool(kPrefQueryStrippingEnabled, pref); + Preferences::SetBool(kPrefQueryStrippingEnabledPBM, prefPBM); + + // If the service is enabled with the given pref config we need for the + // list changes to propagate as they happen async. + if (pref || prefPBM) { + waitForStripListChange("foobar foobaz"_ns); + } + + // Test with normal and private browsing mode. + for (bool isPBM : {false, true}) { + bool expectStrip = (prefPBM && isPBM) || (pref && !isPBM); + + DoTest("https://example.com/"_ns, isPBM, ""_ns, 0); + DoTest("https://example.com/?Barfoo=123"_ns, isPBM, ""_ns, 0); + + DoTest("https://example.com/?fooBar=123"_ns, isPBM, + "https://example.com/"_ns, expectStrip ? 1 : 0); + DoTest("https://example.com/?fooBar=123&foobaz"_ns, isPBM, + "https://example.com/"_ns, expectStrip ? 2 : 0); + DoTest("https://example.com/?fooBar=123&Barfoo=456&foobaz"_ns, isPBM, + "https://example.com/?Barfoo=456"_ns, expectStrip ? 2 : 0); + + DoTest("https://example.com/?FOOBAR=123"_ns, isPBM, + "https://example.com/"_ns, expectStrip ? 1 : 0); + DoTest("https://example.com/?barfoo=foobar"_ns, isPBM, + "https://example.com/?barfoo=foobar"_ns, 0); + DoTest("https://example.com/?foobar=123&nostrip=456&FooBar=789"_ns, + isPBM, "https://example.com/?nostrip=456"_ns, + expectStrip ? 2 : 0); + DoTest("https://example.com/?AfoobazB=123"_ns, isPBM, + "https://example.com/?AfoobazB=123"_ns, 0); + } + } + } + + // Change the strip list pref to see if it is updated properly. + // We test this in normal browsing, so set the prefs accordingly. + Preferences::SetBool(kPrefQueryStrippingEnabled, true); + Preferences::SetBool(kPrefQueryStrippingEnabledPBM, false); + + Preferences::SetCString(kPrefQueryStrippingList, "Barfoo bazfoo"); + + waitForStripListChange("barfoo bazfoo"_ns); + + DoTest("https://example.com/?fooBar=123"_ns, false, ""_ns, 0); + DoTest("https://example.com/?fooBar=123&foobaz"_ns, false, ""_ns, 0); + + DoTest("https://example.com/?bazfoo=123"_ns, false, "https://example.com/"_ns, + 1); + DoTest("https://example.com/?fooBar=123&Barfoo=456&foobaz=abc"_ns, false, + "https://example.com/?fooBar=123&foobaz=abc"_ns, 1); +} diff --git a/toolkit/components/antitracking/test/gtest/moz.build b/toolkit/components/antitracking/test/gtest/moz.build new file mode 100644 index 0000000000..a7e8595c5e --- /dev/null +++ b/toolkit/components/antitracking/test/gtest/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + "TestPartitioningExceptionList.cpp", + "TestStoragePrincipalHelper.cpp", + "TestURLQueryStringStripper.cpp", +] + +LOCAL_INCLUDES += [ + "/xpcom/tests/gtest", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/components/antitracking/test/xpcshell/data/font.woff b/toolkit/components/antitracking/test/xpcshell/data/font.woff Binary files differnew file mode 100644 index 0000000000..acda4f3d9f --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/data/font.woff diff --git a/toolkit/components/antitracking/test/xpcshell/head.js b/toolkit/components/antitracking/test/xpcshell/head.js new file mode 100644 index 0000000000..f9bf797641 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/head.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from ../../../../components/url-classifier/tests/unit/head_urlclassifier.js */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); diff --git a/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js b/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js new file mode 100644 index 0000000000..4063d067f5 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js @@ -0,0 +1,107 @@ +// This test ensures that the URL decoration annotations service works as +// expected, and also we successfully downgrade document.referrer to the +// eTLD+1 URL when tracking identifiers controlled by this service are +// present in the referrer URI. + +"use strict"; + +/* Unit tests for the nsIPartitioningExceptionListService implementation. */ + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +const COLLECTION_NAME = "partitioning-exempt-urls"; +const PREF_NAME = "privacy.restrict3rdpartystorage.skip_list"; + +do_get_profile(); + +class UpdateEvent extends EventTarget {} +function waitForEvent(element, eventName) { + return new Promise(function (resolve) { + element.addEventListener(eventName, e => resolve(e.detail), { once: true }); + }); +} + +add_task(async _ => { + let peuService = Cc[ + "@mozilla.org/partitioning/exception-list-service;1" + ].getService(Ci.nsIPartitioningExceptionListService); + + // Make sure we have a pref initially, since the exception list service + // requires it. + Services.prefs.setStringPref(PREF_NAME, ""); + + let updateEvent = new UpdateEvent(); + let records = [ + { + id: "1", + last_modified: 1000000000000001, + firstPartyOrigin: "https://example.org", + thirdPartyOrigin: "https://tracking.example.com", + }, + ]; + + // Add some initial data + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), records); + + let promise = waitForEvent(updateEvent, "update"); + let obs = data => { + let event = new CustomEvent("update", { detail: data }); + updateEvent.dispatchEvent(event); + }; + peuService.registerAndRunExceptionListObserver(obs); + let list = await promise; + Assert.equal(list, "", "No items in the list"); + + // Second event is from the RemoteSettings record. + list = await waitForEvent(updateEvent, "update"); + Assert.equal( + list, + "https://example.org,https://tracking.example.com", + "Has one item in the list" + ); + + records.push({ + id: "2", + last_modified: 1000000000000002, + firstPartyOrigin: "https://foo.org", + thirdPartyOrigin: "https://bar.com", + }); + + promise = waitForEvent(updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { current: records }, + }); + list = await promise; + Assert.equal( + list, + "https://example.org,https://tracking.example.com;https://foo.org,https://bar.com", + "Has several items in the list" + ); + + promise = waitForEvent(updateEvent, "update"); + Services.prefs.setStringPref(PREF_NAME, "https://test.com,https://test3.com"); + list = await promise; + Assert.equal( + list, + "https://test.com,https://test3.com;https://example.org,https://tracking.example.com;https://foo.org,https://bar.com", + "Has several items in the list" + ); + + promise = waitForEvent(updateEvent, "update"); + Services.prefs.setStringPref( + PREF_NAME, + "https://test.com,https://test3.com;https://abc.com,https://def.com" + ); + list = await promise; + Assert.equal( + list, + "https://test.com,https://test3.com;https://abc.com,https://def.com;https://example.org,https://tracking.example.com;https://foo.org,https://bar.com", + "Has several items in the list" + ); + + peuService.unregisterExceptionListObserver(obs); + await db.clear(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js b/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js new file mode 100644 index 0000000000..3ce1f8bfb7 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js @@ -0,0 +1,94 @@ +/* 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/. */ + +// Note: This test may cause intermittents if run at exactly midnight. + +"use strict"; + +const PREF_FPI = "privacy.firstparty.isolate"; +const PREF_COOKIE_BEHAVIOR = "network.cookie.cookieBehavior"; +const PREF_COOKIE_BEHAVIOR_PBMODE = "network.cookie.cookieBehavior.pbmode"; + +registerCleanupFunction(() => { + Services.prefs.clearUserPref(PREF_FPI); + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR); + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR_PBMODE); +}); + +add_task(function test_FPI_off() { + Services.prefs.setBoolPref(PREF_FPI, false); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR), i); + equal(Services.cookies.getCookieBehavior(false), i); + } + + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR_PBMODE), i); + equal(Services.cookies.getCookieBehavior(true), i); + } +}); + +add_task(function test_FPI_on() { + Services.prefs.setBoolPref(PREF_FPI, true); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR), i); + equal( + Services.cookies.getCookieBehavior(false), + i == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ? Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + : i + ); + } + + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR_PBMODE), i); + equal( + Services.cookies.getCookieBehavior(true), + i == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ? Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + : i + ); + } + + Services.prefs.clearUserPref(PREF_FPI); +}); + +add_task(function test_private_cookieBehavior_mirroring() { + // Test that the private cookieBehavior getter will return the regular pref if + // the regular pref has a user value and the private pref has a default value. + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR_PBMODE); + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i); + if (!Services.prefs.prefHasUserValue(PREF_COOKIE_BEHAVIOR)) { + continue; + } + + equal(Services.cookies.getCookieBehavior(true), i); + } + + // Test that the private cookieBehavior getter will always return the private + // pref if the private cookieBehavior has a user value. + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i); + if (!Services.prefs.prefHasUserValue(PREF_COOKIE_BEHAVIOR_PBMODE)) { + continue; + } + + for (let j = 0; j <= Ci.nsICookieService.BEHAVIOR_LAST; ++j) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, j); + + equal(Services.cookies.getCookieBehavior(true), i); + } + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js b/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js new file mode 100644 index 0000000000..cdf2cdec2c --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +const TEST_CASES = [ + // Tests for different schemes. + { + url: "http://example.com/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "https://example.com/", + partitionKeySite: "(https,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for sub domains + { + url: "http://sub.example.com/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "http://sub.sub.example.com/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for path and query. + { + url: "http://www.example.com/path/to/somewhere/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "http://www.example.com/?query=string", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for other ports. + { + url: "http://example.com:8080/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "https://example.com:8080/", + partitionKeySite: "(https,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for about urls + { + url: "about:about", + partitionKeySite: + "(about,about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla)", + partitionKeyWithoutSite: + "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + }, + { + url: "about:preferences", + partitionKeySite: + "(about,about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla)", + partitionKeyWithoutSite: + "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + }, + // Test for ip addresses + { + url: "http://127.0.0.1/", + partitionKeySite: "(http,127.0.0.1)", + partitionKeyWithoutSite: "127.0.0.1", + }, + { + url: "http://127.0.0.1:8080/", + partitionKeySite: "(http,127.0.0.1,8080)", + partitionKeyWithoutSite: "127.0.0.1", + }, + { + url: "http://[2001:db8::ff00:42:8329]", + partitionKeySite: "(http,[2001:db8::ff00:42:8329])", + partitionKeyWithoutSite: "[2001:db8::ff00:42:8329]", + }, + { + url: "http://[2001:db8::ff00:42:8329]:8080", + partitionKeySite: "(http,[2001:db8::ff00:42:8329],8080)", + partitionKeyWithoutSite: "[2001:db8::ff00:42:8329]", + }, + // Tests for moz-extension + { + url: "moz-extension://bafa4a3f-5c49-48d6-9788-03489419b70e", + partitionKeySite: "", + partitionKeyWithoutSite: "", + }, + // Tests for non tld + { + url: "http://notld", + partitionKeySite: "(http,notld)", + partitionKeyWithoutSite: "notld", + }, + { + url: "http://com", + partitionKeySite: "(http,com)", + partitionKeyWithoutSite: "com", + }, + { + url: "http://com:8080", + partitionKeySite: "(http,com,8080)", + partitionKeyWithoutSite: "com", + }, +]; + +const TEST_INVALID_URLS = [ + "", + "/foo", + "An invalid URL", + "https://", + "http:///", + "http://foo:bar", +]; + +add_task(async function test_get_partition_key_from_url() { + for (const test of TEST_CASES) { + info(`Testing url: ${test.url}`); + let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url); + + Assert.equal( + partitionKey, + test.partitionKeySite, + "The partitionKey is correct." + ); + } +}); + +add_task(async function test_get_partition_key_from_url_without_site() { + Services.prefs.setBoolPref("privacy.dynamic_firstparty.use_site", false); + + for (const test of TEST_CASES) { + info(`Testing url: ${test.url}`); + let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url); + + Assert.equal( + partitionKey, + test.partitionKeyWithoutSite, + "The partitionKey is correct." + ); + } + + Services.prefs.clearUserPref("privacy.dynamic_firstparty.use_site"); +}); + +add_task(async function test_blob_url() { + do_get_profile(); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + + server.registerPathHandler("/empty", (metadata, response) => { + var body = "<h1>Hello!</h1>"; + response.write(body); + }); + + server.registerPathHandler("/iframe", (metadata, response) => { + var body = ` + <script> + var blobUrl = URL.createObjectURL(new Blob([])); + parent.postMessage(blobUrl, "http://example.org"); + </script> + `; + response.write(body); + }); + + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/empty" + ); + + let blobUrl = await contentPage.spawn([], async () => { + // Create a third-party iframe and create a blob url in there. + let f = this.content.document.createElement("iframe"); + f.src = "http://foo.com/iframe"; + + let blob_url = await new Promise(resolve => { + this.content.addEventListener("message", event => resolve(event.data), { + once: true, + }); + this.content.document.body.append(f); + }); + + return blob_url; + }); + + let partitionKey = ChromeUtils.getPartitionKeyFromURL(blobUrl); + + // The partitionKey of the blob url is empty because the principal of the + // blob url is the JS principal of the global, which doesn't have + // partitionKey. And ChromeUtils.getPartitionKeyFromURL() will get + // partitionKey from that principal. So, we will get an empty partitionKey + // here. + // XXX: The behavior here is debatable. + Assert.equal(partitionKey, "", "The partitionKey of blob url is correct."); + + await contentPage.close(); +}); + +add_task(async function test_throw_with_invalid_URL() { + // The API should throw if the url is invalid. + for (const invalidURL of TEST_INVALID_URLS) { + info(`Testing invalid url: ${invalidURL}`); + + Assert.throws( + () => { + ChromeUtils.getPartitionKeyFromURL(invalidURL); + }, + /NS_ERROR_MALFORMED_URI/, + "It should fail on invalid URLs." + ); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js new file mode 100644 index 0000000000..668e905b6c --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js @@ -0,0 +1,728 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TRACKING_PAGE = "https://tracking.example.org"; +const HTTP_TRACKING_PAGE = "http://tracking.example.org"; +const TRACKING_PAGE2 = + "https://tracking.example.org^partitionKey=(https,example.com)"; +const HTTP_TRACKING_PAGE2 = + "http://tracking.example.org^partitionKey=(https,example.com)"; +const BENIGN_PAGE = "https://example.com"; +const FOREIGN_PAGE = "https://example.net"; +const FOREIGN_PAGE2 = "https://example.net^partitionKey=(https,example.com)"; +const FOREIGN_PAGE3 = "https://example.net^partitionKey=(https,example.org)"; + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "PurgeTrackerService", + "@mozilla.org/purge-tracker-service;1", + "nsIPurgeTrackerService" +); + +async function setupTest(aCookieBehavior) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", aCookieBehavior); + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true); + Services.prefs.setCharPref("privacy.purge_trackers.logging.level", "Debug"); + Services.prefs.setStringPref( + "urlclassifier.trackingAnnotationTable.testEntries", + "tracking.example.org" + ); + + // Enables us to test localStorage in xpcshell. + Services.prefs.setBoolPref("dom.storage.client_validation", false); +} + +/** + * Test that purging doesn't happen when it shouldn't happen. + */ +add_task(async function testNotPurging() { + await UrlClassifierTestUtils.addTestTrackers(); + setupTest(Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN); + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE }); + + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_ACCEPT + ); + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains."); + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN + ); + + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", false); + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains."); + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true); + + Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true); + Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true); + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains."); + Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.history"); + + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(!SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie cleared."); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +/** + * Test that cookies indexedDB and localStorage are purged if the cookie is found + * on the tracking list and does not have an Interaction Permission. + */ +async function testIndexedDBAndLocalStorage() { + await UrlClassifierTestUtils.addTestTrackers(); + + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + SiteDataTestUtils.addToCookies({ origin: BENIGN_PAGE }); + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + SiteDataTestUtils.addToLocalStorage(url); + SiteDataTestUtils.addToCookies({ origin: url }); + await SiteDataTestUtils.addToIndexedDB(url); + } + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + SiteDataTestUtils.hasCookies(url), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + + // Run purge after storage access permission has been removed. + PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI"); + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(BENIGN_PAGE), + "A non-tracking page should retain cookies after purging" + ); + + for (let url of [FOREIGN_PAGE, FOREIGN_PAGE2, FOREIGN_PAGE3]) { + ok( + SiteDataTestUtils.hasCookies(url), + `A non-tracking foreign page should retain cookies after purging` + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + `localStorage for ${url} should not have been removed.` + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + + // Cookie should have been removed. + + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + !SiteDataTestUtils.hasCookies(url), + "cookie is removed after purge with no storage access permission." + ); + ok( + !SiteDataTestUtils.hasLocalStorage(url), + "localStorage should have been removed" + ); + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage was deleted" + ); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that trackers are treated based on their base domain, not origin. + */ +async function testBaseDomain() { + await UrlClassifierTestUtils.addTestTrackers(); + + let associatedOrigins = [ + "https://itisatracker.org", + "https://sub.itisatracker.org", + "https://www.itisatracker.org", + "https://sub.sub.sub.itisatracker.org", + "http://itisatracker.org", + "http://sub.itisatracker.org", + ]; + + for (let permissionOrigin of associatedOrigins) { + // Only one of the associated origins gets permission, but + // all should be exempt from purging. + PermissionTestUtils.add( + permissionOrigin, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + for (let origin of associatedOrigins) { + SiteDataTestUtils.addToCookies({ origin }); + } + + // Add another tracker to verify we're actually purging. + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE }); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + for (let origin of associatedOrigins) { + ok( + SiteDataTestUtils.hasCookies(origin), + `${origin} should have retained its cookies when permission is set for ${permissionOrigin}.` + ); + } + + ok( + !SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie is removed after purge with no storage access permission." + ); + + PermissionTestUtils.remove(permissionOrigin, "storageAccessAPI"); + await SiteDataTestUtils.clear(); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that trackers are not cleared if they are associated + * with an entry on the entity list that has user interaction. + */ +async function testUserInteraction(ownerPage) { + Services.prefs.setBoolPref( + "privacy.purge_trackers.consider_entity_list", + true + ); + // The test URL for the entity list for annotation is + // itisatrap.org/?resource=example.org, so we need to + // add example.org as a tracker. + Services.prefs.setCharPref( + "urlclassifier.trackingAnnotationTable.testEntries", + "example.org" + ); + await UrlClassifierTestUtils.addTestTrackers(); + + // example.org and itisatrap.org are hard coded test values on the entity list. + const RESOURCE_PAGE = "https://example.org"; + + PermissionTestUtils.add( + ownerPage, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + SiteDataTestUtils.addToCookies({ origin: RESOURCE_PAGE }); + + // Add another tracker to verify we're actually purging. + SiteDataTestUtils.addToCookies({ + origin: "https://another-tracking.example.net", + }); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(RESOURCE_PAGE), + `${RESOURCE_PAGE} should have retained its cookies when permission is set for ${ownerPage}.` + ); + + ok( + !SiteDataTestUtils.hasCookies("https://another-tracking.example.net"), + "cookie is removed after purge with no storage access permission." + ); + + Services.prefs.setBoolPref( + "privacy.purge_trackers.consider_entity_list", + false + ); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + !SiteDataTestUtils.hasCookies(RESOURCE_PAGE), + `${RESOURCE_PAGE} should not have retained its cookies when permission is set for ${ownerPage} and the entity list pref is off.` + ); + + PermissionTestUtils.remove(ownerPage, "storageAccessAPI"); + await SiteDataTestUtils.clear(); + + Services.prefs.clearUserPref("privacy.purge_trackers.consider_entity_list"); + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that quota storage (even without cookies) is considered when purging trackers. + */ +async function testQuotaStorage() { + await UrlClassifierTestUtils.addTestTrackers(); + + let testCases = [ + { localStorage: true, indexedDB: true }, + { localStorage: false, indexedDB: true }, + { localStorage: true, indexedDB: false }, + ]; + + for (let { localStorage, indexedDB } of testCases) { + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + if (localStorage) { + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + SiteDataTestUtils.addToLocalStorage(url); + } + } + + if (indexedDB) { + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + await SiteDataTestUtils.addToIndexedDB(url); + } + } + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + if (localStorage) { + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "localStorage should not have been removed while storage access permission exists." + ); + } + + if (indexedDB) { + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + } + + // Run purge after storage access permission has been removed. + PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI"); + await PurgeTrackerService.purgeTrackingCookieJars(); + + if (localStorage) { + for (let url of [ + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed for non-tracking page." + ); + } + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + !SiteDataTestUtils.hasLocalStorage(url), + "localStorage should have been removed." + ); + } + } + + if (indexedDB) { + for (let url of [ + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage for non-tracking page was not deleted" + ); + } + + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage was deleted" + ); + } + } + + await SiteDataTestUtils.clear(); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that we correctly delete cookies and storage for sites + * with an expired interaction permission. + */ +async function testExpiredInteractionPermission() { + await UrlClassifierTestUtils.addTestTrackers(); + + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_TIME, + Date.now() + 500 + ); + + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + SiteDataTestUtils.addToLocalStorage(url); + SiteDataTestUtils.addToCookies({ origin: url }); + await SiteDataTestUtils.addToIndexedDB(url); + } + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + ok( + SiteDataTestUtils.hasCookies(url), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + + // Run purge after storage access permission has been removed. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(c => setTimeout(c, 500)); + await PurgeTrackerService.purgeTrackingCookieJars(); + + // Cookie should have been removed. + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + !SiteDataTestUtils.hasCookies(url), + "cookie is removed after purge with no storage access permission." + ); + ok( + !SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage was deleted" + ); + } + + // Cookie should not have been removed. + for (let url of [FOREIGN_PAGE, FOREIGN_PAGE2, FOREIGN_PAGE3]) { + ok( + SiteDataTestUtils.hasCookies(url), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/* + * Test that we correctly do or do not purges cookies + * from sites given thier cookie permissions. + */ +async function testNotPurgingFromAllowedWebsites() { + await UrlClassifierTestUtils.addTestTrackers(); + + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE }); + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE2 }); + + PermissionTestUtils.add( + TRACKING_PAGE, + "cookie", + Ci.nsICookiePermission.ACCESS_ALLOW + ); + + PermissionTestUtils.add( + TRACKING_PAGE2, + "cookie", + Ci.nsICookiePermission.ACCESS_SESSION + ); + + ok( + SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "Cookie is set to the initial state for Tracking Page 1" + ); + ok( + SiteDataTestUtils.hasCookies(TRACKING_PAGE2), + "Cookie is set to the initial state for Tracking Page 2" + ); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "Cookie was not purged for Tracking Page 1" + ); + ok( + !SiteDataTestUtils.hasCookies(TRACKING_PAGE2), + "Cookie was purged for Tracking Page 2" + ); + + PermissionTestUtils.remove(TRACKING_PAGE, "cookie"); + + PermissionTestUtils.remove(TRACKING_PAGE2, "cookie"); + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/* + * Testing that Local Storage is not purged + * from sites based thier cookie permissions. + */ +async function testNotPurgingLocalStorage() { + await UrlClassifierTestUtils.addTestTrackers(); + + SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE); + SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE2); + + PermissionTestUtils.add( + TRACKING_PAGE, + "cookie", + Ci.nsICookiePermission.ACCESS_ALLOW + ); + + PermissionTestUtils.add( + TRACKING_PAGE2, + "cookie", + Ci.nsICookiePermission.ACCESS_SESSION + ); + + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "Local Storage is set to the initial state for Tracking Page 1" + ); + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE2), + "Local Storage is set to the initial state for Tracking Page 2" + ); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "Local Storage was not purged for Tracking Page 1" + ); + ok( + !SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE2), + "Local Storage was not purged for Tracking Page 2" + ); + + PermissionTestUtils.remove(TRACKING_PAGE, "cookie"); + + PermissionTestUtils.remove(TRACKING_PAGE2, "cookie"); + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/* + * Test that we correctly do or do not purges cookies + * from http sites given thier cookie permissions. + */ +async function testNotPurgingFromHTTP() { + await UrlClassifierTestUtils.addTestTrackers(); + + SiteDataTestUtils.addToCookies({ origin: HTTP_TRACKING_PAGE }); + SiteDataTestUtils.addToCookies({ + origin: HTTP_TRACKING_PAGE2, + }); + + PermissionTestUtils.add( + HTTP_TRACKING_PAGE, + "cookie", + Ci.nsICookiePermission.ACCESS_ALLOW + ); + + PermissionTestUtils.add( + HTTP_TRACKING_PAGE2, + "cookie", + Ci.nsICookiePermission.ACCESS_SESSION + ); + + ok( + SiteDataTestUtils.hasCookies(HTTP_TRACKING_PAGE), + "Cookie is set to the initial state for HTTP Tracking Page 1" + ); + ok( + SiteDataTestUtils.hasCookies(HTTP_TRACKING_PAGE2), + "Cookie is set to the initial state for HTTP Tracking Page 2" + ); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(HTTP_TRACKING_PAGE), + "Cookie was not purged for HTTP Tracking Page 1" + ); + ok( + !SiteDataTestUtils.hasCookies(HTTP_TRACKING_PAGE2), + "Cookie was purged for HTTP Tracking Page 2" + ); + + PermissionTestUtils.remove(HTTP_TRACKING_PAGE, "cookie"); + + PermissionTestUtils.remove(HTTP_TRACKING_PAGE2, "cookie"); + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/* + * Test that we correctly do or do not purges local storage + * from http sites if https site has preserve cookies permission + */ +async function testNotPurgingFromDifferentScheme() { + await UrlClassifierTestUtils.addTestTrackers(); + + SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE); + SiteDataTestUtils.addToLocalStorage(HTTP_TRACKING_PAGE); + + PermissionTestUtils.add( + TRACKING_PAGE, + "cookie", + Ci.nsICookiePermission.ACCESS_ALLOW + ); + + PermissionTestUtils.add( + HTTP_TRACKING_PAGE, + "cookie", + Ci.nsICookiePermission.ACCESS_SESSION + ); + + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "Local Storage is set to the initial state for HTTPS Tracking Page " + ); + ok( + SiteDataTestUtils.hasLocalStorage(HTTP_TRACKING_PAGE), + "Local Storage is set to the initial state for HTTP Tracking Page" + ); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "Local Storage was not purged for HTTPS Tracking Page " + ); + ok( + SiteDataTestUtils.hasLocalStorage(HTTP_TRACKING_PAGE), + "Local Storage was not purged for HTTP Tracking Page" + ); + + PermissionTestUtils.remove(TRACKING_PAGE, "cookie"); + + PermissionTestUtils.remove(HTTP_TRACKING_PAGE, "cookie"); + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +add_task(async function () { + const cookieBehaviors = [ + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ]; + + for (let cookieBehavior of cookieBehaviors) { + await setupTest(cookieBehavior); + await testIndexedDBAndLocalStorage(); + await testBaseDomain(); + // example.org and itisatrap.org are hard coded test values on the entity list. + await testUserInteraction("https://itisatrap.org"); + await testUserInteraction( + "https://itisatrap.org^firstPartyDomain=example.net" + ); + await testQuotaStorage(); + await testExpiredInteractionPermission(); + await testNotPurgingFromAllowedWebsites(); + await testNotPurgingLocalStorage(); + await testNotPurgingFromHTTP(); + await testNotPurgingFromDifferentScheme(); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js new file mode 100644 index 0000000000..a1502373dc --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js @@ -0,0 +1,175 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TRACKING_PAGE = "https://tracking.example.org"; +const BENIGN_PAGE = "https://example.com"; + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "PurgeTrackerService", + "@mozilla.org/purge-tracker-service;1", + "nsIPurgeTrackerService" +); + +add_task(async function setup() { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + ); + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true); + Services.prefs.setStringPref( + "urlclassifier.trackingAnnotationTable.testEntries", + "tracking.example.org" + ); + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + // Enables us to test localStorage in xpcshell. + Services.prefs.setBoolPref("dom.storage.client_validation", false); +}); + +/** + * Test telemetry for cookie purging. + */ +add_task(async function () { + await UrlClassifierTestUtils.addTestTrackers(); + + let FIVE_DAYS = 5 * 24 * 60 * 60 * 1000; + + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_TIME, + Date.now() + FIVE_DAYS + ); + + SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE); + SiteDataTestUtils.addToCookies({ origin: BENIGN_PAGE }); + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE }); + await SiteDataTestUtils.addToIndexedDB(TRACKING_PAGE); + + let purgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_ORIGINS_PURGED" + ); + let notPurgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION" + ); + let remainingDaysHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_TRACKERS_USER_INTERACTION_REMAINING_DAYS" + ); + let intervalHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_INTERVAL_HOURS" + ); + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(TRACKING_PAGE), + 0, + `We have data for ${TRACKING_PAGE}` + ); + + TelemetryTestUtils.assertHistogram(purgedHistogram, 0, 1); + TelemetryTestUtils.assertHistogram(notPurgedHistogram, 1, 1); + TelemetryTestUtils.assertHistogram(remainingDaysHistogram, 4, 2); + TelemetryTestUtils.assertHistogram(intervalHistogram, 0, 1); + + purgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_ORIGINS_PURGED" + ); + notPurgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION" + ); + intervalHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_INTERVAL_HOURS" + ); + + // Run purge after storage access permission has been removed. + PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI"); + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(BENIGN_PAGE), + "A non-tracking page should retain cookies after purging" + ); + + // Cookie should have been removed. + ok( + !SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie is removed after purge with no storage access permission." + ); + ok( + !SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(TRACKING_PAGE), + 0, + "quota storage was deleted" + ); + + TelemetryTestUtils.assertHistogram(purgedHistogram, 1, 1); + Assert.equal( + notPurgedHistogram.snapshot().sum, + 0, + "no origins with user interaction" + ); + TelemetryTestUtils.assertHistogram(intervalHistogram, 0, 1); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +/** + * Test counting correctly across cookies batches + */ +add_task(async function () { + await UrlClassifierTestUtils.addTestTrackers(); + + // Enforce deleting the same origin twice by adding two cookies and setting + // the max number of cookies per batch to 1. + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE, name: "cookie1" }); + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE, name: "cookie2" }); + Services.prefs.setIntPref("privacy.purge_trackers.max_purge_count", 1); + + let purgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_ORIGINS_PURGED" + ); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + // Cookie should have been removed. + await TestUtils.waitForCondition( + () => !SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie is removed after purge." + ); + + TelemetryTestUtils.assertHistogram(purgedHistogram, 1, 1); + + Services.prefs.clearUserPref("privacy.purge_trackers.max_purge_count"); + UrlClassifierTestUtils.cleanupTestTrackers(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js new file mode 100644 index 0000000000..0492f5ff2a --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +function Requestor() {} +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIAuthPrompt2", + ]), + + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + promptAuth(channel, level, authInfo) { + Assert.equal("secret", authInfo.realm); + // No passwords in the URL -> nothing should be prefilled + Assert.equal(authInfo.username, ""); + Assert.equal(authInfo.password, ""); + Assert.equal(authInfo.domain, ""); + + authInfo.username = "guest"; + authInfo.password = "guest"; + + return true; + }, + + asyncPromptAuth(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +let observer = channel => { + if ( + !(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "localhost") + ) { + return; + } + channel.notificationCallbacks = new Requestor(); +}; +Services.obs.addObserver(observer, "http-on-modify-request"); + +add_task(async () => { + do_get_profile(); + + Services.prefs.setBoolPref("network.predictor.enabled", false); + Services.prefs.setBoolPref("network.predictor.enable-prefetch", false); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + + for (let test of [true, false]) { + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref("privacy.partition.network_state", test); + + const httpserv = new HttpServer(); + httpserv.registerPathHandler("/auth", (metadata, response) => { + // btoa("guest:guest"), but that function is not available here + const expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + + let body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "success"; + } else { + // didn't know guest:guest, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "failed"; + } + + response.bodyOutputStream.write(body, body.length); + }); + + httpserv.start(-1); + const URL = "http://localhost:" + httpserv.identity.primaryPort; + + const httpHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" + ].getService(Ci.nsIHttpProtocolHandler); + + const contentPage = await CookieXPCShellUtils.loadContentPage( + URL + "/auth?r=" + Math.random() + ); + await contentPage.close(); + + let key; + if (test) { + key = `^partitionKey=%28http%2Clocalhost%2C${httpserv.identity.primaryPort}%29:http://localhost:${httpserv.identity.primaryPort}`; + } else { + key = `:http://localhost:${httpserv.identity.primaryPort}`; + } + + Assert.equal(httpHandler.authCacheKeys.includes(key), true, "Key found!"); + + await new Promise(resolve => httpserv.stop(resolve)); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js new file mode 100644 index 0000000000..ba2f6c6894 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +let cars = Cc["@mozilla.org/security/clientAuthRememberService;1"].getService( + Ci.nsIClientAuthRememberService +); +let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB +); + +function getOAWithPartitionKey( + { scheme = "https", topLevelBaseDomain, port = null } = {}, + originAttributes = {} +) { + if (!topLevelBaseDomain || !scheme) { + return originAttributes; + } + + return { + ...originAttributes, + partitionKey: `(${scheme},${topLevelBaseDomain}${port ? `,${port}` : ""})`, + }; +} + +// These are not actual server and client certs. The ClientAuthRememberService +// does not care which certs we store decisions for, as long as they're valid. +let [clientCert] = certDB.getCerts(); + +function addSecurityInfo({ host, topLevelBaseDomain, originAttributes = {} }) { + let attrs = getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes); + cars.rememberDecisionScriptable(host, attrs, clientCert); +} + +function testSecurityInfo({ + host, + topLevelBaseDomain, + originAttributes = {}, + expected = true, +}) { + let attrs = getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes); + + let messageSuffix = `for ${host}`; + if (topLevelBaseDomain) { + messageSuffix += ` partitioned under ${topLevelBaseDomain}`; + } + + let hasRemembered = cars.hasRememberedDecisionScriptable(host, attrs, {}); + + Assert.equal( + hasRemembered, + expected, + `CAR ${expected ? "is set" : "is not set"} ${messageSuffix}` + ); +} + +function addTestEntries() { + let entries = [ + { host: "example.net" }, + { host: "test.example.net" }, + { host: "example.org" }, + { host: "example.com", topLevelBaseDomain: "example.net" }, + { + host: "test.example.net", + topLevelBaseDomain: "example.org", + }, + { + host: "foo.example.com", + originAttributes: { + privateBrowsingId: 1, + }, + }, + ]; + + info("Add test state"); + entries.forEach(addSecurityInfo); + info("Ensure we have the correct state initially"); + entries.forEach(testSecurityInfo); +} + +add_task(async () => { + addTestEntries(); + + info("Should not be set for unrelated host"); + [undefined, "example.org", "example.net", "example.com"].forEach( + topLevelBaseDomain => + testSecurityInfo({ + host: "mochit.test", + topLevelBaseDomain, + expected: false, + }) + ); + + info("Should not be set for unrelated subdomain"); + testSecurityInfo({ host: "foo.example.net", expected: false }); + + info("Should not be set for unpartitioned first party"); + testSecurityInfo({ + host: "example.com", + expected: false, + }); + + info("Should not be set under different first party"); + testSecurityInfo({ + host: "example.com", + topLevelBaseDomain: "example.org", + expected: false, + }); + testSecurityInfo({ + host: "test.example.net", + topLevelBaseDomain: "example.com", + expected: false, + }); + + info("Should not be set in partitioned context"); + ["example.com", "example.net", "example.org", "mochi.test"].forEach( + topLevelBaseDomain => + testSecurityInfo({ + host: "foo.example.com", + topLevelBaseDomain, + expected: false, + }) + ); + + // Cleanup + cars.clearRememberedDecisions(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js new file mode 100644 index 0000000000..46e230ec3e --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +let gHits = 0; + +add_task(async function () { + do_get_profile(); + + info("Disable predictor and accept all"); + Services.prefs.setBoolPref("network.predictor.enabled", false); + Services.prefs.setBoolPref("network.predictor.enable-prefetch", false); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com", "bar.com"], + }); + + server.registerFile( + "/font.woff", + do_get_file("data/font.woff"), + (_, response) => { + response.setHeader("Access-Control-Allow-Origin", "*", false); + gHits++; + } + ); + + server.registerPathHandler("/font", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + let body = ` + <style type="text/css"> + @font-face { + font-family: foo; + src: url("http://example.org/font.woff") format('woff'); + } + body { font-family: foo } + </style> + <iframe src="http://example.org/font-iframe"> + </iframe>`; + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/font-iframe", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + let body = ` + <style type="text/css"> + @font-face { + font-family: foo; + src: url("http://example.org/font.woff") format('woff'); + } + body { font-family: foo } + </style>`; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + prefValue: true, + hitsCount: 5, + }, + { + prefValue: false, + // The font in page B/C is CORS, the channel will be flagged with + // nsIRequest::LOAD_ANONYMOUS. + // The flag makes the font in A and B/C use different cache key. + hitsCount: 2, + }, + ]; + + for (let test of tests) { + info("Clear network caches"); + Services.cache2.clear(); + + info("Reset the hits count"); + gHits = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + info("Let's load a page with origin A"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/font" + ); + await contentPage.close(); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/font" + ); + await contentPage.close(); + + info("Let's load a page with origin C"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://bar.com/font" + ); + await contentPage.close(); + + Assert.equal(gHits, test.hitsCount, "The number of hits match"); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js new file mode 100644 index 0000000000..7492d2267a --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +let gHits = 0; + +add_task(async function () { + do_get_profile(); + + info("Disable predictor and accept all"); + Services.prefs.setBoolPref("network.predictor.enabled", false); + Services.prefs.setBoolPref("network.predictor.enable-prefetch", false); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + server.registerPathHandler("/image.png", (metadata, response) => { + gHits++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + var body = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII=" + ); + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/image", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = `<img src="http://example.org/image.png">`; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + prefValue: true, + hitsCount: 2, + }, + { + prefValue: false, + hitsCount: 1, + }, + ]; + + for (let test of tests) { + info("Clear image and network caches"); + let imageCache = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .getImgCacheForDocument(null); + imageCache.clearCache(true); // true=chrome + imageCache.clearCache(false); // false=content + Services.cache2.clear(); + + info("Reset the hits count"); + gHits = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + info("Let's load a page with origin A"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/image" + ); + await contentPage.close(); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/image" + ); + await contentPage.close(); + + Assert.equal(gHits, test.hitsCount, "The number of hits match"); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js new file mode 100644 index 0000000000..f7ec4cc8e3 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +// Small red image. +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +let gHints = 0; + +CookieXPCShellUtils.init(this); + +function countMatchingCacheEntries(cacheEntries, domain, path) { + return cacheEntries + .map(entry => entry.uri.asciiSpec) + .filter(spec => spec.includes(domain)) + .filter(spec => spec.includes(path)).length; +} + +async function checkCache(originAttributes) { + const loadContextInfo = Services.loadContextInfo.custom( + false, + originAttributes + ); + + const data = await new Promise(resolve => { + let cacheEntries = []; + let cacheVisitor = { + onCacheStorageInfo(num, consumption) {}, + onCacheEntryInfo(uri, idEnhance) { + cacheEntries.push({ uri, idEnhance }); + }, + onCacheEntryVisitCompleted() { + resolve(cacheEntries); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + // Visiting the disk cache also visits memory storage so we do not + // need to use Services.cache2.memoryCacheStorage() here. + let storage = Services.cache2.diskCacheStorage(loadContextInfo); + storage.asyncVisitStorage(cacheVisitor, true); + }); + + let foundEntryCount = countMatchingCacheEntries( + data, + "example.org", + "image.png" + ); + Assert.greater( + foundEntryCount, + 0, + `Cache entries expected for image.png and OA=${originAttributes}` + ); +} + +add_task(async () => { + do_get_profile(); + + Services.prefs.setBoolPref("network.prefetch-next", true); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + + server.registerPathHandler("/image.png", (metadata, response) => { + gHints++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + response.write(IMG_BYTES); + }); + + server.registerPathHandler("/prefetch", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = `<html><head></head><body><script> + const link = document.createElement("link") + link.setAttribute("rel", "prefetch"); + link.setAttribute("href", "http://example.org/image.png"); + document.head.appendChild(link); + link.onload = () => { + const img = document.createElement("IMG"); + img.src = "http://example.org/image.png"; + document.body.appendChild(img); + fetch("/done").then(() => {}); + } + </script></body></html>`; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + // 2 hints because we have 2 different top-level origins, loading the + // same resource. This will end up creating 2 separate cache entries. + hints: 2, + originAttributes: { partitionKey: "(http,example.org)" }, + prefValue: true, + }, + { + // 1 hint because, with network-state isolation, the cache entry will be + // reused for the second loading, even if the top-level origins are + // different. + hints: 1, + originAttributes: {}, + prefValue: false, + }, + ]; + + for (let test of tests) { + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); + + info("Reset the counter"); + gHints = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + let complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin A"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/prefetch" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/prefetch" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + Assert.equal( + gHints, + test.hints, + "We have the current number of requests with pref " + test.prefValue + ); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js new file mode 100644 index 0000000000..20158f2f7a --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +let gHints = 0; + +CookieXPCShellUtils.init(this); + +function countMatchingCacheEntries(cacheEntries, domain, path) { + return cacheEntries + .map(entry => entry.uri.asciiSpec) + .filter(spec => spec.includes(domain)) + .filter(spec => spec.includes(path)).length; +} + +async function checkCache(originAttributes) { + const loadContextInfo = Services.loadContextInfo.custom( + false, + originAttributes + ); + + const data = await new Promise(resolve => { + let cacheEntries = []; + let cacheVisitor = { + onCacheStorageInfo(num, consumption) {}, + onCacheEntryInfo(uri, idEnhance) { + cacheEntries.push({ uri, idEnhance }); + }, + onCacheEntryVisitCompleted() { + resolve(cacheEntries); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + // Visiting the disk cache also visits memory storage so we do not + // need to use Services.cache2.memoryCacheStorage() here. + let storage = Services.cache2.diskCacheStorage(loadContextInfo); + storage.asyncVisitStorage(cacheVisitor, true); + }); + + let foundEntryCount = countMatchingCacheEntries( + data, + "example.org", + "style.css" + ); + Assert.greater( + foundEntryCount, + 0, + `Cache entries expected for style.css and OA=${originAttributes}` + ); +} + +add_task(async () => { + do_get_profile(); + + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + + server.registerPathHandler("/empty", (metadata, response) => { + var body = "<h1>Hello!</h1>"; + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/style.css", (metadata, response) => { + gHints++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Access-Control-Allow-Origin", "*", false); + var body = "* { color: red }"; + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/preload", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = `<html><head></head><body><script> + const link = document.createElement("link") + link.setAttribute("rel", "preload"); + link.setAttribute("as", "style"); + link.setAttribute("href", "http://example.org/style.css"); + document.head.appendChild(link); + link.onload = () => { + fetch("/done").then(() => {}); + }; + </script></body></html>`; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + // 2 hints because we have 2 different top-level origins, loading the + // same resource. This will end up creating 2 separate cache entries. + hints: 2, + prefValue: true, + originAttributes: { partitionKey: "(http,example.org)" }, + }, + { + // 1 hint because, with network-state isolation, the cache entry will be + // reused for the second loading, even if the top-level origins are + // different. + hints: 1, + originAttributes: {}, + prefValue: false, + }, + ]; + + for (let test of tests) { + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); + + info("Reset the shared sheets"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/empty" + ); + + await contentPage.spawn([], () => + // eslint-disable-next-line no-undef + content.windowUtils.clearSharedStyleSheetCache() + ); + + await contentPage.close(); + + info("Reset the counter"); + gHints = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + let complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin A"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/preload" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/preload" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + Assert.equal( + gHints, + test.hints, + "We have the current number of requests with pref " + test.prefValue + ); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js new file mode 100644 index 0000000000..651b817f85 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js @@ -0,0 +1,538 @@ +/* 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/. */ + +// Note: This test may cause intermittents if run at exactly midnight. + +"use strict"; + +const { Sqlite } = ChromeUtils.importESModule( + "resource://gre/modules/Sqlite.sys.mjs" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "TrackingDBService", + "@mozilla.org/tracking-db-service;1", + "nsITrackingDBService" +); + +ChromeUtils.defineLazyGetter(this, "DB_PATH", function () { + return PathUtils.join(PathUtils.profileDir, "protections.sqlite"); +}); + +const SQL = { + insertCustomTimeEvent: + "INSERT INTO events (type, count, timestamp)" + + "VALUES (:type, :count, date(:timestamp));", + + selectAllEntriesOfType: "SELECT * FROM events WHERE type = :type;", + + selectAll: "SELECT * FROM events", +}; + +// Emulate the content blocking log. We do not record the url key, nor +// do we use the aggregated event number (the last element in the array). +const LOG = { + "https://1.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], + ], + "https://2.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, true, 1], + ], + "https://3.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT, true, 2], + ], + "https://4.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 3], + ], + "https://5.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 1], + ], + // Cookie blocked for other reason, then identified as a tracker + "https://6.example.com": [ + [ + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL | + Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT, + true, + 4, + ], + ], + "https://7.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER, true, 1], + ], + "https://8.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT, true, 1], + ], + + // The contents below should not add to the database. + // Cookie loaded but not blocked. + "https://10.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1], + ], + // Tracker cookie loaded but not blocked. + "https://11.unblocked.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER, true, 1], + ], + // Social tracker cookie loaded but not blocked. + "https://12.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1], + ], + // Cookie blocked for other reason (not a tracker) + "https://13.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION, true, 2], + ], + // Fingerprinters set to block, but this one has an exception + "https://14.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, false, 1], + ], + // Two fingerprinters replaced with a shims script, should be treated as blocked + // and increment the counter. + "https://15.example.com": [ + [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1], + ], + "https://16.example.com": [ + [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1], + ], + "https://17.example.com": [ + [ + Ci.nsIWebProgressListener.STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING, + true, + 1, + ], + ], +}; + +do_get_profile(); + +Services.prefs.setBoolPref("browser.contentblocking.database.enabled", true); +Services.prefs.setBoolPref( + "privacy.socialtracking.block_cookies.enabled", + true +); +Services.prefs.setBoolPref( + "privacy.trackingprotection.fingerprinting.enabled", + true +); +Services.prefs.setBoolPref("privacy.fingerprintingProtection", true); +Services.prefs.setBoolPref( + "browser.contentblocking.cfr-milestone.enabled", + true +); +Services.prefs.setIntPref( + "browser.contentblocking.cfr-milestone.update-interval", + 0 +); +Services.prefs.setStringPref( + "browser.contentblocking.cfr-milestone.milestones", + "[1000, 5000, 10000, 25000, 100000, 500000]" +); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.contentblocking.database.enabled"); + Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled"); + Services.prefs.clearUserPref( + "privacy.trackingprotection.fingerprinting.enabled" + ); + Services.prefs.clearUserPref("privacy.fingerprintingProtection"); + Services.prefs.clearUserPref("browser.contentblocking.cfr-milestone.enabled"); + Services.prefs.clearUserPref( + "browser.contentblocking.cfr-milestone.update-interval" + ); + Services.prefs.clearUserPref( + "browser.contentblocking.cfr-milestone.milestones" + ); +}); + +// This tests that data is added successfully, different types of events should get +// their own entries, when the type is the same they should be aggregated. Events +// that are not blocking events should not be recorded. Cookie blocking events +// should only be recorded if we can identify the cookie as a tracking cookie. +add_task(async function test_save_and_delete() { + await TrackingDBService.saveEvents(JSON.stringify(LOG)); + + // Peek in the DB to make sure we have the right data. + let db = await Sqlite.openConnection({ path: DB_PATH }); + // Make sure the items table was created. + ok(await db.tableExists("events"), "events table exists"); + + // make sure we have the correct contents in the database + let rows = await db.execute(SQL.selectAll); + equal( + rows.length, + 6, + "Events that should not be saved have not been, length is 6" + ); + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKERS_ID, + }); + equal(rows.length, 1, "Only one day has had tracker entries, length is 1"); + let count = rows[0].getResultByName("count"); + equal(count, 1, "there is only one tracker entry"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKING_COOKIES_ID, + }); + equal(rows.length, 1, "Only one day has had cookies entries, length is 1"); + count = rows[0].getResultByName("count"); + equal(count, 3, "Cookie entries were aggregated"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.CRYPTOMINERS_ID, + }); + equal( + rows.length, + 1, + "Only one day has had cryptominer entries, length is 1" + ); + count = rows[0].getResultByName("count"); + equal(count, 1, "there is only one cryptominer entry"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.FINGERPRINTERS_ID, + }); + equal( + rows.length, + 1, + "Only one day has had fingerprinters entries, length is 1" + ); + count = rows[0].getResultByName("count"); + equal(count, 3, "there are three fingerprinter entries"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.SOCIAL_ID, + }); + equal(rows.length, 1, "Only one day has had social entries, length is 1"); + count = rows[0].getResultByName("count"); + equal(count, 2, "there are two social entries"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID, + }); + equal( + rows.length, + 1, + "Only one day has had suspicious fingerprinting entries, length is 1" + ); + count = rows[0].getResultByName("count"); + equal(count, 1, "there is one suspicious fingerprinting entry"); + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + // Make sure the data was deleted. + rows = await db.execute(SQL.selectAll); + equal(rows.length, 0, "length is 0"); + await db.close(); +}); + +// This tests that content blocking events encountered on the same day get aggregated, +// and those on different days get seperate entries +add_task(async function test_timestamp_aggragation() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + let today = new Date().toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 4, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 1, + timestamp: yesterday, + }); + + // Add some events for today which must get aggregated + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: today, + }); + + // Add new events, they will have today's timestamp. + await TrackingDBService.saveEvents(JSON.stringify(LOG)); + + // Ensure events that are inserted today are not aggregated with past events. + let rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKERS_ID, + }); + equal(rows.length, 2, "Tracker entries for today and yesterday, length is 2"); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 4, "Yesterday's count is 4"); + } else if (i == 1) { + equal(count, 3, "Today's count is 3, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.CRYPTOMINERS_ID, + }); + equal( + rows.length, + 2, + "Cryptominer entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 3, "Yesterday's count is 3"); + } else if (i == 1) { + equal(count, 3, "Today's count is 3, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.FINGERPRINTERS_ID, + }); + equal( + rows.length, + 2, + "Fingerprinter entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 2, "Yesterday's count is 2"); + } else if (i == 1) { + equal(count, 5, "Today's count is 5, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKING_COOKIES_ID, + }); + equal( + rows.length, + 2, + "Tracking Cookies entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 1, "Yesterday's count is 1"); + } else if (i == 1) { + equal(count, 5, "Today's count is 5, new entries were aggregated"); + } + } + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + // Make sure the data was deleted. + rows = await db.execute(SQL.selectAll); + equal(rows.length, 0, "length is 0"); + await db.close(); +}); + +let addEventsToDB = async db => { + let d = new Date(1521009000000); + let date = d.toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: date, + }); + + date = new Date(d - 2 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 3 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 4 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 9 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: date, + }); +}; + +// This tests that TrackingDBService.getEventsByDateRange can accept two timestamps in unix epoch time +// and return entries that occur within the timestamps, rounded to the nearest day and inclusive. +add_task(async function test_getEventsByDateRange() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + await addEventsToDB(db); + + let d = new Date(1521009000000); + let daysBefore1 = new Date(d - 24 * 60 * 60 * 1000); + let daysBefore4 = new Date(d - 4 * 24 * 60 * 60 * 1000); + let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000); + + let events = await TrackingDBService.getEventsByDateRange(daysBefore1, d); + equal( + events.length, + 1, + "There is 1 event entry between the date and one day before, inclusive" + ); + + events = await TrackingDBService.getEventsByDateRange(daysBefore4, d); + equal( + events.length, + 4, + "There is 4 event entries between the date and four days before, inclusive" + ); + + events = await TrackingDBService.getEventsByDateRange( + daysBefore9, + daysBefore4 + ); + equal( + events.length, + 2, + "There is 2 event entries between nine and four days before, inclusive" + ); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that TrackingDBService.sumAllEvents returns the number of +// tracking events in the database, and can handle 0 entries. +add_task(async function test_sumAllEvents() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let sum = await TrackingDBService.sumAllEvents(); + equal(sum, 0, "There have been 0 events recorded"); + + // populate the database + await addEventsToDB(db); + + sum = await TrackingDBService.sumAllEvents(); + equal(sum, 11, "There have been 11 events recorded"); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that TrackingDBService.getEarliestRecordedDate returns the +// earliest date recorded and can handle 0 entries. +add_task(async function test_getEarliestRecordedDate() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let timestamp = await TrackingDBService.getEarliestRecordedDate(); + equal(timestamp, null, "There is no earliest recorded date"); + + // populate the database + await addEventsToDB(db); + let d = new Date(1521009000000); + let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0]; + + timestamp = await TrackingDBService.getEarliestRecordedDate(); + let date = new Date(timestamp).toISOString().split("T")[0]; + equal(date, daysBefore9, "The earliest recorded event is nine days before."); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that a message to CFR is sent when the amount of saved trackers meets a milestone +add_task(async function test_sendMilestoneNotification() { + let milestones = JSON.parse( + Services.prefs.getStringPref( + "browser.contentblocking.cfr-milestone.milestones" + ) + ); + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + // save number of trackers equal to the first milestone + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: milestones[0], + timestamp: new Date().toISOString(), + }); + + let awaitNotification = TestUtils.topicObserved( + "SiteProtection:ContentBlockingMilestone" + ); + + // trigger a "save" event to compare the trackers with the milestone. + await TrackingDBService.saveEvents( + JSON.stringify({ + "https://1.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], + ], + }) + ); + await awaitNotification; + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// Ensure we don't record suspicious fingerprinting if the fingerprinting +// protection is disabled. +add_task(async function test_noSuspiciousFingerprintingWithFPPDisabled() { + Services.prefs.setBoolPref("privacy.fingerprintingProtection", false); + + await TrackingDBService.saveEvents(JSON.stringify(LOG)); + + // Peek in the DB to make sure we have the right data. + let db = await Sqlite.openConnection({ path: DB_PATH }); + // Make sure the items table was created. + ok(await db.tableExists("events"), "events table exists"); + + let rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID, + }); + equal( + rows.length, + 0, + "Should be no suspicious entry if the fingerprinting protection is disabled" + ); + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + await db.close(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js b/toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js new file mode 100644 index 0000000000..48a0bd4682 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_validate_strip_on_share_list.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { JsonSchema } = ChromeUtils.importESModule( + "resource://gre/modules/JsonSchema.sys.mjs" +); + +let stripOnShareList; + +// Fetching strip on share list +add_setup(async function () { + /* globals fetch */ + let response = await fetch( + "chrome://global/content/antitracking/StripOnShare.json" + ); + if (!response.ok) { + throw new Error( + "Error fetching strip-on-share strip list" + response.status + ); + } + stripOnShareList = await response.json(); +}); + +// Check if the Strip on Share list contains any duplicate params +add_task(async function test_check_duplicates() { + let stripOnShareParams = stripOnShareList; + + const allQueryParams = []; + + for (const domain in stripOnShareParams) { + for (let param in stripOnShareParams[domain].queryParams) { + allQueryParams.push(stripOnShareParams[domain].queryParams[param]); + } + } + + let setOfParams = new Set(allQueryParams); + + if (setOfParams.size != allQueryParams.length) { + let setToCheckDupes = new Set(); + let dupeList = new Set(); + for (const domain in stripOnShareParams) { + for (let param in stripOnShareParams[domain].queryParams) { + let tempParam = stripOnShareParams[domain].queryParams[param]; + + if (setToCheckDupes.has(tempParam)) { + dupeList.add(tempParam); + } else { + setToCheckDupes.add(tempParam); + } + } + } + + Assert.equal( + setOfParams.size, + allQueryParams.length, + "There are duplicates rules. The duplicate rules are " + [...dupeList] + ); + } + + Assert.equal( + setOfParams.size, + allQueryParams.length, + "There are no duplicates rules." + ); +}); + +// Validate the format of Strip on Share list with Schema +add_task(async function test_check_schema() { + let schema = { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { + type: "object", + properties: { + queryParams: { + type: "array", + items: { type: "string" }, + }, + topLevelSites: { + type: "array", + items: { type: "string" }, + }, + }, + required: ["queryParams", "topLevelSites"], + }, + required: ["global"], + }; + + let stripOnShareParams = stripOnShareList; + let validator = new JsonSchema.Validator(schema); + let { valid, errors } = validator.validate(stripOnShareParams); + + if (!valid) { + info("validation errors: " + JSON.stringify(errors, null, 2)); + } + + Assert.ok(valid, "Strip on share JSON is valid"); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_view_source.js b/toolkit/components/antitracking/test/xpcshell/test_view_source.js new file mode 100644 index 0000000000..ebd70cf476 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_view_source.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +let gCookieHits = 0; +let gLoadingHits = 0; + +add_task(async function () { + do_get_profile(); + + info("Disable predictor and accept all"); + Services.prefs.setBoolPref("network.predictor.enabled", false); + Services.prefs.setBoolPref("network.predictor.enable-prefetch", false); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org"], + }); + server.registerPathHandler("/test", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + if ( + request.hasHeader("Cookie") && + request.getHeader("Cookie") == "foo=bar" + ) { + gCookieHits++; + } else { + response.setHeader("Set-Cookie", "foo=bar"); + } + + gLoadingHits++; + var body = "<html></html>"; + response.bodyOutputStream.write(body, body.length); + }); + + info("Reset the hits count"); + gCookieHits = 0; + gLoadingHits = 0; + + info("Let's load a page"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/test?1" + ); + await contentPage.close(); + + Assert.equal(gCookieHits, 0, "The number of cookie hits match"); + Assert.equal(gLoadingHits, 1, "The number of loading hits match"); + + info("Let's load the source of the page again to see if it loads from cache"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "view-source:http://example.org/test?1" + ); + await contentPage.close(); + + Assert.equal(gCookieHits, 0, "The number of cookie hits match"); + Assert.equal(gLoadingHits, 1, "The number of loading hits match"); + + info( + "Let's load the source of the page without hitting the cache to see if the cookie is sent properly" + ); + contentPage = await CookieXPCShellUtils.loadContentPage( + "view-source:http://example.org/test?2" + ); + await contentPage.close(); + + Assert.equal(gCookieHits, 1, "The number of cookie hits match"); + Assert.equal(gLoadingHits, 2, "The number of loading hits match"); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/xpcshell.toml b/toolkit/components/antitracking/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..86f524ab89 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/xpcshell.toml @@ -0,0 +1,52 @@ +[DEFAULT] +head = "head.js ../../../../components/url-classifier/tests/unit/head_urlclassifier.js" +prefs = ["dom.security.https_first=false"] #Disable https-first because of explicit http/https testing + +["test_ExceptionListService.js"] + +["test_cookie_behavior.js"] + +["test_getPartitionKeyFromURL.js"] +skip-if = ["socketprocess_networking"] # Bug 1759035 + +["test_purge_trackers.js"] +skip-if = [ + "win10_2009", # Bug 1718292 + "win11_2009", # Bug 1797751 +] +run-sequentially = "very high failure rate in parallel" + +["test_purge_trackers_telemetry.js"] + +["test_staticPartition_authhttp.js"] +skip-if = ["socketprocess_networking"] # Bug 1759035 + +["test_staticPartition_clientAuthRemember.js"] + +["test_staticPartition_font.js"] +support-files = ["data/font.woff"] +skip-if = [ + "os == 'linux' && !debug", # Bug 1760086 + "apple_silicon", # bug 1729551 + "os == 'mac' && bits == 64 && !debug", # Bug 1652119 + "os == 'win' && bits == 64 && !debug", # Bug 1652119 + "socketprocess_networking", # Bug 1759035 +] +run-sequentially = "very high failure rate in parallel" + +["test_staticPartition_image.js"] +skip-if = ["socketprocess_networking"] # Bug 1759035 + +["test_staticPartition_prefetch.js"] +skip-if = ["socketprocess_networking"] # Bug 1759035 + +["test_staticPartition_preload.js"] +skip-if = ["socketprocess_networking"] # Bug 1759035 + +["test_tracking_db_service.js"] +skip-if = ["os == 'android'"] # Bug 1697936 + +["test_validate_strip_on_share_list.js"] + +["test_view_source.js"] +skip-if = ["socketprocess_networking"] # Bug 1759035 (not as common on win, perma on linux/osx) |