diff options
Diffstat (limited to 'toolkit/components/antitracking')
266 files changed, 39827 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/AntiTrackingIPCUtils.h b/toolkit/components/antitracking/AntiTrackingIPCUtils.h new file mode 100644 index 0000000000..cbe9b1eef6 --- /dev/null +++ b/toolkit/components/antitracking/AntiTrackingIPCUtils.h @@ -0,0 +1,59 @@ +/* -*- 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> {}; +} // 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..747427abcc --- /dev/null +++ b/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.cpp @@ -0,0 +1,434 @@ +/* -*- 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; +} + +// The helper function to check if we need to check the redirect heuristic for +// RejectForeign later from the old channel perspective. +bool ShouldCheckRedirectHeuristicRejectForeign(nsIChannel* aOldChannel, + nsIURI* aOldURI, + nsIPrincipal* aOldPrincipal) { + if (!ContentBlockingUserInteraction::Exists(aOldPrincipal)) { + LOG_SPEC(("Ignoring redirect from %s because no user-interaction on " + "old origin", + _spec), + aOldURI); + 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); + } + + // We will grant the storage access regardless the new channel is a tracker or + // not for RejectForeign. + if (cookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN && + StaticPrefs::network_cookie_rejectForeignWithExceptions_enabled()) { + return true; + } + + 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); + } + + if (cookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN && + StaticPrefs::network_cookie_rejectForeignWithExceptions_enabled()) { + return ShouldCheckRedirectHeuristicRejectForeign(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 || + net::CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)); + + 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->GetOrigin(oldOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the origin from the Principal")); + return; + } + + nsAutoCString newOrigin; + rv = nsContentUtils::GetASCIIOrigin(aNewURI, newOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the origin from the URI")); + return; + } + + LOG(("Adding a first-party storage exception for %s...", 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, + 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..d40cd71c48 --- /dev/null +++ b/toolkit/components/antitracking/AntiTrackingUtils.cpp @@ -0,0 +1,899 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AntiTrackingUtils.h" + +#include "AntiTrackingLog.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/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 "nsRFPService.h" +#include "nsSandboxFlags.h" +#include "nsScriptSecurityManager.h" +#include "PartitioningExceptionList.h" + +#define ANTITRACKING_PERM_KEY "3rdPartyStorage" + +using namespace mozilla; +using namespace mozilla::dom; + +/* static */ already_AddRefed<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 +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); + + using Permissions = nsTArray<RefPtr<nsIPermission>>; + Permissions perms; + nsresult rv = permManager->GetAllWithTypePrefix(prefix, perms); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + // Create a set of unique origins + using Origins = nsTArray<nsCString>; + Origins origins; + + // 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) { + continue; + } + + nsCOMPtr<nsIPrincipal> principal; + rv = perm->GetPrincipal(getter_AddRefs(principal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + nsAutoCString origin; + rv = principal->GetOrigin(origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Nothing(); + } + + ToLowerCase(origin); + + // Append if it would not be a duplicate + if (origins.IndexOf(origin) == Origins::NoIndex) { + origins.AppendElement(origin); + } + } + + return Some(origins.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 */ +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; + } + + nsAutoCString trackingOrigin; + rv = nsContentUtils::GetASCIIOrigin(trackingURI, trackingOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsILoadInfo::NoStoragePermission; + } + + if (IsThirdPartyChannel(aChannel)) { + nsAutoCString targetOrigin; + if (NS_FAILED(targetPrincipal->GetAsciiOrigin(targetOrigin))) { + return nsILoadInfo::NoStoragePermission; + } + + if (PartitioningExceptionList::Check(targetOrigin, trackingOrigin)) { + return nsILoadInfo::StoragePermissionAllowListed; + } + } + + nsAutoCString type; + AntiTrackingUtils::CreateStoragePermissionKey(trackingOrigin, type); + + uint32_t unusedReason = 0; + + if (AntiTrackingUtils::CheckStoragePermission(targetPrincipal, type, + NS_UsePrivateBrowsing(aChannel), + &unusedReason, unusedReason)) { + 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)); + + // 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(uri, NS_UsePrivateBrowsing(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..07bd39257a --- /dev/null +++ b/toolkit/components/antitracking/AntiTrackingUtils.h @@ -0,0 +1,160 @@ +/* -*- 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); + + // 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); + + // 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..2a8595822d --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingLog.cpp @@ -0,0 +1,272 @@ +/* -*- 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 "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) { + MOZ_ASSERT(XRE_IsParentProcess()); + + uint32_t events = GetContentBlockingEventsInLog(); + + bool blockedValue = aBlocked; + bool unblocked = false; + + 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: + RecordLogInternal(aOrigin, aType, blockedValue); + break; + + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER: + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER: + 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: + RecordLogInternal(aOrigin, aType, blockedValue); + 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::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); +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/ContentBlockingLog.h b/toolkit/components/antitracking/ContentBlockingLog.h new file mode 100644 index 0000000000..f71b2fc0ff --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingLog.h @@ -0,0 +1,427 @@ +/* -*- 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 ContentBlockingLog final { + typedef ContentBlockingNotifier::StorageAccessPermissionGrantedReason + StorageAccessPermissionGrantedReason; + + struct LogEntry { + uint32_t mType; + uint32_t mRepeatCount; + bool mBlocked; + Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason> + mReason; + nsTArray<nsCString> mTrackingFullHashes; + }; + + struct OriginDataEntry { + OriginDataEntry() + : mHasLevel1TrackingContentLoaded(false), + mHasLevel2TrackingContentLoaded(false) {} + + bool mHasLevel1TrackingContentLoaded; + bool mHasLevel2TrackingContentLoaded; + Maybe<bool> mHasCookiesLoaded; + Maybe<bool> mHasTrackerCookiesLoaded; + Maybe<bool> mHasSocialTrackerCookiesLoaded; + nsTArray<LogEntry> mLogs; + }; + + struct OriginEntry { + OriginEntry() { mData = MakeUnique<OriginDataEntry>(); } + + nsCString mOrigin; + UniquePtr<OriginDataEntry> mData; + }; + + 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); + + 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 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->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: + void RecordLogInternal( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe< + ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason = Nothing(), + const nsTArray<nsCString>& aTrackingFullHashes = nsTArray<nsCString>()) { + 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; + } + auto index = mLog.IndexOf(aOrigin, 0, Comparator()); + if (index != OriginDataTable::NoIndex) { + OriginEntry& entry = mLog[index]; + if (!entry.mData) { + return; + } + + if (RecordLogEntryInCustomField(aType, entry, aBlocked)) { + return; + } + if (!entry.mData->mLogs.IsEmpty()) { + auto& last = entry.mData->mLogs.LastElement(); + if (last.mType == aType && last.mBlocked == aBlocked) { + ++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; + } + } + 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()}); + return; + } + + // The entry has not been found. + OriginEntry* entry = mLog.AppendElement(); + if (NS_WARN_IF(!entry || !entry->mData)) { + return; + } + + 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()}); + } + } + + 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(); + } + } + + 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..5de57f91b2 --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingNotifier.cpp @@ -0,0 +1,567 @@ +/* -*- 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 "nsGlobalWindowInner.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 = 0; + 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; + } + + nsAutoString origin; + nsresult rv = nsContentUtils::GetUTFOrigin(principal, origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Not adding grantedOrigin yet because we may not want it later. + AutoTArray<nsString, 2> params = {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 = 0; + 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) { + Unused << nsContentUtils::GetASCIIOrigin(aURI, 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) { + 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); +} + +// 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) { + 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); +} + +} // 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) { + Unused << nsContentUtils::GetASCIIOrigin(uri, 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) { + if (XRE_IsParentProcess()) { + NotifyEventInParent(aTrackingChannel, aBlocked, aRejectedReason, + aTrackingOrigin, aReason); + } else { + NotifyEventInChild(aTrackingChannel, aBlocked, aRejectedReason, + aTrackingOrigin, aReason); + } +} diff --git a/toolkit/components/antitracking/ContentBlockingNotifier.h b/toolkit/components/antitracking/ContentBlockingNotifier.h new file mode 100644 index 0000000000..20f32d7f94 --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingNotifier.h @@ -0,0 +1,74 @@ +/* -*- 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, + }; + + // 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 Maybe<StorageAccessPermissionGrantedReason>& aReason = 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..fd19904309 --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingTelemetryService.cpp @@ -0,0 +1,120 @@ +/* -*- 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<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..fa742b5b5e --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingUserInteraction.cpp @@ -0,0 +1,89 @@ +/* -*- 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/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); + + 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..c4b3ea3d2e --- /dev/null +++ b/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.cpp @@ -0,0 +1,343 @@ +/* -*- 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->GetOrigin(oldOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the origin from the Principal")); + return; + } + + nsAutoCString newOrigin; + rv = nsContentUtils::GetASCIIOrigin(aNewURI, newOrigin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the origin from the URI")); + return; + } + + if (!HasEligibleVisit(aOldURI) || !HasEligibleVisit(aNewURI)) { + LOG(("No previous visit record, bailing out early.")); + 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, + 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..42314954a7 --- /dev/null +++ b/toolkit/components/antitracking/PurgeTrackerService.sys.mjs @@ -0,0 +1,471 @@ +/* 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" +); + +XPCOMUtils.defineLazyGetter(lazy, "gClassifierFeature", () => { + return lazy.gClassifier.getFeatureByName("tracking-annotation"); +}); + +XPCOMUtils.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, + 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); + }, + + /** + * 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}.` + ); + } + if (httpPrincipal) { + maybeClearPrincipals.set(httpPrincipal.origin, httpPrincipal); + } + + 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}.` + ); + } + if (httpsPrincipal) { + 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()) { + 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/RejectForeignAllowList.cpp b/toolkit/components/antitracking/RejectForeignAllowList.cpp new file mode 100644 index 0000000000..3af0494558 --- /dev/null +++ b/toolkit/components/antitracking/RejectForeignAllowList.cpp @@ -0,0 +1,127 @@ +/* -*- 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 "RejectForeignAllowList.h" +#include "mozilla/dom/Document.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPtr.h" +#include "nsIHttpChannel.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsScriptSecurityManager.h" + +#define REJECTFOREIGNALLOWLIST_PREF "privacy.rejectForeign.allowList"_ns +#define REJECTFOREIGNALLOWLIST_NAME "RejectForeignAllowList"_ns + +namespace mozilla { + +namespace { + +StaticRefPtr<RejectForeignAllowList> gRejectForeignAllowList; + +} // namespace + +// static +bool RejectForeignAllowList::Check(dom::Document* aDocument) { + MOZ_ASSERT(aDocument); + + nsIURI* documentURI = aDocument->GetDocumentURI(); + if (!documentURI) { + return false; + } + + return GetOrCreate()->CheckInternal(documentURI); +} + +// static +bool RejectForeignAllowList::Check(nsIHttpChannel* aChannel) { + MOZ_ASSERT(aChannel); + + nsCOMPtr<nsIURI> channelURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return GetOrCreate()->CheckInternal(channelURI); +} + +// static +bool RejectForeignAllowList::Check(nsIPrincipal* aPrincipal) { + return GetOrCreate()->CheckInternal(aPrincipal); +} + +// static +RejectForeignAllowList* RejectForeignAllowList::GetOrCreate() { + if (!gRejectForeignAllowList) { + gRejectForeignAllowList = new RejectForeignAllowList(); + + nsCOMPtr<nsIUrlClassifierExceptionListService> exceptionListService = + do_GetService("@mozilla.org/url-classifier/exception-list-service;1"); + if (exceptionListService) { + exceptionListService->RegisterAndRunExceptionListObserver( + REJECTFOREIGNALLOWLIST_NAME, REJECTFOREIGNALLOWLIST_PREF, + gRejectForeignAllowList); + } + + RunOnShutdown([exceptionListService] { + if (gRejectForeignAllowList) { + if (exceptionListService) { + exceptionListService->UnregisterExceptionListObserver( + REJECTFOREIGNALLOWLIST_NAME, gRejectForeignAllowList); + } + gRejectForeignAllowList = nullptr; + } + }); + } + + return gRejectForeignAllowList; +} + +// static +bool RejectForeignAllowList::Check(nsIURI* aURI) { + return GetOrCreate()->CheckInternal(aURI); +} + +bool RejectForeignAllowList::CheckInternal(nsIURI* aURI) { + MOZ_ASSERT(aURI); + return nsContentUtils::IsURIInList(aURI, mList); +} + +bool RejectForeignAllowList::CheckInternal(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aPrincipal); + + auto* basePrin = BasePrincipal::Cast(aPrincipal); + if (!basePrin) { + return false; + } + + bool result = false; + basePrin->IsURIInList(mList, &result); + + return result; +} + +NS_IMETHODIMP +RejectForeignAllowList::OnExceptionListUpdate(const nsACString& aList) { + mList = aList; + return NS_OK; +} + +RejectForeignAllowList::RejectForeignAllowList() = default; +RejectForeignAllowList::~RejectForeignAllowList() = default; + +NS_INTERFACE_MAP_BEGIN(RejectForeignAllowList) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, + nsIUrlClassifierExceptionListObserver) + NS_INTERFACE_MAP_ENTRY(nsIUrlClassifierExceptionListObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(RejectForeignAllowList) +NS_IMPL_RELEASE(RejectForeignAllowList) + +} // namespace mozilla diff --git a/toolkit/components/antitracking/RejectForeignAllowList.h b/toolkit/components/antitracking/RejectForeignAllowList.h new file mode 100644 index 0000000000..f767bf88fc --- /dev/null +++ b/toolkit/components/antitracking/RejectForeignAllowList.h @@ -0,0 +1,47 @@ +/* -*- 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_RejectForeignAllowList_h +#define mozilla_RejectForeignAllowList_h + +#include "nsIUrlClassifierExceptionListService.h" +#include "nsIPrincipal.h" + +class nsIHttpChannel; +class nsIURI; + +namespace mozilla { + +namespace dom { +class Document; +} + +class RejectForeignAllowList final + : public nsIUrlClassifierExceptionListObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIURLCLASSIFIEREXCEPTIONLISTOBSERVER + + static bool Check(dom::Document* aDocument); + static bool Check(nsIHttpChannel* aChannel); + static bool Check(nsIPrincipal* aPrincipal); + static bool Check(nsIURI* aURI); + + private: + static RejectForeignAllowList* GetOrCreate(); + + RejectForeignAllowList(); + ~RejectForeignAllowList(); + + bool CheckInternal(nsIURI* aURI); + bool CheckInternal(nsIPrincipal* aPrincipal); + + nsCString mList; +}; + +} // namespace mozilla + +#endif // mozilla_RejectForeignAllowList_h 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..4bdd7eccc1 --- /dev/null +++ b/toolkit/components/antitracking/StorageAccess.cpp @@ -0,0 +1,916 @@ +/* -*- 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" +#include "RejectForeignAllowList.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. If aWindow is non-null, its principal must be passed as + * aPrincipal, and the third-party iframe and sandboxing status of the window + * are also checked. If aURI is non-null, then it is used as the comparison + * against aWindow to determine if this is a third-party load. We also + * allow a channel instead of the window reference when determining 3rd party + * status. + * + * 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; + } + + if (!StorageDisabledByAntiTracking(aWindow, aChannel, aPrincipal, aURI, + aRejectedReason)) { + 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; +} + +static bool StorageDisabledByAntiTrackingInternal( + nsPIDOMWindowInner* aWindow, nsIChannel* aChannel, nsIPrincipal* aPrincipal, + nsIURI* aURI, nsICookieJarSettings* aCookieJarSettings, + uint32_t& aRejectedReason) { + MOZ_ASSERT(aWindow || aChannel || aPrincipal); + + if (aWindow) { + nsIURI* documentURI = aURI ? aURI : aWindow->GetDocumentURI(); + return !documentURI || + !ShouldAllowAccessFor(aWindow, documentURI, &aRejectedReason); + } + + if (aChannel) { + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return !ShouldAllowAccessFor(aChannel, uri, &aRejectedReason); + } + + MOZ_ASSERT(aPrincipal); + return !ShouldAllowAccessFor(aPrincipal, aCookieJarSettings); +} + +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 StorageDisabledByAntiTracking(nsPIDOMWindowInner* aWindow, + nsIChannel* aChannel, + nsIPrincipal* aPrincipal, nsIURI* aURI, + uint32_t& aRejectedReason) { + MOZ_ASSERT(aWindow || aChannel || aPrincipal); + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + if (aWindow) { + if (aWindow->GetExtantDoc()) { + cookieJarSettings = aWindow->GetExtantDoc()->CookieJarSettings(); + } + } else if (aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + } + if (!cookieJarSettings) { + cookieJarSettings = net::CookieJarSettings::Create(aPrincipal); + } + bool disabled = StorageDisabledByAntiTrackingInternal( + aWindow, aChannel, aPrincipal, aURI, cookieJarSettings, aRejectedReason); + + if (aWindow) { + ContentBlockingNotifier::OnDecision( + aWindow, + disabled ? ContentBlockingNotifier::BlockingDecision::eBlock + : ContentBlockingNotifier::BlockingDecision::eAllow, + aRejectedReason); + } else if (aChannel) { + ContentBlockingNotifier::OnDecision( + aChannel, + disabled ? ContentBlockingNotifier::BlockingDecision::eBlock + : ContentBlockingNotifier::BlockingDecision::eAllow, + aRejectedReason); + } + return disabled; +} + +bool StorageDisabledByAntiTracking(dom::Document* aDocument, nsIURI* aURI, + uint32_t& aRejectedReason) { + aRejectedReason = 0; + // 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. + return StorageDisabledByAntiTracking( + aDocument->GetInnerWindow(), aDocument->GetChannel(), + aDocument->NodePrincipal(), aURI, aRejectedReason); +} + +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(); +} + +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 && + !CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)) || + behavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) { + // XXX For non-cookie forms of storage, we handle BEHAVIOR_LIMIT_FOREIGN by + // simply rejecting the request to use the storage. In the future, if we + // change the meaning of BEHAVIOR_LIMIT_FOREIGN to be one which makes sense + // for non-cookie storage types, this may change. + LOG(("Nothing more to do due to the behavior code %d", int(behavior))); + *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN; + return false; + } + + // The document has been allowlisted. We can return from here directly. + if (document->HasStorageAccessPermissionGrantedByAllowList()) { + return true; + } + + MOZ_ASSERT( + CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) || + behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || + behavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); + + uint32_t blockedReason = + nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER; + + if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER) { + if (!nsContentUtils::IsThirdPartyTrackingResourceWindow(aWindow)) { + LOG(("Our window isn't a third-party tracking window")); + return true; + } + + nsCOMPtr<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(CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)); + if (RejectForeignAllowList::Check(document)) { + LOG(("This window is exceptionlisted for reject foreign")); + return true; + } + + blockedReason = nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN; + } + + Document* doc = aWindow->GetExtantDoc(); + // Make sure storage access isn't disabled + if (doc && (doc->StorageAccessSandboxed())) { + LOG(("Our document is sandboxed")); + *aRejectedReason = blockedReason; + return false; + } + + // Document::HasStoragePermission first checks if storage access granted is + // cached in the inner window, if no, it then checks the storage permission + // flag in the channel's loadinfo + bool allowed = document->HasStorageAccessPermissionGranted(); + + if (!allowed) { + *aRejectedReason = blockedReason; + } else { + if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug) && + aWindow->HasStorageAccessPermissionGranted()) { + LOG(("Permission stored in the window. All good.")); + } + } + + return allowed; +} + +bool 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 && + !CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)) || + behavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) { + // XXX For non-cookie forms of storage, we handle BEHAVIOR_LIMIT_FOREIGN by + // simply rejecting the request to use the storage. In the future, if we + // change the meaning of BEHAVIOR_LIMIT_FOREIGN to be one which makes sense + // for non-cookie storage types, this may change. + LOG(("Nothing more to do due to the behavior code %d", int(behavior))); + *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN; + return false; + } + + // The channel has been allowlisted. We can return from here. + if (loadInfo->GetStoragePermission() == + nsILoadInfo::StoragePermissionAllowListed) { + return true; + } + + MOZ_ASSERT( + CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) || + behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || + behavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); + + uint32_t blockedReason = + nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER; + + // Not a tracker. + nsCOMPtr<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(CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)); + if (httpChannel && RejectForeignAllowList::Check(httpChannel)) { + LOG(("This channel is exceptionlisted")); + return true; + } + blockedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN; + } + + 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. + + // HasStorageAccessPermissionGranted only applies to channels that load + // documents, for sub-resources loads, just returns the result from loadInfo. + bool isDocument = false; + aChannel->GetIsDocument(&isDocument); + + if (isDocument) { + nsCOMPtr<nsPIDOMWindowInner> inner = + AntiTrackingUtils::GetInnerWindow(targetBC); + if (inner && inner->HasStorageAccessPermissionGranted()) { + 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 = nsICookiePermission::ACCESS_DEFAULT; + if (aPrincipal->GetIsContentPrincipal()) { + PermissionManager* permManager = PermissionManager::GetInstance(); + if (permManager) { + Unused << NS_WARN_IF(NS_FAILED(permManager->TestPermissionFromPrincipal( + aPrincipal, "cookie"_ns, &access))); + } + } + + if (access != nsICookiePermission::ACCESS_DEFAULT) { + return access != nsICookiePermission::ACCESS_DENY; + } + + int32_t behavior = CookiesBehavior(aPrincipal, aCookieJarSettings); + return behavior != nsICookieService::BEHAVIOR_REJECT; +} + +/* static */ +bool ApproximateAllowAccessForWithoutChannel( + nsPIDOMWindowInner* aFirstPartyWindow, nsIURI* aURI) { + MOZ_ASSERT(aFirstPartyWindow); + MOZ_ASSERT(aURI); + + LOG_SPEC( + ("Computing a best guess as to whether window %p has access to URI %s", + aFirstPartyWindow, _spec), + aURI); + + Document* parentDocument = + nsGlobalWindowInner::Cast(aFirstPartyWindow)->GetExtantDoc(); + if (NS_WARN_IF(!parentDocument)) { + LOG(("Failed to get the first party window's document")); + return false; + } + + if (!parentDocument->CookieJarSettings()->GetRejectThirdPartyContexts()) { + LOG(("Disabled by the pref (%d), bail out early", + parentDocument->CookieJarSettings()->GetCookieBehavior())); + return true; + } + + if (ContentBlockingAllowList::Check(aFirstPartyWindow)) { + return true; + } + + if (!AntiTrackingUtils::IsThirdPartyWindow(aFirstPartyWindow, aURI)) { + LOG(("Our window isn't a third-party window")); + return true; + } + + uint32_t cookiePermission = 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; + } + + nsAutoCString origin; + nsresult rv = nsContentUtils::GetASCIIOrigin(aURI, origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG_SPEC(("Failed to compute the origin from %s", _spec), aURI); + return false; + } + + nsIPrincipal* parentPrincipal = parentDocument->NodePrincipal(); + + nsAutoCString type; + AntiTrackingUtils::CreateStoragePermissionKey(origin, type); + + return AntiTrackingUtils::CheckStoragePermission( + parentPrincipal, type, + nsContentUtils::IsInPrivateBrowsing(parentDocument), nullptr, 0); +} +} // namespace mozilla diff --git a/toolkit/components/antitracking/StorageAccess.h b/toolkit/components/antitracking/StorageAccess.h new file mode 100644 index 0000000000..24d8e05357 --- /dev/null +++ b/toolkit/components/antitracking/StorageAccess.h @@ -0,0 +1,168 @@ +/* -*- 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); + +/* + * Returns true if this window/channel/aPrincipal should disable storages + * because of the anti-tracking feature. + * Note that either aWindow or aChannel may be null when calling this + * function. If the caller wants the UI to be notified when the storage gets + * disabled, it must pass a non-null channel object. + */ +bool StorageDisabledByAntiTracking(nsPIDOMWindowInner* aWindow, + nsIChannel* aChannel, + nsIPrincipal* aPrincipal, nsIURI* aURI, + uint32_t& aRejectedReason); + +/* + * Returns true if this document should disable storages because of the + * anti-tracking feature. + */ +bool StorageDisabledByAntiTracking(dom::Document* aDocument, nsIURI* aURI, + uint32_t& aRejectedReason); + +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 +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. +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. +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. +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..1a49f7708c --- /dev/null +++ b/toolkit/components/antitracking/StorageAccessAPIHelper.cpp @@ -0,0 +1,1105 @@ +/* -*- 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/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/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 "nsGlobalWindowInner.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 "RejectForeignAllowList.h" +#include "StorageAccess.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::AllowAccessFor( + nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + const StorageAccessAPIHelper::PerformPermissionGrant& aPerformFinalChecks) { + MOZ_ASSERT(aParentContext); + + 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->GetAsciiOrigin(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. + uint32_t behavior = *parentWindowContext->GetCookieBehavior(); + if (!CookieJarSettings::IsRejectThirdPartyContexts(behavior)) { + LOG( + ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out " + "early", + behavior)); + return StorageAccessPermissionGrantPromise::CreateAndResolve(true, + __func__); + } + + MOZ_ASSERT( + CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) || + behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || + behavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); + + // No need to continue when we are already in the allow list. + if (parentWindowContext->GetIsOnContentBlockingAllowList()) { + return StorageAccessPermissionGrantPromise::CreateAndResolve(true, + __func__); + } + + // Make sure storage access isn't disabled + if (!aParentContext->IsTopContent() && + Document::StorageAccessSandboxed(aParentContext->GetSandboxFlags())) { + LOG(("Our document is sandboxed")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + + bool isParentThirdParty = parentWindowContext->GetIsThirdPartyWindow(); + uint64_t topLevelWindowId; + nsAutoCString trackingOrigin; + nsCOMPtr<nsIPrincipal> trackingPrincipal; + + LOG(("The current resource is %s-party", + isParentThirdParty ? "third" : "first")); + + // We are a first party resource. + if (!isParentThirdParty) { + nsAutoCString origin; + nsresult rv = aPrincipal->GetAsciiOrigin(origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("Can't get the origin from the URI")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + + trackingOrigin = origin; + trackingPrincipal = aPrincipal; + topLevelWindowId = aParentContext->GetCurrentInnerWindowId(); + if (NS_WARN_IF(!topLevelWindowId)) { + LOG(("Top-level storage area window id not found, bailing out early")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + + } else { + // We should be a 3rd party source. + if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER && + !parentWindowContext->GetIsThirdPartyTrackingResourceWindow()) { + LOG(("Our window isn't a third-party tracking window")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + if ((CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) || + behavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) && + !isParentThirdParty) { + LOG(("Our window isn't a third-party window")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + + if (!GetTopLevelWindowId(aParentContext, + // Don't request the ETP specific behaviour of + // allowing only singly-nested iframes here, + // because we are recording an allow permission. + nsICookieService::BEHAVIOR_ACCEPT, + topLevelWindowId)) { + LOG(("Error while retrieving the parent window id, bailing out early")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + + // If we can't get the principal and tracking origin at this point, the + // tracking principal will be gotten while running ::CompleteAllowAccessFor + // in the parent. + if (aParentContext->IsInProcess()) { + if (!AntiTrackingUtils::GetPrincipalAndTrackingOrigin( + aParentContext, getter_AddRefs(trackingPrincipal), + trackingOrigin)) { + LOG( + ("Error while computing the parent principal and tracking origin, " + "bailing out early")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + } + } + + // We MAY need information that is only accessible in the parent, + // so we need to determine whether we can run it in the current process (in + // most of cases it should be a child process). + // + // 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. + + // The only case that aParentContext is not in-process is when the heuristic + // is triggered because of user interactions. + MOZ_ASSERT_IF( + !aParentContext->IsInProcess(), + aReason == ContentBlockingNotifier::eOpenerAfterUserInteraction); + + bool runInSameProcess; + if (XRE_IsParentProcess()) { + // If we are already in the parent, then continue to run in the parent. + runInSameProcess = true; + } else { + // 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); + runInSameProcess = + aReason == + ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI || + !isThirdParty; + } else { + runInSameProcess = false; + } + } + + if (runInSameProcess) { + return StorageAccessAPIHelper::CompleteAllowAccessFor( + aParentContext, topLevelWindowId, trackingPrincipal, trackingOrigin, + behavior, aReason, aPerformFinalChecks); + } + + MOZ_ASSERT(XRE_IsContentProcess()); + // 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::CompleteAllowAccessFor( + dom::BrowsingContext* aParentContext, uint64_t aTopLevelWindowId, + nsIPrincipal* aTrackingPrincipal, const nsACString& aTrackingOrigin, + uint32_t aCookieBehavior, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason, + const PerformPermissionGrant& aPerformFinalChecks) { + MOZ_ASSERT(aParentContext); + MOZ_ASSERT_IF(XRE_IsContentProcess(), aParentContext->IsInProcess()); + + nsCOMPtr<nsIPrincipal> trackingPrincipal; + nsAutoCString trackingOrigin; + if (!aTrackingPrincipal) { + // User interaction is the only case that tracking principal is not + // available. + MOZ_ASSERT(XRE_IsParentProcess() && + aReason == ContentBlockingNotifier::eOpenerAfterUserInteraction); + + if (!AntiTrackingUtils::GetPrincipalAndTrackingOrigin( + aParentContext, getter_AddRefs(trackingPrincipal), + trackingOrigin)) { + LOG( + ("Error while computing the parent principal and tracking origin, " + "bailing out early")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + } else { + trackingPrincipal = aTrackingPrincipal; + trackingOrigin = aTrackingOrigin; + } + + LOG(("Tracking origin is %s", PromiseFlatCString(trackingOrigin).get())); + + // We hardcode this block reason since the first-party storage access + // permission is granted for the purpose of blocking trackers. + // Note that if aReason is eOpenerAfterUserInteraction and the + // trackingPrincipal is not in a blocklist, we don't check the + // user-interaction state, because it could be that the current process has + // just sent the request to store the user-interaction permission into the + // parent, without having received the permission itself yet. + // + // 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, + CookieJarSettings::IsRejectThirdPartyWithExceptions(aCookieBehavior) + ? nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN + : nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + + // Ensure we can find the window before continuing, so we can safely + // execute storePermission. + if (aParentContext->IsInProcess() && + (!aParentContext->GetDOMWindow() || + !aParentContext->GetDOMWindow()->GetCurrentInnerWindow())) { + LOG( + ("No window found for our parent browsing context, bailing out " + "early")); + return StorageAccessPermissionGrantPromise::CreateAndReject(false, + __func__); + } + + auto storePermission = + [aParentContext, aTopLevelWindowId, trackingOrigin, trackingPrincipal, + aCookieBehavior, + aReason](int aAllowMode) -> RefPtr<StorageAccessPermissionGrantPromise> { + // Inform the window we granted permission for. This has to be done in the + // window's process. + if (aParentContext->IsInProcess()) { + StorageAccessAPIHelper::OnAllowAccessFor(aParentContext, trackingOrigin, + aCookieBehavior, aReason); + } else { + MOZ_ASSERT(XRE_IsParentProcess()); + + // We don't have the window, send an IPC to the content process that + // owns the parent window. But there is a special case, for window.open, + // we'll return to the content process we need to inform when this + // function is done. So we don't need to create an extra IPC for the case. + if (aReason != ContentBlockingNotifier::eOpener) { + 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. + if (XRE_IsParentProcess() || aParentContext->Top()->IsInProcess()) { + ContentBlockingNotifier::ReportUnblockingToConsole( + aParentContext, NS_ConvertUTF8toUTF16(trackingOrigin), aReason); + + // Set the report reason to nothing if we've already reported. + reportReason = Nothing(); + } else { + // Set the report reason, so that we can know the reason when reporting + // in the parent. + reportReason.emplace(aReason); + } + + if (XRE_IsParentProcess()) { + LOG(("Saving the permission: trackingOrigin=%s", trackingOrigin.get())); + return SaveAccessForOriginOnParentProcess(aTopLevelWindowId, + aParentContext, + trackingPrincipal, aAllowMode) + ->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__); + }); + } + + ContentChild* cc = ContentChild::GetSingleton(); + MOZ_ASSERT(cc); + + LOG( + ("Asking the parent process to save the permission for us: " + "trackingOrigin=%s", + trackingOrigin.get())); + + // This is not really secure, because here we have the content process + // sending the request of storing a permission. + return cc + ->SendStorageAccessPermissionGrantedForOrigin( + aTopLevelWindowId, aParentContext, trackingPrincipal, + trackingOrigin, aAllowMode, reportReason) + ->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. + 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, + CookieJarSettings::IsRejectThirdPartyWithExceptions(aCookieBehavior) + ? nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN + : nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER, + aTrackingOrigin, Some(aReason)); +} + +/* static */ +RefPtr<mozilla::StorageAccessAPIHelper::ParentAccessGrantPromise> +StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess( + uint64_t aTopLevelWindowId, BrowsingContext* aParentContext, + nsIPrincipal* aTrackingPrincipal, int aAllowMode, + 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. + StorageAccessAPIHelper::UpdateAllowAccessOnParentProcess(aParentContext, + trackingOrigin); + + return StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess( + wgp->DocumentPrincipal(), aTrackingPrincipal, aAllowMode, + aExpirationTime); +} + +/* static */ +RefPtr<mozilla::StorageAccessAPIHelper::ParentAccessGrantPromise> +StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess( + nsIPrincipal* aParentPrincipal, nsIPrincipal* aTrackingPrincipal, + int aAllowMode, uint64_t aExpirationTime) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aAllowMode == eAllow || aAllowMode == eAllowAutoGrant); + + if (!aParentPrincipal || !aTrackingPrincipal) { + LOG(("Invalid input arguments passed")); + return ParentAccessGrantPromise::CreateAndReject(false, __func__); + }; + + 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; + 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); + } else 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::AsyncCheckCookiesPermittedDecidesStorageAccessAPI( + 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 aOnRejectForeignAllowlist, 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); + } + if (!StaticPrefs::network_cookie_rejectForeignWithExceptions_enabled()) { + return Some(false); + } + return Some(aOnRejectForeignAllowlist); + 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 (!aThirdParty) { + return Some(true); + } + 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); + // Window doesn't have user activation and we are asking for access -> reject. + if (aRequestingStorageAccess) { + 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); + } + } + + if (aDocument->IsTopLevelContentDocument()) { + return Some(true); + } + + RefPtr<BrowsingContext> bc = aDocument->GetBrowsingContext(); + if (!bc) { + return Some(false); + } + + // We check if the document is a first-party document here by testing if the + // top-level window is same-origin. In non-Fission mode, we can directly get + // the top-level window through the top browsing context since it should be + // in-process. And test their principals. + // + // In fission, if the sub frame's origin differs from the main frame's + // origin, they will be in different processes. We use IsInProcess() + // check here to deterimine whether they have the same origin. In + // non-fission mode, it is always in-process so we need to compare their + // principals. + if (bc->Top()->IsInProcess()) { + nsCOMPtr<nsPIDOMWindowOuter> topOuter = bc->Top()->GetDOMWindow(); + if (!topOuter) { + return Some(false); + } + nsCOMPtr<Document> topLevelDoc = topOuter->GetExtantDoc(); + if (!topLevelDoc) { + return Some(false); + } + + if (topLevelDoc->NodePrincipal()->Equals(aDocument->NodePrincipal())) { + return Some(true); + } + } + + // 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 (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->HasStorageAccessPermissionGranted()) { + return Some(true); + } + return Nothing(); +} + +// static +RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise> +StorageAccessAPIHelper::RequestStorageAccessAsyncHelper( + dom::Document* aDocument, nsPIDOMWindowInner* aInnerWindow, + dom::BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal, + bool aHasUserInteraction, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aNotifier, + bool aRequireGrant) { + MOZ_ASSERT(aDocument); + + if (!aRequireGrant) { + // Try to allow access for the given principal. + return StorageAccessAPIHelper::AllowAccessFor(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, Nothing()); + + // Try to allow access for the given principal. + return StorageAccessAPIHelper::AllowAccessFor( + 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..9be39df1c5 --- /dev/null +++ b/toolkit/components/antitracking/StorageAccessAPIHelper.h @@ -0,0 +1,195 @@ +/* -*- 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 method 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.prg 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> + AllowAccessFor( + 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, + uint64_t aExpirationTime = + StaticPrefs::privacy_restrict3rdpartystorage_expiration()); + + static RefPtr<ParentAccessGrantPromise> SaveAccessForOriginOnParentProcess( + uint64_t aTopLevelWindowId, dom::BrowsingContext* aParentContext, + nsIPrincipal* aTrackingPrincipal, int aAllowMode, + 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>> + AsyncCheckCookiesPermittedDecidesStorageAccessAPI( + 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 aOnRejectForeignAllowlist, 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, + ContentBlockingNotifier::StorageAccessPermissionGrantedReason aNotifier, + bool aRequireGrant); + + private: + friend class dom::ContentParent; + // This should be running either in the parent process or in the child + // processes with an in-process browsing context. + [[nodiscard]] static RefPtr<StorageAccessPermissionGrantPromise> + CompleteAllowAccessFor( + 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..a22b0caee8 --- /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; + +UniquePtr<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 = MakeUnique<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.reset(); + } + + 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..09ff93311a --- /dev/null +++ b/toolkit/components/antitracking/TemporaryAccessGrantObserver.h @@ -0,0 +1,87 @@ +/* -*- 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 "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 UniquePtr<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..9f3b952a65 --- /dev/null +++ b/toolkit/components/antitracking/TrackingDBService.sys.mjs @@ -0,0 +1,375 @@ +/* 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 = {}; + +XPCOMUtils.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, + "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 ( + // 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..481e9a316b --- /dev/null +++ b/toolkit/components/antitracking/URLQueryStringStripper.cpp @@ -0,0 +1,283 @@ +/* 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" + +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; + } + uint32_t numStripped; + return StripQueryString(aURI, strippedURI, &numStripped); +} + +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() { + if (mIsInitialized) { + return NS_OK; + } + mIsInitialized = true; + + mListService = do_GetService("@mozilla.org/query-stripping-list-service;1"); + NS_ENSURE_TRUE(mListService, NS_ERROR_FAILURE); + + return mListService->RegisterAndRunObserver(gQueryStringStripper); +} + +nsresult URLQueryStringStripper::Shutdown() { + if (!mIsInitialized) { + return NS_OK; + } + mIsInitialized = false; + + mList.Clear(); + mAllowList.Clear(); + + MOZ_ASSERT(mListService); + mListService = do_GetService("@mozilla.org/query-stripping-list-service;1"); + + mListService->UnregisterObserver(this); + 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_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; +} + +// 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..c2da0023c9 --- /dev/null +++ b/toolkit/components/antitracking/URLQueryStringStripper.h @@ -0,0 +1,58 @@ +/* -*- 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 "nsStringFwd.h" +#include "nsTHashSet.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); + + [[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; + bool mIsInitialized; +}; + +} // 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..e9be0a3ac4 --- /dev/null +++ b/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs @@ -0,0 +1,307 @@ +/* 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 lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + RemoteSettings: "resource://services-settings/remote-settings.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"; + +XPCOMUtils.defineLazyGetter(lazy, "logger", () => { + return console.createInstance({ + prefix: "URLQueryStrippingListService", + maxLogLevelPref: "privacy.query_stripping.listService.logLevel", + }); +}); + +export class URLQueryStrippingListService { + classId = Components.ID("{afff16f0-3fd2-4153-9ccd-c6d9abd879e4}"); + QueryInterface = ChromeUtils.generateQI(["nsIURLQueryStrippingListService"]); + + #isInitialized = false; + #pendingInit = null; + #initResolver; + + #rs; + #onSyncCallback; + + constructor() { + lazy.logger.debug("constructor"); + this.observers = new Set(); + 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 #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); + } + + _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(); + } + + _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 registerAndRunObserver(observer) { + lazy.logger.debug("registerAndRunObserver", { + isInitialized: this.#isInitialized, + pendingInit: this.#pendingInit, + }); + + await this.#init(); + this.observers.add(observer); + this._notifyObservers(observer); + } + + async unregisterObserver(observer) { + this.observers.delete(observer); + + if (!this.observers.size) { + 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/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/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..412896c200 --- /dev/null +++ b/toolkit/components/antitracking/docs/data-sanitization/index.md @@ -0,0 +1,443 @@ +# 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 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 - in ETP strict mode or Private Browsing Mode. + +### 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). + +From a user perspective it’s difficult to understand what kind of data +is cleared from which UI. The category selection in the “Clear recent +history” dialog is especially confusing. + +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.| + +### 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`. + +#### Clear Data + +- Accessible via `about:preferences#privacy` + +- Clears site data and caches depending on user selection + +- Clears + + - Cookies + + - DOM storages + + - HSTS + + - EME + + - Caches: CSS, Preflight, HSTS + +- Source + + - [clearSiteData.xhtml](https://searchfox.org/mozilla-central/source/browser/components/preferences/dialogs/clearSiteData.xhtml) + + - [clearSiteData.js](https://searchfox.org/mozilla-central/source/browser/components/preferences/dialogs/clearSiteData.js) + + - [clearSiteData.css](https://searchfox.org/mozilla-central/source/browser/components/preferences/dialogs/clearSiteData.css) + +![image3](media/image3.png) + +![image1](media/image1.png) + +#### Clear Recent History + +- Accessible via hamburger menu => History => Clear Recent + history or `about:preferences#privacy` => History => Clear + History + +- Clears a configurable list of categories as [defined in + Sanitizer.jsm](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm#356) + +- Can clear everything or a specific time range + +- Source + + - [sanitize.xhtml](https://searchfox.org/mozilla-central/source/browser/base/content/sanitize.xhtml) + + - [sanitizeDialog.js](https://searchfox.org/mozilla-central/source/browser/base/content/sanitizeDialog.js) + +![image4](media/image4.png) + +#### 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/source/toolkit/components/forgetaboutsite/ForgetAboutSite.sys.mjs) + + - [nsIClearDataService flags + used](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/toolkit/components/cleardata/nsIClearDataService.idl#302-307) + +![image6](media/image6.png) + +![image2](media/image2.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.jsm](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm#356) + +- 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/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/toolkit/components/cleardata/PrincipalsCollector.jsm#72) + 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.jsm [calls the + ClearDataService](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm#1022,1027-1032) + to clear data for every principal from the filtered list + +- Source + + - Most of the sanitize-on-shutdown logic is implemented in + [Sanitizer.jsm](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm) + + - The main entry point is + [sanitizeOnShutdown](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm#790) + + - [Parts of + sanitize-on-shutdown](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm#904-911) + 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) + +#### Manage Cookies and Site Data + +- Accessible via `about:preferences#privacy` => Cookies and Site Data + => Manage Data + +- Clears + \[[flags](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/SiteDataManager.jsm#499,510-514)\] + + - 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/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/SiteDataManager.jsm#366) + +- 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/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/SiteDataManager.jsm#499,510-514)\] + + - 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/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/cleardata/ClearDataService.jsm#85-105). +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..566b0d3b36 --- /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/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/moz.build b/toolkit/components/antitracking/moz.build new file mode 100644 index 0000000000..94073953bf --- /dev/null +++ b/toolkit/components/antitracking/moz.build @@ -0,0 +1,97 @@ +# -*- 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 += [ + "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", + "RejectForeignAllowList.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", + "RejectForeignAllowList.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.ini", + "test/browser/browser.ini", + ] + +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"] + +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..f755eedbac --- /dev/null +++ b/toolkit/components/antitracking/nsITrackingDBService.idl @@ -0,0 +1,64 @@ +/* -*- 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; +}; 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..d8b10943cd --- /dev/null +++ b/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl @@ -0,0 +1,71 @@ +/* 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, function, 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); +}; + +/** + * 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); + + /** + * Unregister an observer. + * + * @param aObserver + * The nsIURLQueryStrippingListObserver object to unregister. + */ + void unregisterObserver(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(); +}; 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..7cb88ea17a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdParty.html @@ -0,0 +1,53 @@ +<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 || + (cookieBehavior == SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN && + SpecialPowers.Services.prefs.getBoolPref("network.cookie.rejectForeignWithExceptions.enabled"))); + 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..1b98e38f83 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/antitracking_head.js @@ -0,0 +1,1448 @@ +/* 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 + ) { + // Normal mode + this.runTest( + name, + callbackTracking, + callbackNonTracking, + cleanupFunction, + extraPrefs, + windowOpenTest, + userInteractionTest, + expectedBlockingNotifications, + false, + iframeSandbox, + accessRemoval, + callbackAfterRemoval + ); + + // Private mode + this.runTest( + name, + callbackTracking, + callbackNonTracking, + cleanupFunction, + extraPrefs, + windowOpenTest, + userInteractionTest, + expectedBlockingNotifications, + true, + iframeSandbox, + accessRemoval, + callbackAfterRemoval + ); + }, + + 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 + ) { + 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, + }); + 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, + }); + 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, + }); + 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, + }); + 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, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name: name + " reject foreign without exception", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: true, + callback: callbackNonTracking, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", false], + ...(extraPrefs || []), + ], + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name: name + " reject foreign with exception", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: true, + callback: callbackNonTracking, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ], + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: false, + callback: callbackTracking, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", false], + ...(extraPrefs || []), + ], + expectedBlockingNotifications: expectedBlockingNotifications + ? Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN + : 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: false, + callback: callbackNonTracking, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ], + expectedBlockingNotifications: false, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: true, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + }); + 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, + }); + 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, + }); + 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 + ); + this._createCleanupTask(cleanupFunction); + + this._createWindowOpenTask( + name, + BEHAVIOR_REJECT_FOREIGN, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + false, + [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ] + ); + this._createCleanupTask(cleanupFunction); + + // Now, check if it works for nested iframes. + this._createWindowOpenTask( + name, + BEHAVIOR_REJECT_TRACKER, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + true, + extraPrefs + ); + this._createCleanupTask(cleanupFunction); + + this._createWindowOpenTask( + name, + BEHAVIOR_REJECT_FOREIGN, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + true, + [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ] + ); + 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 + ); + this._createCleanupTask(cleanupFunction); + + this._createUserInteractionTask( + name, + BEHAVIOR_REJECT_FOREIGN, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + false, + [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ] + ); + this._createCleanupTask(cleanupFunction); + + // Now, check if it works for nested iframes. + this._createUserInteractionTask( + name, + BEHAVIOR_REJECT_TRACKER, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + true, + extraPrefs + ); + this._createCleanupTask(cleanupFunction); + + this._createUserInteractionTask( + name, + BEHAVIOR_REJECT_FOREIGN, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + true, + [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ] + ); + 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 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, + 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); + } + + 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); + } + + 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.loadURIString(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, + }, + ], + 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); + } + + 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 + ) { + 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, + }, + ], + 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); + } + + 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 + ) { + 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, + }, + ], + 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); + } + 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, + }, + ], + 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); + } + 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/browser-blocking.ini b/toolkit/components/antitracking/test/browser/browser-blocking.ini new file mode 100644 index 0000000000..1533d1273f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser-blocking.ini @@ -0,0 +1,72 @@ +[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_blockingDOMCacheSAA.js] +skip-if = socketprocess_networking +[browser_blockingDOMCacheAlwaysPartition.js] +skip-if = socketprocess_networking +[browser_blockingDOMCacheAlwaysPartitionSAA.js] +skip-if = socketprocess_networking +[browser_blockingDOMCache.js] +[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_blockingSessionStorage.js] +[browser_blockingServiceWorkers.js] +[browser_blockingServiceWorkersStorageAccessAPI.js] +[browser_blockingSharedWorkers.js] +skip-if = os == 'linux' && socketprocess_networking +[browser_blockingMessaging.js] +skip-if = os == "linux" && debug #bug 1627094 +[browser_blockingNoOpener.js] +[browser_contentBlockingAllowListPrincipal.js] +support-files = + sandboxed.html + sandboxed.html^headers^ +[browser_contentBlockingTelemetry.js] +skip-if = + win10_2004 && fission && !debug # high frequency intermittent, same as bug 1727097 diff --git a/toolkit/components/antitracking/test/browser/browser.ini b/toolkit/components/antitracking/test/browser/browser.ini new file mode 100644 index 0000000000..52b25a4b60 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser.ini @@ -0,0 +1,226 @@ +[DEFAULT] +skip-if = + os == "linux" && (asan || tsan) # bug 1662229 - task exception + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +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 + # Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + network.cookie.sameSite.laxByDefault=false + # Disable https-first because of explicit http/https testing + dom.security.https_first=false + +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 + 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_aboutblank.js] +[browser_allowListNotifications.js] +[browser_allowListNotifications_alwaysPartition.js] +support-files = subResources.sjs +[browser_addonHostPermissionIgnoredInTP.js] +[browser_allowListSeparationInPrivateAndNormalWindows.js] +skip-if = os == "mac" && !debug # Bug 1503778, 1577362 +[browser_backgroundImageAssertion.js] +[browser_doublyNestedTracker.js] +[browser_emailtracking.js] +[browser_existingCookiesForSubresources.js] +[browser_fileUrl.js] +[browser_firstPartyCookieRejectionHonoursAllowList.js] +[browser_fpiServiceWorkers_fingerprinting.js] +[browser_hasStorageAccess.js] +[browser_hasStorageAccess_alwaysPartition.js] +[browser_iframe_document_open.js] +[browser_imageCache4.js] +[browser_imageCache8.js] +[browser_onBeforeRequestNotificationForTrackingResources.js] +[browser_onModifyRequestNotificationForTrackingResources.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 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_referrerDefaultPolicy.js] +support-files = referrer.sjs +[browser_siteSpecificWorkArounds.js] +[browser_subResources.js] +support-files = subResources.sjs +[browser_subResourcesPartitioned.js] +support-files = subResources.sjs +[browser_subResourcesPartitioned_alwaysPartition.js] +support-files = subResources.sjs +[browser_script.js] +support-files = tracker.js +[browser_userInteraction.js] +[browser_serviceWorkersWithStorageAccessGranted.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_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_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] +[browser_storageAccessWithDynamicFpi.js] +[browser_storageAccessWithHeuristics.js] +[browser_allowPermissionForTracker.js] +[browser_denyPermissionForTracker.js] +[browser_localStorageEvents.js] +[browser_partitionedLocalStorage.js] +[browser_partitionedLocalStorage_events.js] +support-files = localStorageEvents.html +[browser_workerPropagation.js] +support-files = workerIframe.html +[browser_cookieBetweenTabs.js] +[browser_partitionedMessaging.js] +skip-if = true #Bug 1588241 +[browser_partitionedIndexedDB.js] +[browser_partitionedConsoleMessage.js] +[browser_partitionedCookies.js] +support-files = cookies.sjs +[browser_partitionedDOMCache.js] +[browser_partitionedServiceWorkers.js] +support-files = dedicatedWorker.js matchAll.js serviceWorker.js +[browser_partitionedSharedWorkers.js] +support-files = sharedWorker.js partitionedSharedWorker.js +[browser_PBMCookieBehavior.js] +[browser_socialtracking.js] +[browser_socialtracking_save_image.js] +[browser_thirdPartyStorageRejectionForCORS.js] +[browser_urlDecorationStripping.js] +[browser_urlDecorationStripping_alwaysPartition.js] +tags = remote-settings +[browser_urlQueryStringStripping_allowList.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_urlQueryStringStripping.js] +skip-if = + fission && os == "linux" && asan # Bug 1713909 - new Fission platform triage +support-files = file_stripping.html +[browser_urlQueryStringStripping_pbmode.js] +support-files = file_stripping.html +[browser_urlQueryStringStripping_nimbus.js] +support-files = file_stripping.html +[browser_urlQueryStrippingListService.js] +[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_CORS_preflight.js] +support-files = browser_staticPartition_CORS_preflight.sjs +[browser_staticPartition_HSTS.js] +support-files = browser_staticPartition_HSTS.sjs +[browser_staticPartition_saveAs.js] +skip-if = + os == "linux" && bits == 64 # Bug 1775746 + win10_2004 && debug # 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_partitionedClearSiteDataHeader.js] +support-files = + clearSiteData.sjs +[browser_AntiTrackingETPHeuristic.js] 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..a8e69b5e15 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingCookies.js @@ -0,0 +1,183 @@ +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 +); 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..dc7bf0c80e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js @@ -0,0 +1,404 @@ +/** + * 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(); +} + +const expectedExpiredDays = getExpectedExpiredDaysFromPref( + "privacy.restrict3rdpartystorage.expiration" +); +const expectedExpiredDaysRedirect = getExpectedExpiredDaysFromPref( + "privacy.restrict3rdpartystorage.expiration_redirect" +); + +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], + ], + }); + + // 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); + + // 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); + + // 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); + + // 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); + + // 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_fpiServiceWorkers_fingerprinting.js b/toolkit/components/antitracking/test/browser/browser_fpiServiceWorkers_fingerprinting.js new file mode 100644 index 0000000000..fcdc83cde6 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_fpiServiceWorkers_fingerprinting.js @@ -0,0 +1,90 @@ +/* import-globals-from storageAccessAPIHelpers.js */ + +// This test ensures that a service worker for an exempted domain is exempted when it is +// in the first party context, and not exempted when it is in a third party context. + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Check that RFP correctly is exempted and not exempted when FPI is enabled", + async (win3rdParty, win1stParty, allowed) => { + // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency + // It can't be set enternally and then captured because this function gets stringified and then evaled in a new scope. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.firstParty.isolate", false], + ["privacy.resistFingerprinting", false], + ], + }); + + var SPOOFED_HW_CONCURRENCY = 2; + var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + + await SpecialPowers.popPrefEnv(); + + // 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" + ); + } + + // Check DOM cache from the first-party service worker. + let res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "GetHWConcurrency" } + ); + console.info( + "First Party, got: " + + res.value + + " Expected: " + + DEFAULT_HARDWARE_CONCURRENCY + ); + is( + res.value, + DEFAULT_HARDWARE_CONCURRENCY, + "As a first party, HW Concurrency should not be spoofed" + ); + + // Check DOM cache from the third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "GetHWConcurrency" } + ); + console.info( + "Third Party, got: " + res.value + " Expected: " + SPOOFED_HW_CONCURRENCY + ); + is( + res.value, + SPOOFED_HW_CONCURRENCY, + "As a third party, HW Concurrency should be spoofed" + ); + }, + + 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.firstParty.isolate", true], + ["privacy.resistFingerprinting", true], + ["privacy.resistFingerprinting.exemptedDomains", "*.example.com"], + ] +); 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..c51959fe78 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js @@ -0,0 +1,196 @@ +// 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, + thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html", + }, + // 3rd-party no-tracker + { + name: "Test whether 3rd-party non-tracker frame has storage access", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_4TH_PARTY_PAGE, + }, + // 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, + thirdPartyPage: TEST_4TH_PARTY_PAGE, + setup: () => { + let type = "3rdPartyStorage^http://not-tracking.example.com"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add(TEST_DOMAIN, 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, + 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, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + setup: () => { + let type = "3rdPartyStorage^https://tracking.example.org"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add(TEST_DOMAIN, 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, + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }, + // same-origin 3rd-party tracker + { + name: "Test whether same-origin 3rd-party tracker frame has storage access", + topPage: TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "page.html", + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }, +]; + +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 */, + ], + }, + { + behavior: BEHAVIOR_REJECT_FOREIGN, // 1 + hasStorageAccess: [ + true /* same-origin non-tracker */, + false /* 3rd-party non-tracker */, + SpecialPowers.Services.prefs.getBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled" + ) /* 3rd-party tracker with permission */, + false /* 3rd-party tracker */, + SpecialPowers.Services.prefs.getBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled" + ) /* 3rd-party non-tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + ], + }, + { + 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 */, + ], + }, + { + 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 */, + ], + }, + { + 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 */, + true /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // 5 + hasStorageAccess: [ + true /* same-origin non-tracker */, + false /* 3rd-party non-tracker */, + true /* 3rd-party non-tracker with permission */, + false /* 3rd-party tracker */, + true /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + ], + }, +]; + +(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..d89b0b1103 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js @@ -0,0 +1,209 @@ +// 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, + thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html", + }, + // 3rd-party no-tracker + { + name: "Test whether 3rd-party non-tracker frame has storage access", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_4TH_PARTY_PAGE, + }, + // 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, + thirdPartyPage: TEST_4TH_PARTY_PAGE, + setup: () => { + let type = "3rdPartyStorage^http://not-tracking.example.com"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add(TEST_DOMAIN, 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, + 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, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + setup: () => { + let type = "3rdPartyStorage^https://tracking.example.org"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add(TEST_DOMAIN, 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, + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }, + // same-origin 3rd-party tracker + { + name: "Test whether same-origin 3rd-party tracker frame has storage access", + topPage: TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "page.html", + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }, +]; + +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 */, + ], + }, + { + behavior: BEHAVIOR_REJECT_FOREIGN, // 1 + cases: [ + [true] /* same-origin non-tracker */, + [false, foreignBlocked] /* 3rd-party non-tracker */, + [ + SpecialPowers.Services.prefs.getBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled" + ), + foreignBlocked, + ] /* 3rd-party tracker with permission */, + [false, foreignBlocked] /* 3rd-party tracker */, + [ + SpecialPowers.Services.prefs.getBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled" + ), + foreignBlocked, + ] /* 3rd-party non-tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin 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 */, + ], + }, + { + 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 */, + ], + }, + { + 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 */, + [true] /* 3rd-party tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // 5 + cases: [ + [true] /* same-origin non-tracker */, + [false] /* 3rd-party non-tracker */, + [true] /* 3rd-party non-tracker with permission */, + [false, trackerBlocked] /* 3rd-party tracker */, + [true] /* 3rd-party tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin 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, + ], + ], + 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..93fc71b11e --- /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 not available in PBM. +PartitionedStorageHelper.runTest( + "DOMCache", + async (win3rdParty, win1stParty, allowed) => { + await win1stParty.caches.open("wow").then( + async cache => { + ok(false, "DOM Cache should not be available in PBM"); + }, + _ => { + ok(true, "DOM Cache should not be available in PBM"); + } + ); + + await win3rdParty.caches.open("wow").then( + async cache => { + ok(false, "DOM Cache should not be available in PBM"); + }, + _ => { + ok(true, "DOM Cache should not 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_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..f45caba43a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js @@ -0,0 +1,625 @@ +/* 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, + "http://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, + "http://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], + ] +); 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..337d36b6e4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js @@ -0,0 +1,74 @@ +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() + ); + }); + } +); + +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_permissionInNormalWindows.js b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js new file mode 100644 index 0000000000..a414e7363b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js @@ -0,0 +1,106 @@ +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^")) { + 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^")) { + 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..8a53782ba2 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js @@ -0,0 +1,109 @@ +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", _ => { + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + 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^")) { + 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^")) { + 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..8c8944fe2f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js @@ -0,0 +1,50 @@ +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^")) { + 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..defb96004b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js @@ -0,0 +1,49 @@ +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^")) { + 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..35fd83bd31 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionPropagation.js @@ -0,0 +1,425 @@ +/* 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) { + await SpecialPowers.spawn( + browser, + [block, params], + async function (block, params) { + for (let i = 0; ; i++) { + let ifr = content.document.getElementById("ifr" + i); + if (!ifr) { + 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 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"); + 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("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() + ); + }); +}); + +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..5c232fabc2 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js @@ -0,0 +1,149 @@ +/* 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.loadURIString(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..0f7041aaa7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs @@ -0,0 +1,40 @@ +/* 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"; + +Cu.importGlobalProperties(["URLSearchParams"]); + +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..6bacac0af9 --- /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.loadURIString(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.loadURIString( + 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..f6bc072b75 --- /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.loadURIString(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..733eb34240 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js @@ -0,0 +1,372 @@ +/* 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("3rdPartyStorage^") && + 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) { + 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-3rdPartyStorage" + ); + ok(permissionItem, "Permission item exists"); + ok( + BrowserTestUtils.is_visible(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.is_visible(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 = "3rdPartyStorage^https://tracking.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) { + 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..ba109768be --- /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, "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( + "http://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( + "http://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, "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("http://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_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..d83bcf4b7b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js @@ -0,0 +1,214 @@ +/* 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 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 + 0, // no 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 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 + 0 // no blocking notifications +); 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..402ba74ec8 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js @@ -0,0 +1,214 @@ +/* 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 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 + 0, // no 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 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 + 0 // no blocking notifications +); 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..5a8c7970c5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js @@ -0,0 +1,157 @@ +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 = + "http://another-tracking.example.net/browser/browser/base/content/test/protectionsUI/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_HTTP, + errorMessageDomains: [ + "http://tracking.example.org", + "http://tracking.example.org", + "http://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=1, when network.cookie.rejectForeignWithExceptions.enabled is set to false", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", false], + [APS_PREF, false], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: [ + "http://tracking.example.org", + "http://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_HTTP, + errorMessageDomains: [ + "http://tracking.example.org", + "http://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_HTTP, + errorMessageDomains: [ + "http://tracking.example.org", + "http://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..1ba0b89230 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js @@ -0,0 +1,153 @@ +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 = + "http://another-tracking.example.net/browser/browser/base/content/test/protectionsUI/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_HTTP, + errorMessageDomains: [ + "http://tracking.example.org", + "http://tracking.example.org", + "http://tracking.example.org", + "http://tracking.example.org", + "http://trackertest.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=1, when network.cookie.rejectForeignWithExceptions.enabled is set to false", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", false], + [APS_PREF, true], + ], + expectedBlockingNotifications: foreignBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: ["http://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, true]], + expectedBlockingNotifications: allBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: ["http://example.net", "http://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_HTTP, + errorMessageDomains: ["http://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..f50e515c84 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js @@ -0,0 +1,612 @@ +/* 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 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(); +} + +add_task(async function testRedirectHeuristic() { + await runTestRedirectHeuristic(false); +}); + +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_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..b415594662 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js @@ -0,0 +1,96 @@ +// 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: [["network.cookie.rejectForeignWithExceptions.enabled", true]], + 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() + ); + }); +}); + +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: [["network.cookie.rejectForeignWithExceptions.enabled", false]], + 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..6874e4bc62 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js @@ -0,0 +1,159 @@ +"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", + ], + ], + }); + + // 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"); + await BrowserTestUtils.withNewTab(url.href, async browser => { + await verifyQueryString(browser, ""); + }); + testTelemetry({ mc_eid: 2, fbclid: 1, mkt_tok: 1, __s: 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 }); + + 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..1d46986ff1 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js @@ -0,0 +1,249 @@ +/* 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 {} +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 updateEvent = new UpdateEvent(); + let obs = (stripList, allowList) => { + let event = new CustomEvent("update", { detail: { stripList, allowList } }); + updateEvent.dispatchEvent(event); + }; + let promise = waitForEvent(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(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(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 updateEvent = new UpdateEvent(); + let obs = (stripList, allowList) => { + let event = new CustomEvent("update", { detail: { stripList, allowList } }); + updateEvent.dispatchEvent(event); + }; + let promise = waitForEvent(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(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(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..da99cb433b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/head.js @@ -0,0 +1,149 @@ +/* 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_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 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..42574fdb93 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/partitionedstorage_head.js @@ -0,0 +1,455 @@ +/* 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) { + // Normal mode + this.runTest(name, callback, cleanupFunction, extraPrefs, { + runInPrivateWindow: false, + }); + + // Private mode + this.runTest(name, callback, cleanupFunction, extraPrefs, { + runInPrivateWindow: true, + }); + }, + + runTest( + name, + callback, + cleanupFunction, + extraPrefs, + { runInPrivateWindow = false, runInSecureContext = false } = {} + ) { + 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..6a983ec250 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js @@ -0,0 +1,236 @@ +/* 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. + await waitUntilPermission( + `${protocol}://example.net/browser/toolkit/components/antitracking/test/browser/page.html`, + "3rdPartyStorage^" + window.origin + ); + } + + 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..274e128783 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/storage_access_head.js @@ -0,0 +1,248 @@ +/* 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"); + }); + }; +} + +// 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 = false) { + 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/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..84694a35d5 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js @@ -0,0 +1,523 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TRACKING_PAGE = "https://tracking.example.org"; +const TRACKING_PAGE2 = + "https://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(); +} + +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(); + } +}); 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_rejectForeignAllowList.js b/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js new file mode 100644 index 0000000000..97e95a43f4 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js @@ -0,0 +1,116 @@ +"use strict"; + +do_get_profile(); + +// Let's use XPCShellContentUtils to open/close tabs. +const { XPCShellContentUtils } = ChromeUtils.importESModule( + "resource://testing-common/XPCShellContentUtils.sys.mjs" +); + +XPCShellContentUtils.init(this); + +var createHttpServer = (...args) => { + return XPCShellContentUtils.createHttpServer(...args); +}; + +const server = createHttpServer({ + hosts: ["3rdparty.org", "4thparty.org", "foobar.com"], +}); + +async function testThings(prefValue, expected) { + await new Promise(resolve => + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_ALL_CACHES, + resolve + ) + ); + + Services.prefs.setCharPref("privacy.rejectForeign.allowList", prefValue); + + let cookiePromise = new Promise(resolve => { + server.registerPathHandler("/test3rdPartyChannel", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write(`<html><img src="http://3rdparty.org/img" /></html>`); + }); + + server.registerPathHandler("/img", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + resolve(request.hasHeader("Cookie") ? request.getHeader("Cookie") : ""); + response.setHeader("Content-Type", "image/png", false); + response.write("Not an image"); + }); + }); + + // Let's load 3rdparty.org as a 3rd-party. + let contentPage = await XPCShellContentUtils.loadContentPage( + "http://foobar.com/test3rdPartyChannel" + ); + Assert.equal(await cookiePromise, expected, "Cookies received?"); + await contentPage.close(); + + cookiePromise = new Promise(resolve => { + server.registerPathHandler("/test3rdPartyDocument", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write( + `<html><iframe src="http://3rdparty.org/iframe" /></html>` + ); + }); + + server.registerPathHandler("/iframe", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + resolve(request.hasHeader("Cookie") ? request.getHeader("Cookie") : ""); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write(`<html><img src="http://4thparty.org/img" /></html>`); + }); + + server.registerPathHandler("/img", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + resolve(request.hasHeader("Cookie") ? request.getHeader("Cookie") : ""); + response.setHeader("Content-Type", "image/png", false); + response.write("Not an image"); + }); + }); + + // Let's load 3rdparty.org loading a 4th-party. + contentPage = await XPCShellContentUtils.loadContentPage( + "http://foobar.com/test3rdPartyDocument" + ); + Assert.equal(await cookiePromise, expected, "Cookies received?"); + await contentPage.close(); +} + +add_task(async function test_rejectForeignAllowList() { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 1); + Services.prefs.setBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled", + true + ); + + // We don't want to have 'secure' cookies because our test http server doesn't run in https. + Services.prefs.setBoolPref( + "network.cookie.sameSite.noneRequiresSecure", + false + ); + + server.registerPathHandler("/setCookies", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.setHeader("Set-Cookie", "cookie=wow; sameSite=none", true); + response.write("<html></html>"); + }); + + // Let's set a cookie. + let contentPage = await XPCShellContentUtils.loadContentPage( + "http://3rdparty.org/setCookies" + ); + await contentPage.close(); + Assert.equal(Services.cookies.cookies.length, 1); + + // Without exceptionlisting, no cookies should be shared. + await testThings("", ""); + + // Let's exceptionlist 3rdparty.org + await testThings("3rdparty.org", "cookie=wow"); +}); 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..1215ec2384 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js @@ -0,0 +1,177 @@ +/* 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" + ); + ok( + 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..be61a2e6d4 --- /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" + ); + ok( + foundEntryCount > 0, + `Cache entries expected for style.css and OA=${originAttributes}` + ); +} + +add_task(async () => { + do_get_profile(); + + Services.prefs.setBoolPref("network.preload", true); + 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..0f6ded1218 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js @@ -0,0 +1,492 @@ +/* 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" +); + +XPCOMUtils.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], + ], +}; + +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( + "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("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, + 5, + "Events that should not be saved have not been, length is 4" + ); + 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"); + + // 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(); +}); 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.ini b/toolkit/components/antitracking/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..b895aa8e07 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/xpcshell.ini @@ -0,0 +1,51 @@ +[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_cookie_behavior.js] +[test_getPartitionKeyFromURL.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_purge_trackers.js] +skip-if = + win10_2004 # Bug 1718292 + win10_2009 # Bug 1718292 + win11_2009 # Bug 1797751 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +run-sequentially = very high failure rate in parallel + +[test_purge_trackers_telemetry.js] +[test_tracking_db_service.js] +skip-if = toolkit == "android" # Bug 1697936 +[test_rejectForeignAllowList.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 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + socketprocess_networking # Bug 1759035 +run-sequentially = very high failure rate in parallel +[test_staticPartition_image.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_staticPartition_authhttp.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_ExceptionListService.js] +[test_view_source.js] +skip-if = + socketprocess_networking # Bug 1759035 (not as common on win, perma on linux/osx) |