diff options
Diffstat (limited to '')
34 files changed, 1795 insertions, 270 deletions
diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.cpp new file mode 100644 index 0000000000..2c00306441 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.cpp @@ -0,0 +1,22 @@ +/* -*- 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 "BounceTrackingMapEntry.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(BounceTrackingMapEntry, nsIBounceTrackingMapEntry); + +NS_IMETHODIMP BounceTrackingMapEntry::GetSiteHost(nsACString& aSiteHost) { + aSiteHost = mSiteHost; + return NS_OK; +} +NS_IMETHODIMP BounceTrackingMapEntry::GetTimeStamp(int64_t* aTimeStamp) { + *aTimeStamp = mTimeStamp; + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.h new file mode 100644 index 0000000000..45d7237cff --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingMapEntry.h @@ -0,0 +1,35 @@ +/* -*- 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_BounceTrackingMapEntry_h +#define mozilla_BounceTrackingMapEntry_h + +#include "nsIBounceTrackingMapEntry.h" +#include "nsString.h" + +namespace mozilla { + +/** + * Represents an entry in the global bounce tracker or user activation map. + */ +class BounceTrackingMapEntry final : public nsIBounceTrackingMapEntry { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIBOUNCETRACKINGMAPENTRY + + BounceTrackingMapEntry(const nsACString& aSiteHost, PRTime aTimeStamp) + : mSiteHost(aSiteHost), mTimeStamp(aTimeStamp) {} + + private: + ~BounceTrackingMapEntry() = default; + + nsAutoCString mSiteHost; + PRTime mTimeStamp; +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp index 2b0577d5c6..7882cd6d79 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp @@ -7,6 +7,7 @@ #include "BounceTrackingProtectionStorage.h" #include "BounceTrackingState.h" #include "BounceTrackingRecord.h" +#include "BounceTrackingMapEntry.h" #include "BounceTrackingStateGlobal.h" #include "ErrorList.h" @@ -14,6 +15,7 @@ #include "mozilla/ClearOnShutdown.h" #include "mozilla/ContentBlockingAllowList.h" #include "mozilla/Logging.h" +#include "mozilla/Maybe.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_privacy.h" #include "mozilla/dom/Promise.h" @@ -21,6 +23,7 @@ #include "nsHashPropertyBag.h" #include "nsIClearDataService.h" #include "nsIObserverService.h" +#include "nsIPermissionManager.h" #include "nsIPrincipal.h" #include "nsISupports.h" #include "nsServiceManagerUtils.h" @@ -39,6 +42,10 @@ LazyLogModule gBounceTrackingProtectionLog("BounceTrackingProtection"); static StaticRefPtr<BounceTrackingProtection> sBounceTrackingProtection; +// Keeps track of whether the feature is enabled based on pref state. +// Initialized on first call of GetSingleton. +Maybe<bool> BounceTrackingProtection::sFeatureIsEnabled; + static constexpr uint32_t TRACKER_PURGE_FLAGS = nsIClearDataService::CLEAR_ALL_CACHES | nsIClearDataService::CLEAR_COOKIES | nsIClearDataService::CLEAR_DOM_STORAGES | @@ -53,10 +60,33 @@ already_AddRefed<BounceTrackingProtection> BounceTrackingProtection::GetSingleton() { MOZ_ASSERT(XRE_IsParentProcess()); - if (!StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()) { + // First call to GetSingleton, check main feature pref and record telemetry. + if (sFeatureIsEnabled.isNothing()) { + if (StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()) { + sFeatureIsEnabled = Some(true); + + glean::bounce_tracking_protection::enabled_at_startup.Set(true); + glean::bounce_tracking_protection::enabled_dry_run_mode_at_startup.Set( + StaticPrefs::privacy_bounceTrackingProtection_enableDryRunMode()); + } else { + sFeatureIsEnabled = Some(false); + + glean::bounce_tracking_protection::enabled_at_startup.Set(false); + glean::bounce_tracking_protection::enabled_dry_run_mode_at_startup.Set( + false); + + // Feature is disabled. + return nullptr; + } + } + MOZ_ASSERT(sFeatureIsEnabled.isSome()); + + // Feature is disabled. + if (!sFeatureIsEnabled.value()) { return nullptr; } + // Feature is enabled, lazily create singleton instance. if (!sBounceTrackingProtection) { sBounceTrackingProtection = new BounceTrackingProtection(); @@ -67,7 +97,25 @@ BounceTrackingProtection::GetSingleton() { } BounceTrackingProtection::BounceTrackingProtection() { - MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("constructor")); + MOZ_LOG( + gBounceTrackingProtectionLog, LogLevel::Info, + ("Init BounceTrackingProtection. Config: enableDryRunMode: %d, " + "bounceTrackingActivationLifetimeSec: %d, bounceTrackingGracePeriodSec: " + "%d, bounceTrackingPurgeTimerPeriodSec: %d, " + "clientBounceDetectionTimerPeriodMS: %d, requireStatefulBounces: %d, " + "HasMigratedUserActivationData: %d", + StaticPrefs::privacy_bounceTrackingProtection_enableDryRunMode(), + StaticPrefs:: + privacy_bounceTrackingProtection_bounceTrackingActivationLifetimeSec(), + StaticPrefs:: + privacy_bounceTrackingProtection_bounceTrackingGracePeriodSec(), + StaticPrefs:: + privacy_bounceTrackingProtection_bounceTrackingPurgeTimerPeriodSec(), + StaticPrefs:: + privacy_bounceTrackingProtection_clientBounceDetectionTimerPeriodMS(), + StaticPrefs::privacy_bounceTrackingProtection_requireStatefulBounces(), + StaticPrefs:: + privacy_bounceTrackingProtection_hasMigratedUserActivationData())); mStorage = new BounceTrackingProtectionStorage(); @@ -78,6 +126,12 @@ BounceTrackingProtection::BounceTrackingProtection() { return; } + rv = MaybeMigrateUserInteractionPermissions(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Error, + ("user activation permission migration failed")); + } + // Schedule timer for tracker purging. The timer interval is determined by // pref. uint32_t purgeTimerPeriod = StaticPrefs:: @@ -134,6 +188,11 @@ nsresult BounceTrackingProtection::RecordStatefulBounces( // For each host in navigable’s bounce tracking record's bounce set: for (const nsACString& host : record->GetBounceHosts()) { + // Skip "null" entries, they are only used for logging purposes. + if (host.EqualsLiteral("null")) { + continue; + } + // If host equals navigable’s bounce tracking record's initial host, // continue. if (host == record->GetInitialHost()) { @@ -187,9 +246,10 @@ nsresult BounceTrackingProtection::RecordStatefulBounces( } MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, - ("%s: Added candidate to mBounceTrackers: %s, Time: %" PRIu64, + ("%s: Added bounce tracker candidate. siteHost: %s, " + "aBounceTrackingState: %s", __FUNCTION__, PromiseFlatCString(host).get(), - static_cast<uint64_t>(now))); + aBounceTrackingState->Describe().get())); } // Set navigable’s bounce tracking record to null. @@ -220,30 +280,34 @@ nsresult BounceTrackingProtection::RecordStatefulBounces( } nsresult BounceTrackingProtection::RecordUserActivation( - nsIPrincipal* aPrincipal) { + nsIPrincipal* aPrincipal, Maybe<PRTime> aActivationTime) { MOZ_ASSERT(XRE_IsParentProcess()); - NS_ENSURE_ARG_POINTER(aPrincipal); - NS_ENSURE_TRUE(aPrincipal->GetIsContentPrincipal(), NS_ERROR_FAILURE); + + if (!BounceTrackingState::ShouldTrackPrincipal(aPrincipal)) { + return NS_OK; + } nsAutoCString siteHost; nsresult rv = aPrincipal->GetBaseDomain(siteHost); NS_ENSURE_SUCCESS(rv, rv); - MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s: siteHost: %s", __FUNCTION__, siteHost.get())); RefPtr<BounceTrackingStateGlobal> globalState = mStorage->GetOrCreateStateGlobal(aPrincipal); MOZ_ASSERT(globalState); - return globalState->RecordUserActivation(siteHost, PR_Now()); + // aActivationTime defaults to current time if no value is provided. + return globalState->RecordUserActivation(siteHost, + aActivationTime.valueOr(PR_Now())); } NS_IMETHODIMP BounceTrackingProtection::TestGetBounceTrackerCandidateHosts( JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx, - nsTArray<nsCString>& aCandidates) { + nsTArray<RefPtr<nsIBounceTrackingMapEntry>>& aCandidates) { MOZ_ASSERT(aCx); OriginAttributes oa; @@ -254,8 +318,11 @@ BounceTrackingProtection::TestGetBounceTrackerCandidateHosts( BounceTrackingStateGlobal* globalState = mStorage->GetOrCreateStateGlobal(oa); MOZ_ASSERT(globalState); - for (const nsACString& host : globalState->BounceTrackersMapRef().Keys()) { - aCandidates.AppendElement(host); + for (auto iter = globalState->BounceTrackersMapRef().ConstIter(); + !iter.Done(); iter.Next()) { + RefPtr<nsIBounceTrackingMapEntry> candidate = + new BounceTrackingMapEntry(iter.Key(), iter.Data()); + aCandidates.AppendElement(candidate); } return NS_OK; @@ -264,7 +331,7 @@ BounceTrackingProtection::TestGetBounceTrackerCandidateHosts( NS_IMETHODIMP BounceTrackingProtection::TestGetUserActivationHosts( JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx, - nsTArray<nsCString>& aHosts) { + nsTArray<RefPtr<nsIBounceTrackingMapEntry>>& aHosts) { MOZ_ASSERT(aCx); OriginAttributes oa; @@ -275,8 +342,11 @@ BounceTrackingProtection::TestGetUserActivationHosts( BounceTrackingStateGlobal* globalState = mStorage->GetOrCreateStateGlobal(oa); MOZ_ASSERT(globalState); - for (const nsACString& host : globalState->UserActivationMapRef().Keys()) { - aHosts.AppendElement(host); + for (auto iter = globalState->UserActivationMapRef().ConstIter(); + !iter.Done(); iter.Next()) { + RefPtr<nsIBounceTrackingMapEntry> candidate = + new BounceTrackingMapEntry(iter.Key(), iter.Data()); + aHosts.AppendElement(candidate); } return NS_OK; @@ -419,6 +489,11 @@ BounceTrackingProtection::TestAddUserActivation( return stateGlobal->RecordUserActivation(host, aActivationTime); } +NS_IMETHODIMP +BounceTrackingProtection::TestMaybeMigrateUserInteractionPermissions() { + return MaybeMigrateUserInteractionPermissions(); +} + RefPtr<BounceTrackingProtection::PurgeBounceTrackersMozPromise> BounceTrackingProtection::PurgeBounceTrackers() { // Prevent multiple purge operations from running at the same time. @@ -468,7 +543,7 @@ BounceTrackingProtection::PurgeBounceTrackers() { aResults) { MOZ_ASSERT(aResults.IsResolve(), "AllSettled never rejects"); - MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s: Done. Cleared %zu hosts.", __FUNCTION__, aResults.ResolveValue().Length())); @@ -572,6 +647,10 @@ nsresult BounceTrackingProtection::PurgeBounceTrackersForStateGlobal( __FUNCTION__, PromiseFlatCString(host).get(), originAttributeSuffix.get())); } + // Remove allow-listed host so we don't need to check in again next purge + // run. If it gets classified again and the allow-list entry gets removed + // it will be purged in the next run. + bounceTrackerCandidatesToRemove.AppendElement(host); continue; } @@ -581,15 +660,23 @@ nsresult BounceTrackingProtection::PurgeBounceTrackersForStateGlobal( new ClearDataMozPromise::Private(__func__); RefPtr<ClearDataCallback> cb = new ClearDataCallback(clearPromise, host); - MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, - ("%s: Purge state for host: %s", __FUNCTION__, - PromiseFlatCString(host).get())); - - // TODO: Bug 1842067: Clear by site + OA. - rv = clearDataService->DeleteDataFromBaseDomain(host, false, - TRACKER_PURGE_FLAGS, cb); - if (NS_WARN_IF(NS_FAILED(rv))) { - clearPromise->Reject(0, __func__); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + ("%s: Purging bounce tracker. siteHost: %s, bounceTime: %" PRIu64 + " aStateGlobal: %s", + __FUNCTION__, PromiseFlatCString(host).get(), bounceTime, + aStateGlobal->Describe().get())); + + if (StaticPrefs::privacy_bounceTrackingProtection_enableDryRunMode()) { + // In dry-run mode, we don't actually clear the data, but we still want to + // resolve the promise to indicate that the data would have been cleared. + clearPromise->Resolve(host, __func__); + } else { + // TODO: Bug 1842067: Clear by site + OA. + rv = clearDataService->DeleteDataFromBaseDomain(host, false, + TRACKER_PURGE_FLAGS, cb); + if (NS_WARN_IF(NS_FAILED(rv))) { + clearPromise->Reject(0, __func__); + } } aClearPromises.AppendElement(clearPromise); @@ -642,22 +729,129 @@ nsresult BounceTrackingProtection::ClearExpiredUserInteractions( return NS_OK; } +nsresult BounceTrackingProtection::MaybeMigrateUserInteractionPermissions() { + // Only run the migration once. + if (StaticPrefs:: + privacy_bounceTrackingProtection_hasMigratedUserActivationData()) { + return NS_OK; + } + + MOZ_LOG( + gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Importing user activation data from permissions", __FUNCTION__)); + + // Get all user activation permissions that are within our user activation + // lifetime. We don't care about the rest since they are considered expired + // for BTP. + + nsresult rv = NS_OK; + nsCOMPtr<nsIPermissionManager> permManager = + do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(permManager, NS_ERROR_FAILURE); + + // Construct the since time param. The permission manager expects epoch in + // miliseconds. + int64_t nowMS = PR_Now() / PR_USEC_PER_MSEC; + int64_t activationLifetimeMS = + static_cast<int64_t>( + StaticPrefs:: + privacy_bounceTrackingProtection_bounceTrackingActivationLifetimeSec()) * + PR_MSEC_PER_SEC; + int64_t since = nowMS - activationLifetimeMS; + MOZ_ASSERT(since > 0); + + // Get all user activation permissions last modified between "since" and now. + nsTArray<RefPtr<nsIPermission>> userActivationPermissions; + rv = permManager->GetAllByTypeSince("storageAccessAPI"_ns, since, + userActivationPermissions); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Found %zu (non-expired) user activation permissions", + __FUNCTION__, userActivationPermissions.Length())); + + for (const auto& perm : userActivationPermissions) { + nsCOMPtr<nsIPrincipal> permPrincipal; + + rv = perm->GetPrincipal(getter_AddRefs(permPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + MOZ_ASSERT(permPrincipal); + + // The time the permission was last modified is the time of last user + // activation. + int64_t modificationTimeMS; + rv = perm->GetModificationTime(&modificationTimeMS); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(modificationTimeMS >= since, + "Unexpected permission modification time"); + + // We may end up with duplicates here since user activation permissions are + // tracked by origin, while BTP tracks user activation by site host. + // RecordUserActivation is responsible for only keeping the most recent user + // activation flag for a given site host and needs to make sure existing + // activation flags are not overwritten by older timestamps. + // RecordUserActivation expects epoch in microseconds. + rv = RecordUserActivation(permPrincipal, + Some(modificationTimeMS * PR_USEC_PER_MSEC)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + } + + // Migration successful, set the pref to indicate that we have migrated. + return mozilla::Preferences::SetBool( + "privacy.bounceTrackingProtection.hasMigratedUserActivationData", true); +} + // ClearDataCallback NS_IMPL_ISUPPORTS(BounceTrackingProtection::ClearDataCallback, nsIClearDataCallback); +BounceTrackingProtection::ClearDataCallback::ClearDataCallback( + ClearDataMozPromise::Private* aPromise, const nsACString& aHost) + : mHost(aHost), mClearDurationTimer(0), mPromise(aPromise) { + MOZ_ASSERT(!aHost.IsEmpty(), "Host must not be empty"); + if (!StaticPrefs::privacy_bounceTrackingProtection_enableDryRunMode()) { + // Only collect timing information when actually performing the deletion + mClearDurationTimer = + glean::bounce_tracking_protection::purge_duration.Start(); + MOZ_ASSERT(mClearDurationTimer); + } +}; + +BounceTrackingProtection::ClearDataCallback::~ClearDataCallback() { + mPromise->Reject(0, __func__); + if (mClearDurationTimer) { + glean::bounce_tracking_protection::purge_duration.Cancel( + std::move(mClearDurationTimer)); + } +} + // nsIClearDataCallback implementation NS_IMETHODIMP BounceTrackingProtection::ClearDataCallback::OnDataDeleted( uint32_t aFailedFlags) { if (aFailedFlags) { mPromise->Reject(aFailedFlags, __func__); } else { - MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s: Cleared %s", __FUNCTION__, mHost.get())); mPromise->Resolve(std::move(mHost), __func__); } + RecordClearDurationTelemetry(); return NS_OK; } +void BounceTrackingProtection::ClearDataCallback:: + RecordClearDurationTelemetry() { + if (mClearDurationTimer) { + glean::bounce_tracking_protection::purge_duration.StopAndAccumulate( + std::move(mClearDurationTimer)); + mClearDurationTimer = 0; + } +} + } // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h index e99cf895be..98b235ead1 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h @@ -6,8 +6,10 @@ #include "mozilla/Logging.h" #include "mozilla/MozPromise.h" +#include "mozilla/glean/GleanMetrics.h" #include "nsIBounceTrackingProtection.h" #include "nsIClearDataService.h" +#include "mozilla/Maybe.h" class nsIPrincipal; class nsITimer; @@ -37,8 +39,11 @@ class BounceTrackingProtection final : public nsIBounceTrackingProtection { [[nodiscard]] nsresult RecordStatefulBounces( BounceTrackingState* aBounceTrackingState); - // Stores a user activation flag with a timestamp for the given principal. - [[nodiscard]] nsresult RecordUserActivation(nsIPrincipal* aPrincipal); + // Stores a user activation flag with a timestamp for the given principal. The + // timestamp defaults to the current time, but can be overridden via + // aActivationTime. + [[nodiscard]] nsresult RecordUserActivation( + nsIPrincipal* aPrincipal, Maybe<PRTime> aActivationTime = Nothing()); // Clears expired user interaction flags for the given state global. If // aStateGlobal == nullptr, clears expired user interaction flags for all @@ -50,6 +55,10 @@ class BounceTrackingProtection final : public nsIBounceTrackingProtection { BounceTrackingProtection(); ~BounceTrackingProtection() = default; + // Keeps track of whether the feature is enabled based on pref state. + // Initialized on first call of GetSingleton. + static Maybe<bool> sFeatureIsEnabled; + // Timer which periodically runs PurgeBounceTrackers. nsCOMPtr<nsITimer> mBounceTrackingPurgeTimer; @@ -82,15 +91,23 @@ class BounceTrackingProtection final : public nsIBounceTrackingProtection { NS_DECL_NSICLEARDATACALLBACK explicit ClearDataCallback(ClearDataMozPromise::Private* aPromise, - const nsACString& aHost) - : mHost(aHost), mPromise(aPromise){}; + const nsACString& aHost); private: - virtual ~ClearDataCallback() { mPromise->Reject(0, __func__); } + virtual ~ClearDataCallback(); nsCString mHost; + + void RecordClearDurationTelemetry(); + + glean::TimerId mClearDurationTimer; RefPtr<ClearDataMozPromise::Private> mPromise; }; + + // Imports user activation permissions from permission manager if needed. This + // is important so we don't purge data for sites the user has interacted with + // before the feature was enabled. + [[nodiscard]] nsresult MaybeMigrateUserInteractionPermissions(); }; } // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp index bdd2c7dc18..d3a7cb9062 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp @@ -15,9 +15,8 @@ #include "mozStorageCID.h" #include "mozilla/Components.h" #include "mozilla/Monitor.h" -#include "mozilla/AppShutdown.h" +#include "mozilla/IntegerPrintfMacros.h" #include "mozilla/Services.h" -#include "mozilla/ShutdownPhase.h" #include "nsCOMPtr.h" #include "nsDirectoryServiceUtils.h" #include "nsIObserverService.h" @@ -464,19 +463,19 @@ nsresult BounceTrackingProtectionStorage::Init() { nsresult rv = self->CreateDatabaseConnection(); if (NS_WARN_IF(NS_FAILED(rv))) { self->mErrored.Flip(); - self->mMonitor.Notify(); + self->mMonitor.NotifyAll(); return; } rv = self->LoadMemoryStateFromDisk(); if (NS_WARN_IF(NS_FAILED(rv))) { self->mErrored.Flip(); - self->mMonitor.Notify(); + self->mMonitor.NotifyAll(); return; } self->mInitialized.Flip(); - self->mMonitor.Notify(); + self->mMonitor.NotifyAll(); }), NS_DISPATCH_EVENT_MAY_BLOCK); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp index d4b33f9edb..c522895638 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp @@ -29,6 +29,8 @@ const nsACString& BounceTrackingRecord::GetFinalHost() const { } void BounceTrackingRecord::AddBounceHost(const nsACString& aHost) { + MOZ_ASSERT(!aHost.IsEmpty()); + mBounceHosts.Insert(aHost); MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s: %s", __FUNCTION__, Describe().get())); @@ -54,6 +56,8 @@ nsCString BounceTrackingRecord::DescribeSet(const nsTHashSet<nsCString>& set) { } void BounceTrackingRecord::AddStorageAccessHost(const nsACString& aHost) { + MOZ_ASSERT(!aHost.IsEmpty()); + mStorageAccessHosts.Insert(aHost); } diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp index b4af3daa07..08716b44ae 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp @@ -29,6 +29,7 @@ #include "mozilla/ClearOnShutdown.h" #include "nsTHashMap.h" #include "mozilla/dom/Element.h" +#include "mozilla/dom/WindowContext.h" namespace mozilla { @@ -54,8 +55,13 @@ BounceTrackingState::~BounceTrackingState() { // static already_AddRefed<BounceTrackingState> BounceTrackingState::GetOrCreate( - dom::BrowsingContextWebProgress* aWebProgress) { - MOZ_ASSERT(aWebProgress); + dom::BrowsingContextWebProgress* aWebProgress, nsresult& aRv) { + aRv = NS_OK; + + if (!aWebProgress) { + aRv = NS_ERROR_INVALID_ARG; + return nullptr; + } if (!ShouldCreateBounceTrackingStateForWebProgress(aWebProgress)) { return nullptr; @@ -73,8 +79,8 @@ already_AddRefed<BounceTrackingState> BounceTrackingState::GetOrCreate( sStorageObserver = new BounceTrackingStorageObserver(); ClearOnShutdown(&sStorageObserver); - DebugOnly<nsresult> rv = sStorageObserver->Init(); - NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to init storage observer"); + aRv = sStorageObserver->Init(); + NS_ENSURE_SUCCESS(aRv, nullptr); } dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext(); @@ -82,7 +88,8 @@ already_AddRefed<BounceTrackingState> BounceTrackingState::GetOrCreate( return nullptr; } uint64_t browserId = browsingContext->BrowserId(); - bool createdNew; + bool createdNew = false; + RefPtr<BounceTrackingState> bounceTrackingState = do_AddRef(sBounceTrackingStates->LookupOrInsertWith(browserId, [&] { createdNew = true; @@ -90,8 +97,10 @@ already_AddRefed<BounceTrackingState> BounceTrackingState::GetOrCreate( })); if (createdNew) { - nsresult rv = bounceTrackingState->Init(aWebProgress); - if (NS_WARN_IF(NS_FAILED(rv))) { + aRv = bounceTrackingState->Init(aWebProgress); + if (NS_FAILED(aRv)) { + NS_WARNING("Failed to initialize BounceTrackingState."); + sBounceTrackingStates->Remove(browserId); return nullptr; } } @@ -116,6 +125,10 @@ void BounceTrackingState::ResetAllForOriginAttributesPattern( nsresult BounceTrackingState::Init( dom::BrowsingContextWebProgress* aWebProgress) { + MOZ_ASSERT(!mIsInitialized, + "BounceTrackingState must not be initialized twice."); + mIsInitialized = true; + NS_ENSURE_ARG_POINTER(aWebProgress); NS_ENSURE_TRUE( StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup(), @@ -156,9 +169,10 @@ nsCString BounceTrackingState::Describe() { OriginAttributesRef().CreateSuffix(oaSuffix); return nsPrintfCString( - "{ mBounceTrackingRecord: %s, mOriginAttributes: %s }", + "{ mBounceTrackingRecord: %s, mOriginAttributes: %s, mBrowserId: %" PRIu64 + " }", mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() : "null", - oaSuffix.get()); + oaSuffix.get(), mBrowserId); } // static @@ -212,6 +226,28 @@ bool BounceTrackingState::ShouldCreateBounceTrackingStateForWebProgress( } // static +bool BounceTrackingState::ShouldTrackPrincipal(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aPrincipal); + + // Only track content principals. + if (!aPrincipal->GetIsContentPrincipal()) { + return false; + } + + // Skip non http schemes. + if (!aPrincipal->SchemeIs("http") && !aPrincipal->SchemeIs("https")) { + return false; + } + + // Skip partitioned principals. + if (!aPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()) { + return false; + } + + return true; +} + +// static nsresult BounceTrackingState::HasBounceTrackingStateForSite( const nsACString& aSiteHost, bool& aResult) { aResult = false; @@ -285,6 +321,10 @@ nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) { nsresult rv = aChannel->GetLoadInfo(getter_AddRefs(loadInfo)); NS_ENSURE_SUCCESS(rv, rv); + // Used to keep track of whether we added entries to the site list that are + // not "null". + bool siteListIsEmpty = true; + // Collect uri list including any redirects. nsTArray<nsCString> siteList; @@ -294,8 +334,8 @@ nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) { rv = redirectHistoryEntry->GetPrincipal(getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); - // Filter out non-content principals. - if (!principal->GetIsContentPrincipal()) { + if (!BounceTrackingState::ShouldTrackPrincipal(principal)) { + siteList.AppendElement("null"_ns); continue; } @@ -303,7 +343,12 @@ nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) { rv = principal->GetBaseDomain(baseDomain); NS_ENSURE_SUCCESS(rv, rv); - siteList.AppendElement(baseDomain); + if (NS_WARN_IF(baseDomain.IsEmpty())) { + siteList.AppendElement("null"); + } else { + siteList.AppendElement(baseDomain); + siteListIsEmpty = false; + } } // Add site via the current URI which is the end of the chain. @@ -311,18 +356,35 @@ nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) { rv = aChannel->GetURI(getter_AddRefs(channelURI)); NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr<nsIEffectiveTLDService> tldService = - do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv); - NS_ENSURE_SUCCESS(rv, rv); + if (channelURI->SchemeIs("http") || channelURI->SchemeIs("https")) { + nsCOMPtr<nsIEffectiveTLDService> tldService = + do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); - nsAutoCString siteHost; - rv = tldService->GetSchemelessSite(channelURI, siteHost); + nsAutoCString siteHost; + rv = tldService->GetSchemelessSite(channelURI, siteHost); - if (NS_FAILED(rv)) { - NS_WARNING("Failed to retrieve site for final channel URI."); + // Skip URIs where we can't get a site host. + if (NS_FAILED(rv)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Failed to get site host from channelURI: %s", __FUNCTION__, + channelURI->GetSpecOrDefault().get())); + siteList.AppendElement("null"_ns); + } else { + MOZ_ASSERT(!siteHost.IsEmpty(), "siteHost should not be empty."); + siteList.AppendElement(siteHost); + siteListIsEmpty = false; + } } - siteList.AppendElement(siteHost); + // Do not record empty site lists. This can happen if none of the principals + // are suitable for tracking. It includes when OnDocumentStartRequest is + // called for the initial about:blank. + if (siteListIsEmpty) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: skip empty site list.", __FUNCTION__)); + return NS_OK; + } return OnResponseReceived(siteList); } @@ -437,16 +499,16 @@ nsresult BounceTrackingState::OnStartNavigation( // If origin is an opaque origin, set initialHost to empty host. Strictly // speaking we only need to check IsNullPrincipal, but we're generally only - // interested in content principals. Other principal types are not considered - // to be trackers. - if (!aTriggeringPrincipal->GetIsContentPrincipal()) { - siteHost = ""; - } - - // obtain site - nsresult rv = aTriggeringPrincipal->GetBaseDomain(siteHost); - if (NS_WARN_IF(NS_FAILED(rv))) { + // interested in content principals with http/s scheme. Other principal types + // or schemes are not considered to be trackers. + if (!BounceTrackingState::ShouldTrackPrincipal(aTriggeringPrincipal)) { siteHost = ""; + } else { + // obtain site + nsresult rv = aTriggeringPrincipal->GetBaseDomain(siteHost); + if (NS_WARN_IF(NS_FAILED(rv))) { + siteHost = ""; + } } // If navigable’s bounce tracking record is null: Set navigable’s bounce @@ -474,7 +536,7 @@ nsresult BounceTrackingState::OnStartNavigation( ("%s: site: %s, hasUserActivation? %d", __FUNCTION__, siteHost.get(), hasUserActivation)); if (hasUserActivation) { - rv = mBounceTrackingProtection->RecordStatefulBounces(this); + nsresult rv = mBounceTrackingProtection->RecordStatefulBounces(this); NS_ENSURE_SUCCESS(rv, rv); MOZ_ASSERT(!mBounceTrackingRecord); @@ -485,7 +547,11 @@ nsresult BounceTrackingState::OnStartNavigation( } // There is no transient user activation. Add host as a bounce candidate. - mBounceTrackingRecord->AddBounceHost(siteHost); + if (siteHost.IsEmpty()) { + mBounceTrackingRecord->AddBounceHost("null"_ns); + } else { + mBounceTrackingRecord->AddBounceHost(siteHost); + } return NS_OK; } @@ -494,7 +560,12 @@ nsresult BounceTrackingState::OnStartNavigation( nsresult BounceTrackingState::OnResponseReceived( const nsTArray<nsCString>& aSiteList) { - NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE); +#ifdef DEBUG + MOZ_ASSERT(!aSiteList.IsEmpty(), "siteList should not be empty."); + for (const nsCString& site : aSiteList) { + MOZ_ASSERT(!site.IsEmpty(), "site should not be an empty string."); + } +#endif // Logging if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { @@ -510,6 +581,9 @@ nsresult BounceTrackingState::OnResponseReceived( siteListStr.get())); } + // Record should exist by now. It gets created in OnStartNavigation. + NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE); + // Check if there is still an active timeout. This shouldn't happen since // OnStartNavigation already cancels it. if (NS_WARN_IF(mClientBounceDetectionTimeout)) { @@ -567,9 +641,6 @@ nsresult BounceTrackingState::OnDocumentLoaded( nsIPrincipal* aDocumentPrincipal) { NS_ENSURE_ARG_POINTER(aDocumentPrincipal); - // Assert: navigable’s bounce tracking record is not null. - NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE); - // Logging if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { nsAutoCString origin; @@ -578,14 +649,15 @@ nsresult BounceTrackingState::OnDocumentLoaded( origin = "err"; } MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, - ("%s: origin: %s, mBounceTrackingRecord: %s", __FUNCTION__, - origin.get(), - mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() - : "null")); + ("%s: origin: %s, this: %s", __FUNCTION__, origin.get(), + Describe().get())); } + // Assert: navigable’s bounce tracking record is not null. + NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE); + nsAutoCString siteHost; - if (!aDocumentPrincipal->GetIsContentPrincipal()) { + if (!BounceTrackingState::ShouldTrackPrincipal(aDocumentPrincipal)) { siteHost = ""; } else { nsresult rv = aDocumentPrincipal->GetBaseDomain(siteHost); @@ -614,4 +686,36 @@ nsresult BounceTrackingState::OnCookieWrite(const nsACString& aSiteHost) { return NS_OK; } +nsresult BounceTrackingState::OnStorageAccess(nsIPrincipal* aPrincipal) { + NS_ENSURE_ARG_POINTER(aPrincipal); + // The caller should already filter out principals for us. + MOZ_ASSERT(BounceTrackingState::ShouldTrackPrincipal(aPrincipal)); + + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString origin; + nsresult rv = aPrincipal->GetOrigin(origin); + if (NS_FAILED(rv)) { + origin = "err"; + } + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: origin: %s, mBounceTrackingRecord: %s", __FUNCTION__, + origin.get(), + mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() + : "null")); + } + + if (!mBounceTrackingRecord) { + return NS_OK; + } + + nsAutoCString siteHost; + nsresult rv = aPrincipal->GetBaseDomain(siteHost); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(!siteHost.IsEmpty(), NS_ERROR_FAILURE); + + mBounceTrackingRecord->AddStorageAccessHost(siteHost); + + return NS_OK; +} + } // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h index 17d324bda9..a9e186206a 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h @@ -45,7 +45,7 @@ class BounceTrackingState : public nsIWebProgressListener, // return nullptr if the given web progress / browsing context is not suitable // (see ShouldCreateBounceTrackingStateForWebProgress). static already_AddRefed<BounceTrackingState> GetOrCreate( - dom::BrowsingContextWebProgress* aWebProgress); + dom::BrowsingContextWebProgress* aWebProgress, nsresult& aRv); // Reset state for all BounceTrackingState instances this includes resetting // BounceTrackingRecords and cancelling any running timers. @@ -62,31 +62,35 @@ class BounceTrackingState : public nsIWebProgressListener, // Callback for when we received a response from the server and are about to // create a document for the response. Calls into // BounceTrackingState::OnResponseReceived. - nsresult OnDocumentStartRequest(nsIChannel* aChannel); + [[nodiscard]] nsresult OnDocumentStartRequest(nsIChannel* aChannel); // At the start of a navigation, either initialize a new bounce tracking // record, or append a client-side redirect to the current bounce tracking // record. // Should only be called for top level content navigations. - nsresult OnStartNavigation(nsIPrincipal* aTriggeringPrincipal, - const bool aHasValidUserGestureActivation); + [[nodiscard]] nsresult OnStartNavigation( + nsIPrincipal* aTriggeringPrincipal, + const bool aHasValidUserGestureActivation); // Record sites which have written cookies in the current extended // navigation. - nsresult OnCookieWrite(const nsACString& aSiteHost); + [[nodiscard]] nsresult OnCookieWrite(const nsACString& aSiteHost); // Whether the given BrowsingContext should hold a BounceTrackingState // instance to monitor bounce tracking navigations. static bool ShouldCreateBounceTrackingStateForBC( dom::CanonicalBrowsingContext* aBrowsingContext); + // Whether the given principal should be tracked for bounce tracking. + static bool ShouldTrackPrincipal(nsIPrincipal* aPrincipal); + // Check if there is a BounceTrackingState which current browsing context is // associated with aSiteHost. // This is an approximation for checking if a given site is currently loaded // in the top level context, e.g. in a tab. See Bug 1842047 for adding a more // accurate check that calls into the browser implementations. - static nsresult HasBounceTrackingStateForSite(const nsACString& aSiteHost, - bool& aResult); + [[nodiscard]] static nsresult HasBounceTrackingStateForSite( + const nsACString& aSiteHost, bool& aResult); // Get the currently associated BrowsingContext. Returns nullptr if it has not // been attached yet. @@ -99,10 +103,16 @@ class BounceTrackingState : public nsIWebProgressListener, // Create a string that describes this object. Used for logging. nsCString Describe(); + // Record sites which have accessed storage in the current extended + // navigation. + [[nodiscard]] nsresult OnStorageAccess(nsIPrincipal* aPrincipal); + private: explicit BounceTrackingState(); virtual ~BounceTrackingState(); + bool mIsInitialized{false}; + uint64_t mBrowserId{}; // OriginAttributes associated with the browser this state is attached to. @@ -130,25 +140,20 @@ class BounceTrackingState : public nsIWebProgressListener, dom::BrowsingContextWebProgress* aWebProgress); // Init to be called after creation, attaches nsIWebProgressListener. - nsresult Init(dom::BrowsingContextWebProgress* aWebProgress); + [[nodiscard]] nsresult Init(dom::BrowsingContextWebProgress* aWebProgress); // When the response is received at the end of a navigation, fill the // bounce set. - nsresult OnResponseReceived(const nsTArray<nsCString>& aSiteList); + [[nodiscard]] nsresult OnResponseReceived( + const nsTArray<nsCString>& aSiteList); // When the document is loaded at the end of a navigation, update the // final host. - nsresult OnDocumentLoaded(nsIPrincipal* aDocumentPrincipal); - - // TODO: Bug 1839918: Detection of stateful bounces. - - // Record sites which have accessed storage in the current extended - // navigation. - nsresult OnStorageAccess(); + [[nodiscard]] nsresult OnDocumentLoaded(nsIPrincipal* aDocumentPrincipal); // Record sites which have activated service workers in the current // extended navigation. - nsresult OnServiceWorkerActivation(); + [[nodiscard]] nsresult OnServiceWorkerActivation(); }; } // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp index b94c887c90..bcd9e57bd8 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp @@ -9,6 +9,7 @@ #include "ErrorList.h" #include "mozilla/Assertions.h" #include "mozilla/Logging.h" +#include "mozilla/IntegerPrintfMacros.h" #include "nsIPrincipal.h" namespace mozilla { @@ -42,6 +43,19 @@ nsresult BounceTrackingStateGlobal::RecordUserActivation( __FUNCTION__, PromiseFlatCString(aSiteHost).get())); } + // Make sure we don't overwrite an existing, more recent user activation. This + // is only relevant for callers that pass in a timestamp that isn't PR_Now(), + // e.g. when importing user activation data. + Maybe<PRTime> existingUserActivation = mUserActivation.MaybeGet(aSiteHost); + if (existingUserActivation.isSome() && + existingUserActivation.value() >= aTime) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip: A more recent user activation " + "already exists for %s", + __FUNCTION__, PromiseFlatCString(aSiteHost).get())); + return NS_OK; + } + mUserActivation.InsertOrUpdate(aSiteHost, aTime); if (aSkipStorage || !ShouldPersistToDisk()) { diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp index cc9c3ce971..fcc806e0f4 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp @@ -6,14 +6,15 @@ #include "BounceTrackingState.h" #include "mozilla/Services.h" -#include "mozilla/StaticPrefs_privacy.h" -#include "mozilla/ClearOnShutdown.h" -#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/WindowGlobalParent.h" #include "nsCOMPtr.h" #include "nsICookieNotification.h" #include "nsIObserverService.h" #include "mozilla/dom/BrowsingContext.h" #include "nsICookie.h" +#include "nsIPrincipal.h" namespace mozilla { @@ -69,13 +70,25 @@ BounceTrackingStorageObserver::Observe(nsISupports* aSubject, return NS_OK; } - // Check if the cookie is partitioned. Partitioned cookies can not be used for - // bounce tracking. + // Filter http(s) cookies nsCOMPtr<nsICookie> cookie; rv = notification->GetCookie(getter_AddRefs(cookie)); NS_ENSURE_SUCCESS(rv, rv); MOZ_ASSERT(cookie); + nsICookie::schemeType schemeMap; + rv = cookie->GetSchemeMap(&schemeMap); + NS_ENSURE_SUCCESS(rv, rv); + + if (!(schemeMap & (nsICookie::schemeType::SCHEME_HTTP | + nsICookie::schemeType::SCHEME_HTTPS))) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("Skipping non-HTTP(S) cookie.")); + return NS_OK; + } + + // Check if the cookie is partitioned. Partitioned cookies can not be used for + // bounce tracking. if (!cookie->OriginAttributesNative().mPartitionKey.IsEmpty()) { MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, ("Skipping partitioned cookie.")); @@ -104,4 +117,65 @@ BounceTrackingStorageObserver::Observe(nsISupports* aSubject, return bounceTrackingState->OnCookieWrite(baseDomain); } +// static +nsresult BounceTrackingStorageObserver::OnInitialStorageAccess( + dom::WindowContext* aWindowContext) { + NS_ENSURE_ARG_POINTER(aWindowContext); + + if (!XRE_IsParentProcess()) { + // Check if the principal needs to be tracked for bounce tracking. Checking + // this in the content process may save us IPC to the parent. + nsIPrincipal* storagePrincipal = + aWindowContext->GetInnerWindow()->GetEffectiveStoragePrincipal(); + if (!BounceTrackingState::ShouldTrackPrincipal(storagePrincipal)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("%s: Skipping principal (content process).", __FUNCTION__)); + return NS_OK; + } + + dom::WindowGlobalChild* windowGlobalChild = + aWindowContext->GetWindowGlobalChild(); + NS_ENSURE_TRUE(windowGlobalChild, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(windowGlobalChild->SendOnInitialStorageAccess(), + NS_ERROR_FAILURE); + + return NS_OK; + } + + MOZ_ASSERT(XRE_IsParentProcess()); + nsCOMPtr<nsIPrincipal> storagePrincipal = + aWindowContext->Canonical()->DocumentStoragePrincipal(); + NS_ENSURE_TRUE(storagePrincipal, NS_ERROR_FAILURE); + + if (!BounceTrackingState::ShouldTrackPrincipal(storagePrincipal)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("%s: Skipping principal.", __FUNCTION__)); + return NS_OK; + } + + if (!storagePrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("Skipping partitioned storage access.")); + return NS_OK; + } + + dom::BrowsingContext* browsingContext = aWindowContext->GetBrowsingContext(); + NS_ENSURE_TRUE(browsingContext, NS_ERROR_FAILURE); + + nsresult rv = NS_OK; + RefPtr<BounceTrackingState> bounceTrackingState = + BounceTrackingState::GetOrCreate( + browsingContext->Top()->Canonical()->GetWebProgress(), rv); + NS_ENSURE_SUCCESS(rv, rv); + + // We may not always get a BounceTrackingState, e.g. if the feature is + // disabled or we don't keep track of bounce tracking for the given + // BrowsingContext. + if (!bounceTrackingState) { + return NS_OK; + } + + return bounceTrackingState->OnStorageAccess(storagePrincipal); +} + } // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h index 1e76c85a3c..50fdc79c64 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h @@ -9,6 +9,10 @@ namespace mozilla { +namespace dom { +class WindowContext; +} + extern LazyLogModule gBounceTrackingProtectionLog; class BounceTrackingStorageObserver final : public nsIObserver { @@ -17,7 +21,10 @@ class BounceTrackingStorageObserver final : public nsIObserver { public: BounceTrackingStorageObserver() = default; - nsresult Init(); + [[nodiscard]] nsresult Init(); + + [[nodiscard]] static nsresult OnInitialStorageAccess( + dom::WindowContext* aWindowContext); private: ~BounceTrackingStorageObserver() = default; diff --git a/toolkit/components/antitracking/bouncetrackingprotection/metrics.yaml b/toolkit/components/antitracking/bouncetrackingprotection/metrics.yaml new file mode 100644 index 0000000000..a298fa6d4c --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/metrics.yaml @@ -0,0 +1,64 @@ +# 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/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Core :: Privacy: Anti-Tracking' + +bounce.tracking.protection: + purge_duration: + type: timing_distribution + description: > + For every purge that is scheduled, we call the ClearDataService to + purge persistent storage for each detected bounce tracker. This may + do some blocking work on main thread and dispatch some cleanups to + other threads. + Collect telemetry on how long it takes to clear in the wild to + determine whether we need to improve performance here. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1890582 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1890582#c4 + data_sensitivity: + - technical + notification_emails: + - pbz@mozilla.com + - bvandersloot@mozilla.com + - manuel@mozilla.com + expires: 130 + enabled_at_startup: + type: boolean + description: > + Keeps track of whether the feature is enabled at startup. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1893964 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1893964#c4 + data_sensitivity: + - technical + notification_emails: + - pbz@mozilla.com + - bvandersloot@mozilla.com + - manuel@mozilla.com + expires: never + enabled_dry_run_mode_at_startup: + type: boolean + description: > + Keeps track of whether the feature is enabled and running in dry-run mode + at startup. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1893964 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1893964#c4 + data_sensitivity: + - technical + notification_emails: + - pbz@mozilla.com + - bvandersloot@mozilla.com + - manuel@mozilla.com + expires: never diff --git a/toolkit/components/antitracking/bouncetrackingprotection/moz.build b/toolkit/components/antitracking/bouncetrackingprotection/moz.build index 09107bb782..cc35d81bd9 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/moz.build +++ b/toolkit/components/antitracking/bouncetrackingprotection/moz.build @@ -8,6 +8,7 @@ with Files("**"): BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking") XPIDL_SOURCES += [ + "nsIBounceTrackingMapEntry.idl", "nsIBounceTrackingProtection.idl", ] @@ -19,6 +20,7 @@ XPCOM_MANIFESTS += [ ] EXPORTS.mozilla += [ + "BounceTrackingMapEntry.h", "BounceTrackingProtection.h", "BounceTrackingProtectionStorage.h", "BounceTrackingRecord.h", @@ -28,6 +30,7 @@ EXPORTS.mozilla += [ ] UNIFIED_SOURCES += [ + "BounceTrackingMapEntry.cpp", "BounceTrackingProtection.cpp", "BounceTrackingProtectionStorage.cpp", "BounceTrackingRecord.cpp", diff --git a/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingMapEntry.idl b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingMapEntry.idl new file mode 100644 index 0000000000..2e2f1823ab --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingMapEntry.idl @@ -0,0 +1,17 @@ +/* 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" + +/** + * This interface represents an entry in the global bounce tracker or user activation map. + */ +[scriptable, uuid(51B0B5AE-0AC2-4A3C-8C7E-3523FA42881B)] +interface nsIBounceTrackingMapEntry : nsISupports { + // The host of the site that has received user activation or is a bounce tracker candidate. + readonly attribute ACString siteHost; + // The time when the user activation or bounce tracker candidate was added. + // This is a PRTime, which is USEC since the epoch. + readonly attribute int64_t timeStamp; +}; diff --git a/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl index 1163492333..d52113118e 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl +++ b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsISupports.idl" +#include "nsIBounceTrackingMapEntry.idl" [scriptable, uuid(4866F748-29DA-4C10-8EAA-ED2F7851E6B1)] interface nsIBounceTrackingProtection : nsISupports { @@ -37,14 +38,20 @@ interface nsIBounceTrackingProtection : nsISupports { // State is keyed by OriginAttributes. [implicit_jscontext] - Array<ACString> testGetBounceTrackerCandidateHosts(in jsval originAttributes); + Array<nsIBounceTrackingMapEntry> testGetBounceTrackerCandidateHosts(in jsval originAttributes); [implicit_jscontext] - Array<ACString> testGetUserActivationHosts(in jsval originAttributes); + Array<nsIBounceTrackingMapEntry> testGetUserActivationHosts(in jsval originAttributes); [implicit_jscontext] void testAddBounceTrackerCandidate(in jsval originAttributes, in ACString aSiteHost, in PRTime aBounceTime); [implicit_jscontext] void testAddUserActivation(in jsval originAttributes, in ACString aSiteHost, in PRTime aActivationTime); + + // Test helper to trigger user activation import from the permission + // manager. Will only import if the pref + // privacy.bounceTrackingProtection.hasMigratedUserActivationData is set to + // false. + void testMaybeMigrateUserInteractionPermissions(); }; diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml index 1c44d7804e..0e8a01db4a 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml @@ -4,17 +4,31 @@ prefs = [ "privacy.bounceTrackingProtection.enabled=true", "privacy.bounceTrackingProtection.enableTestMode=true", "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0", + "privacy.bounceTrackingProtection.enableDryRunMode=false", ] support-files = [ "file_start.html", "file_bounce.sjs", "file_bounce.html", + "file_web_worker.js", ] +["browser_bouncetracking_dry_run.js"] + ["browser_bouncetracking_oa_isolation.js"] +["browser_bouncetracking_popup.js"] + ["browser_bouncetracking_purge.js"] +["browser_bouncetracking_schemes.js"] + ["browser_bouncetracking_simple.js"] -["browser_bouncetracking_stateful.js"] +["browser_bouncetracking_stateful_cookies.js"] + +["browser_bouncetracking_stateful_storage.js"] + +["browser_bouncetracking_stateful_web_worker.js"] + +["browser_bouncetracking_telemetry_purge_duration.js"] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_dry_run.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_dry_run.js new file mode 100644 index 0000000000..627a5f233a --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_dry_run.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_ORIGIN = "https://itisatracker.org"; +const TEST_BASE_DOMAIN = "itisatracker.org"; + +async function runPurgeTest(expectPurge) { + ok(!SiteDataTestUtils.hasCookies(TEST_ORIGIN), "No cookies initially."); + + // Adding a cookie that should later be purged. + info("Add a test cookie to be purged later."); + SiteDataTestUtils.addToCookies({ origin: TEST_ORIGIN }); + ok(SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookie added."); + + // The bounce adds localStorage. Test that there is none initially. + ok( + !SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN), + "No localStorage initially." + ); + + info("Test client bounce with cookie."); + await runTestBounce({ + bounceType: "client", + setState: "localStorage", + skipSiteDataCleanup: true, + postBounceCallback: () => { + info( + "Test that after the bounce but before purging cookies and localStorage are present." + ); + ok(SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookies not purged."); + ok( + SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN), + "localStorage not purged." + ); + + Assert.deepEqual( + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost), + [TEST_BASE_DOMAIN], + `Bounce tracker candidate '${TEST_BASE_DOMAIN}' added` + ); + }, + }); + + if (expectPurge) { + info("After purging the site shouldn't have any data."); + ok(!SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookies purged."); + ok(!SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN), "localStorage purged."); + } else { + info("Purging did not run meaning the site should still have data."); + + ok(SiteDataTestUtils.hasCookies(TEST_ORIGIN), "Cookies still set."); + ok( + SiteDataTestUtils.hasLocalStorage(TEST_ORIGIN), + "localStorage still set." + ); + } + + info( + "Candidates should have been removed after running the purging algorithm. This is true for both regular and dry-run mode where we pretend to purge." + ); + Assert.deepEqual( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}), + [], + "No bounce tracker candidates after purging." + ); + + // Cleanup. + bounceTrackingProtection.clearAll(); + await SiteDataTestUtils.clear(); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + // Required to use SiteDataTestUtils localStorage helpers. + ["dom.storage.client_validation", false], + ], + }); +}); + +add_task(async function test_purge_in_regular_mode() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.bounceTrackingProtection.enableDryRunMode", false]], + }); + + await runPurgeTest(true); +}); + +add_task(async function test_purge_in_dry_run_mode() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.bounceTrackingProtection.enableDryRunMode", true]], + }); + + await runPurgeTest(false); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_popup.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_popup.js new file mode 100644 index 0000000000..9e6fa8caf1 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_popup.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); +}); + +async function runTest(spawnWindowType) { + if (!spawnWindowType || !["newTab", "popup"].includes(spawnWindowType)) { + throw new Error(`Invalid option '${spawnWindowType}' for spawnWindowType`); + } + + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker hosts initially." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activation hosts initially." + ); + + // Spawn a tab with A, the start of the bounce chain. + await BrowserTestUtils.withNewTab( + getBaseUrl(ORIGIN_A) + "file_start.html", + async browser => { + // The destination site C to navigate to after the bounce. + let finalURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"); + // The middle hop in the bounce chain B that redirects to finalURL C. + let bounceURL = getBounceURL({ + bounceType: "client", + targetURL: finalURL, + setState: "cookie-client", + }); + + // Register a promise for the new popup window. This resolves once the popup + // has opened and the final url (C) has been loaded. + let openPromise; + + if (spawnWindowType == "newTab") { + openPromise = BrowserTestUtils.waitForNewTab(gBrowser, finalURL.href); + } else { + openPromise = BrowserTestUtils.waitForNewWindow({ url: finalURL.href }); + } + + // Navigate through the bounce chain by opening a popup to the bounce URL. + await navigateLinkClick(browser, bounceURL, { + spawnWindow: spawnWindowType, + }); + + let tabOrWindow = await openPromise; + + let tabOrWindowBrowser; + if (spawnWindowType == "newTab") { + tabOrWindowBrowser = tabOrWindow.linkedBrowser; + } else { + tabOrWindowBrowser = tabOrWindow.gBrowser.selectedBrowser; + } + + let promiseRecordBounces = waitForRecordBounces(tabOrWindowBrowser); + + // Navigate again with user gesture which triggers + // BounceTrackingProtection::RecordStatefulBounces. We could rely on the + // timeout (mClientBounceDetectionTimeout) here but that can cause races + // in debug where the load is quite slow. + await navigateLinkClick( + tabOrWindowBrowser, + new URL(getBaseUrl(ORIGIN_C) + "file_start.html") + ); + + info("Wait for bounce trackers to be recorded."); + await promiseRecordBounces; + + // Cleanup popup or tab. + if (spawnWindowType == "newTab") { + await BrowserTestUtils.removeTab(tabOrWindow); + } else { + await BrowserTestUtils.closeWindow(tabOrWindow); + } + } + ); + + // Check that the bounce tracker was detected. + Assert.deepEqual( + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost), + [SITE_TRACKER], + "Bounce tracker in popup detected." + ); + + // Cleanup. + bounceTrackingProtection.clearAll(); + await SiteDataTestUtils.clear(); +} + +/** + * Tests that bounce trackers which use popups as the first hop in the bounce + * chain can not bypass detection. + * + * A -> popup -> B -> C + * + * A opens a popup and loads B in it. B is the tracker that performs a + * short-lived redirect and C is the final destination. + */ + +add_task(async function test_popup() { + await runTest("popup"); +}); + +add_task(async function test_new_tab() { + await runTest("newTab"); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js index eedd374197..676ac5fe92 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js @@ -133,8 +133,21 @@ add_task(async function test_purging_skip_content_blocking_allow_list() { "Should only purge example.net. example.org is within the grace period, example.com is allow-listed." ); + // example.net is removed because it is purged, example.com is removed because + // it is allow-listed. + Assert.deepEqual( + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost), + ["example.org"], + "Should have removed example.net and example.com from bounce tracker candidate list." + ); + + info("Add example.com as a bounce tracker candidate again."); + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.com", 1); + info( - "Remove the allow-list entry for example.com and test that it gets purged now." + "Remove the allow-list entry for example.com and test that it gets purged now." ); await BrowserTestUtils.withNewTab("https://example.com", async browser => { @@ -146,6 +159,16 @@ add_task(async function test_purging_skip_content_blocking_allow_list() { "example.com should have been purged now that it is no longer allow-listed." ); + // example.org is still in the grace period so it neither gets purged nor + // removed from the candidate list. + Assert.deepEqual( + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost), + ["example.org"], + "Should have removed example.com from bounce tracker candidate list." + ); + bounceTrackingProtection.clearAll(); }); @@ -166,8 +189,24 @@ add_task( "Should only purge example.net. example.org is within the grace period, example.com is allow-listed via test1.example.com." ); + // example.net is removed because it is purged, example.com is removed because it is allow-listed. + Assert.deepEqual( + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost), + ["example.org"], + "Should have removed example.net and example.com from bounce tracker candidate list." + ); + + info("Add example.com as a bounce tracker candidate again."); + bounceTrackingProtection.testAddBounceTrackerCandidate( + {}, + "example.com", + 1 + ); + info( - "Remove the allow-list entry for test1.example.com and test that it gets purged now." + "Remove the allow-list entry for test1.example.com and test that it gets purged now." ); await BrowserTestUtils.withNewTab( @@ -179,7 +218,7 @@ add_task( Assert.deepEqual( await bounceTrackingProtection.testRunPurgeBounceTrackers(), ["example.com"], - "example.com should have been purged now that test1.example.com it is no longer allow-listed." + "example.com should have been purged now that test1.example.com is no longer allow-listed." ); bounceTrackingProtection.clearAll(); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_schemes.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_schemes.js new file mode 100644 index 0000000000..21b72cdc45 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_schemes.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); + bounceTrackingProtection.clearAll(); +}); + +async function testInteractWithSite(origin, expectRecorded) { + is( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activation hosts initially" + ); + + let baseDomain; + let scheme; + + await BrowserTestUtils.withNewTab(origin, async browser => { + baseDomain = browser.contentPrincipal.baseDomain; + scheme = browser.contentPrincipal.URI.scheme; + + info( + `Trigger a user activation, which should ${ + expectRecorded ? "" : "not " + }be recorded.` + ); + // We intentionally turn off this a11y check, because the following click + // is purposefully sent on an arbitrary web content that is not expected + // to be tested by itself with the browser mochitests, therefore this rule + // check shall be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + await BrowserTestUtils.synthesizeMouseAtPoint(50, 50, {}, browser); + AccessibilityUtils.resetEnv(); + }); + if (expectRecorded) { + Assert.deepEqual( + bounceTrackingProtection + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost), + [baseDomain], + `User activation should be recorded for ${scheme} scheme.` + ); + } else { + Assert.deepEqual( + bounceTrackingProtection.testGetUserActivationHosts({}), + [], + `User activation should not be recorded for ${scheme} scheme.` + ); + } + + bounceTrackingProtection.clearAll(); +} + +/** + * Test that we only record user activation for supported schemes. + */ +add_task(async function test_userActivationSchemes() { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await testInteractWithSite("http://example.com", true); + await testInteractWithSite("https://example.com", true); + + await testInteractWithSite("about:blank", false); + await testInteractWithSite("about:robots", false); + await testInteractWithSite( + "file://" + Services.dirsvc.get("TmpD", Ci.nsIFile).path, + false + ); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js index dfbd4d0fc0..cdc21eb788 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js @@ -73,12 +73,18 @@ add_task(async function test_bounce_chain() { await promiseRecordBounces; Assert.deepEqual( - bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(), + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost) + .sort(), [SITE_TRACKER_B, SITE_TRACKER].sort(), `Identified all bounce trackers in the redirect chain.` ); Assert.deepEqual( - bounceTrackingProtection.testGetUserActivationHosts({}).sort(), + bounceTrackingProtection + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost) + .sort(), [SITE_A, SITE_B].sort(), "Should only have user activation for sites where we clicked links." ); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js deleted file mode 100644 index e7fb4521a7..0000000000 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js +++ /dev/null @@ -1,63 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -let bounceTrackingProtection; - -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - ["privacy.bounceTrackingProtection.requireStatefulBounces", true], - ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], - ], - }); - bounceTrackingProtection = Cc[ - "@mozilla.org/bounce-tracking-protection;1" - ].getService(Ci.nsIBounceTrackingProtection); -}); - -// Cookie tests. - -add_task(async function test_bounce_stateful_cookies_client() { - info("Test client bounce with cookie."); - await runTestBounce({ - bounceType: "client", - setState: "cookie-client", - }); - info("Test client bounce without cookie."); - await runTestBounce({ - bounceType: "client", - setState: null, - expectCandidate: false, - expectPurge: false, - }); -}); - -add_task(async function test_bounce_stateful_cookies_server() { - info("Test server bounce with cookie."); - await runTestBounce({ - bounceType: "server", - setState: "cookie-server", - }); - info("Test server bounce without cookie."); - await runTestBounce({ - bounceType: "server", - setState: null, - expectCandidate: false, - expectPurge: false, - }); -}); - -// Storage tests. - -// TODO: Bug 1848406: Implement stateful bounce detection for localStorage. -add_task(async function test_bounce_stateful_localStorage() { - info("TODO: client bounce with localStorage."); - await runTestBounce({ - bounceType: "client", - setState: "localStorage", - expectCandidate: false, - expectPurge: false, - }); -}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_cookies.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_cookies.js new file mode 100644 index 0000000000..d4940e668d --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_cookies.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); +}); + +// Cookie tests. + +add_task(async function test_bounce_stateful_cookies_client() { + info("Test client bounce with cookie."); + await runTestBounce({ + bounceType: "client", + setState: "cookie-client", + }); + info("Test client bounce without cookie."); + await runTestBounce({ + bounceType: "client", + setState: null, + expectCandidate: false, + expectPurge: false, + }); +}); + +add_task(async function test_bounce_stateful_cookies_client_sameSiteFrame() { + info("Test client bounce with cookie set in same site frame."); + await runTestBounce({ + bounceType: "client", + setState: "cookie-client", + setStateSameSiteFrame: true, + }); +}); + +add_task(async function test_bounce_stateful_cookies_server() { + info("Test server bounce with cookie."); + await runTestBounce({ + bounceType: "server", + setState: "cookie-server", + }); + info("Test server bounce without cookie."); + await runTestBounce({ + bounceType: "server", + setState: null, + expectCandidate: false, + expectPurge: false, + }); +}); + +add_task(async function test_bounce_stateful_cookies_server_sameSiteFrame() { + info("Test client bounce with cookie set in same site frame."); + await runTestBounce({ + bounceType: "server", + setState: "cookie-server", + setStateSameSiteFrame: true, + }); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_storage.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_storage.js new file mode 100644 index 0000000000..ff9daabcdb --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_storage.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); +}); + +// Storage tests. + +add_task(async function test_bounce_stateful_localStorage() { + info("Client bounce with localStorage."); + await runTestBounce({ + bounceType: "client", + setState: "localStorage", + }); +}); + +add_task(async function test_bounce_stateful_localStorage_sameSiteFrame() { + info("Client bounce with localStorage set in same site frame."); + await runTestBounce({ + bounceType: "client", + setState: "localStorage", + setStateSameSiteFrame: true, + }); +}); + +add_task(async function test_bounce_stateful_indexedDB() { + info("Client bounce with indexedDB."); + await runTestBounce({ + bounceType: "client", + setState: "indexedDB", + }); +}); + +add_task(async function test_bounce_stateful_indexedDB_sameSiteFrame() { + info("Client bounce with indexedDB populated in same site frame."); + await runTestBounce({ + bounceType: "client", + setState: "indexedDB", + setStateSameSiteFrame: true, + }); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_web_worker.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_web_worker.js new file mode 100644 index 0000000000..1e6672e784 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful_web_worker.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); +}); + +add_task(async function test_bounce_stateful_indexedDB() { + info("Client bounce with indexedDB."); + await runTestBounce({ + bounceType: "client", + setState: "indexedDB", + setStateInWebWorker: true, + }); +}); + +// FIXME: (Bug 1889898) This test is skipped because it triggers a shutdown +// hang. +add_task(async function test_bounce_stateful_indexedDB_nestedWorker() { + info("Client bounce with indexedDB access from a nested worker."); + await runTestBounce({ + bounceType: "client", + setState: "indexedDB", + setStateInNestedWebWorker: true, + }); +}).skip(); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_telemetry_purge_duration.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_telemetry_purge_duration.js new file mode 100644 index 0000000000..74b1fdb30d --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_telemetry_purge_duration.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +async function test_purge_duration(isDryRunMode) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.bounceTrackingProtection.enableDryRunMode", isDryRunMode]], + }); + + is( + Glean.bounceTrackingProtection.purgeDuration.testGetValue(), + null, + "Histogram should not exist initially." + ); + + info("Run server bounce with cookie."); + await runTestBounce({ + bounceType: "server", + setState: "cookie-server", + postBounceCallback: () => { + is( + Glean.bounceTrackingProtection.purgeDuration.testGetValue(), + null, + "Histogram should still be empty after bounce, because we haven't purged yet." + ); + }, + }); + + let events = Glean.bounceTrackingProtection.purgeDuration.testGetValue(); + if (isDryRunMode) { + is(events, null, "Should not collect purge timining in dry mode"); + } else { + is(events.count, 1, "Histogram should contain one value."); + } + + // Cleanup + Services.fog.testResetFOG(); + await SpecialPowers.popPrefEnv(); + bounceTrackingProtection.clearAll(); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); + + // Clear telemetry before test. + Services.fog.testResetFOG(); +}); + +add_task(async function test_purge_duration_dry_mode() { + await test_purge_duration(true); +}); + +add_task(async function test_purge_duration_enabled() { + await test_purge_duration(false); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html index 2756555fa5..d7aa117481 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html @@ -1,59 +1,150 @@ <!DOCTYPE html> <html> - <head> - <meta charset="utf-8" /> - <meta http-equiv="X-UA-Compatible" content="IE=edge" /> - <title>Bounce!</title> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - </head> - <body> - <p>Nothing to see here...</p> - <script> - // Wrap the entire block so we can run async code. - (async () => { - let url = new URL(location.href); - - let redirectDelay = url.searchParams.get("redirectDelay"); - if(redirectDelay != null) { - redirectDelay = Number.parseInt(redirectDelay); - } else { - redirectDelay = 50; - } +<head> + <meta charset="utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <title>Bounce!</title> + <meta name="viewport" content="width=device-width, initial-scale=1" /> +</head> + +<body> + <p id="test-config"></p> + <script> + const SET_STATE_HANDLERS = { + "cookie-client": setCookie, + "localStorage": setLocalStorage, + "indexedDB": setIndexedDB, + }; + + function setCookie(id) { + let cookie = document.cookie; + if (cookie) { + console.info("Received cookie", cookie); + } else { + let newCookie = `id=${id}`; + console.info("Setting new cookie", newCookie); + document.cookie = newCookie; + } + } + + function setLocalStorage(id) { + let entry = localStorage.getItem("id"); + if (entry) { + console.info("Found localStorage entry. id", entry); + } else { + console.info("Setting new localStorage entry. id", id); + localStorage.setItem(id, id); + } + } + + function setIndexedDB() { + return new Promise((resolve, reject) => { + let request = window.indexedDB.open("bounce", 1); + request.onsuccess = () => { + console.info("Opened indexedDB"); + resolve() + }; + request.onerror = (event) => { + console.error("Error opening indexedDB", event); + reject(); + }; + request.onupgradeneeded = (event) => { + console.info("Initializing indexedDB"); + let db = event.target.result; + db.createObjectStore("bounce"); + }; + }); + } + + function setIndexedDBInWorker(nested = false) { + let worker = new Worker("file_web_worker.js"); + let msg = nested ? "setIndexedDBNested" : "setIndexedDB"; + worker.postMessage(msg); + return new Promise((resolve, reject) => { + worker.onmessage = () => { + console.info("IndexedDB set in worker"); + resolve(); + }; + worker.onerror = (event) => { + console.error("Error setting indexedDB in worker", event); + reject(); + }; + }); + } + + /** + * Set a state in a child frame. + */ + function setStateInFrame() { + // Embed self + let iframe = document.createElement("iframe"); + + let src = new URL(location.href); + // Remove search params we don't need for the iframe. + src.searchParams.delete("target"); + src.searchParams.delete("redirectDelay"); + src.searchParams.delete("setStateSameSiteFrame"); + iframe.src = src.href; + + let frameReadyPromise = new Promise((resolve) => { + iframe.addEventListener("load", () => { + iframe.contentWindow.readyPromise.then(resolve); + }); + }); + document.body.appendChild(iframe); + + return frameReadyPromise; + } + + // Wrap the entire block so we can run async code. Store the result in a + // promise so that parent windows can wait for us to be ready. + window.readyPromise = (async () => { + let url = new URL(location.href); + // Display the test config in the body. + document.getElementById("test-config").innerText = JSON.stringify(Object.fromEntries(url.searchParams), null, 2); + + if (url.searchParams.get("setStateSameSiteFrame") === "true") { + // Set state in a child frame. + await setStateInFrame(url); + } else if(url.searchParams.get("setStateInWebWorker") === "true") { + // Set state in a worker. + await setIndexedDBInWorker(); + } else if(url.searchParams.get("setStateInNestedWebWorker") === "true") { + // Set state in a nested worker. + await setIndexedDBInWorker(true); + } else { + // Set a state in this window. let setState = url.searchParams.get("setState"); if (setState) { let id = Math.random().toString(); - if (setState == "cookie-client") { - let cookie = document.cookie; - - if (cookie) { - console.info("Received cookie", cookie); - } else { - let newCookie = `id=${id}`; - console.info("Setting new cookie", newCookie); - document.cookie = newCookie; - } - } else if (setState == "localStorage") { - let entry = localStorage.getItem("id"); - - if (entry) { - console.info("Found localStorage entry. id", entry); - } else { - console.info("Setting new localStorage entry. id", id); - localStorage.setItem(id, id); - } + let handler = SET_STATE_HANDLERS[setState]; + if (!handler) { + throw new Error("Unknown state handler: " + setState); } + await handler(id); } + } + + // Redirect to the target URL after a delay. + // If no target is specified, do nothing. + let redirectDelay = url.searchParams.get("redirectDelay"); + if (redirectDelay != null) { + redirectDelay = Number.parseInt(redirectDelay); + } else { + redirectDelay = 50; + } + + let target = url.searchParams.get("target"); + if (target) { + console.info("Redirecting to", target); + setTimeout(() => { + location.href = target; + }, redirectDelay); + } + })(); + </script> +</body> - let target = url.searchParams.get("target"); - if (target) { - console.info("redirecting to", target); - setTimeout(() => { - location.href = target; - }, redirectDelay); - } - })(); - </script> - </body> </html> diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_web_worker.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_web_worker.js new file mode 100644 index 0000000000..78fdf2a149 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_web_worker.js @@ -0,0 +1,40 @@ +// A web worker which can set indexedDB. + +function setIndexedDB() { + return new Promise((resolve, reject) => { + let request = self.indexedDB.open("bounce", 1); + request.onsuccess = () => { + console.info("Opened indexedDB"); + resolve(); + }; + request.onerror = event => { + console.error("Error opening indexedDB", event); + reject(); + }; + request.onupgradeneeded = event => { + console.info("Initializing indexedDB"); + let db = event.target.result; + db.createObjectStore("bounce"); + }; + }); +} + +self.onmessage = function (event) { + console.info("Web worker received message", event.data); + + if (event.data === "setIndexedDB") { + setIndexedDB().then(() => { + self.postMessage("indexedDBSet"); + }); + } else if (event.data === "setIndexedDBNested") { + console.info("set state nested"); + // Rather than setting indexedDB in this worker spawn a nested worker to set + // indexedDB. + let nestedWorker = new Worker("file_web_worker.js"); + nestedWorker.postMessage("setIndexedDB"); + nestedWorker.onmessage = () => { + console.info("IndexedDB set in nested worker"); + self.postMessage("indexedDBSet"); + }; + } +}; diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js index f5857b6919..71d9acedc6 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js @@ -3,6 +3,17 @@ "use strict"; +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "bounceTrackingProtection", + "@mozilla.org/bounce-tracking-protection;1", + "nsIBounceTrackingProtection" +); + const SITE_A = "example.com"; const ORIGIN_A = `https://${SITE_A}`; @@ -25,13 +36,6 @@ const OBSERVER_MSG_RECORD_BOUNCES_FINISHED = "test-record-bounces-finished"; const ROOT_DIR = getRootDirectory(gTestPath); -XPCOMUtils.defineLazyServiceGetter( - this, - "bounceTrackingProtection", - "@mozilla.org/bounce-tracking-protection;1", - "nsIBounceTrackingProtection" -); - /** * Get the base url for the current test directory using the given origin. * @param {string} origin - Origin to use in URL. @@ -48,8 +52,14 @@ function getBaseUrl(origin) { * the bounce. * @param {string} [options.bounceOrigin] - The origin of the bounce URL. * @param {string} [options.targetURL] - URL to redirect to after the bounce. - * @param {("cookie"|null)} [options.setState] - What type of state should be set during - * the bounce. No state by default. + * @param {('cookie-server'|'cookie-client'|'localStorage')} [options.setState] + * Type of state to set during the redirect. Defaults to non stateful redirect. + * @param {boolean} [options.setStateSameSiteFrame=false] - Whether to set the + * state in a sub frame that is same site to the top window. + * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the + * state in a web worker. This only supports setState == "indexedDB". + * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the + * state in a nested web worker. Otherwise the same as setStateInWebWorker. * @param {number} [options.statusCode] - HTTP status code to use for server * side redirect. Only applies to bounceType == "server". * @param {number} [options.redirectDelayMS] - How long to wait before @@ -62,6 +72,9 @@ function getBounceURL({ bounceOrigin = ORIGIN_TRACKER, targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"), setState = null, + setStateSameSiteFrame = false, + setStateInWebWorker = false, + setStateInNestedWebWorker = false, statusCode = 302, redirectDelayMS = 50, }) { @@ -79,6 +92,25 @@ function getBounceURL({ if (setState) { searchParams.set("setState", setState); } + if (setStateSameSiteFrame) { + searchParams.set("setStateSameSiteFrame", setStateSameSiteFrame); + } + if (setStateInWebWorker) { + if (setState != "indexedDB") { + throw new Error( + "setStateInWebWorker only supports setState == 'indexedDB'" + ); + } + searchParams.set("setStateInWebWorker", setStateInWebWorker); + } + if (setStateInNestedWebWorker) { + if (setState != "indexedDB") { + throw new Error( + "setStateInNestedWebWorker only supports setState == 'indexedDB'" + ); + } + searchParams.set("setStateInNestedWebWorker", setStateInNestedWebWorker); + } if (bounceType == "server") { searchParams.set("statusCode", statusCode); @@ -94,23 +126,58 @@ function getBounceURL({ * click on it. * @param {MozBrowser} browser - Browser to insert the link in. * @param {URL} targetURL - Destination for navigation. + * @param {Object} options - Additional options. + * @param {string} [options.spawnWindow] - If set to "newTab" or "popup" the + * link will be opened in a new tab or popup window respectively. If unset the + * link is opened in the given browser. * @returns {Promise} Resolves once the click is done. Does not wait for * navigation or load. */ -async function navigateLinkClick(browser, targetURL) { - await SpecialPowers.spawn(browser, [targetURL.href], targetURL => { - let link = content.document.createElement("a"); - - link.href = targetURL; - link.textContent = targetURL; - // The link needs display: block, otherwise synthesizeMouseAtCenter doesn't - // hit it. - link.style.display = "block"; - - content.document.body.appendChild(link); - }); +async function navigateLinkClick( + browser, + targetURL, + { spawnWindow = null } = {} +) { + if (spawnWindow && !["newTab", "popup"].includes(spawnWindow)) { + throw new Error(`Invalid option '${spawnWindow}' for spawnWindow`); + } - await BrowserTestUtils.synthesizeMouseAtCenter("a[href]", {}, browser); + await SpecialPowers.spawn( + browser, + [targetURL.href, spawnWindow], + async (targetURL, spawnWindow) => { + let link = content.document.createElement("a"); + link.id = "link"; + link.textContent = "Click Me"; + link.style.display = "block"; + link.style.fontSize = "40px"; + + // For opening a popup we attach an event listener to trigger via click. + if (spawnWindow) { + link.href = "#"; + link.addEventListener("click", event => { + event.preventDefault(); + if (spawnWindow == "newTab") { + // Open a new tab. + content.window.open(targetURL, "bounce"); + } else { + // Open a popup window. + content.window.open(targetURL, "bounce", "height=200,width=200"); + } + }); + } else { + // For regular navigation add href and click. + link.href = targetURL; + } + + content.document.body.appendChild(link); + + // TODO: Bug 1892091: Use EventUtils.synthesizeMouse instead for a real click. + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + content.document.userInteractionForTesting(); + link.click(); + } + ); } /** @@ -141,25 +208,38 @@ async function waitForRecordBounces(browser) { * or server side redirect. * @param {('cookie-server'|'cookie-client'|'localStorage')} [options.setState] * Type of state to set during the redirect. Defaults to non stateful redirect. - * @param {boolean} [options.expectCandidate=true] - Expect the redirecting site to be - * identified as a bounce tracker (candidate). - * @param {boolean} [options.expectPurge=true] - Expect the redirecting site to have - * its storage purged. + * @param {boolean} [options.setStateSameSiteFrame=false] - Whether to set the + * state in a sub frame that is same site to the top window. + * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the + * state in a web worker. This only supports setState == "indexedDB". + * @param {boolean} [options.setStateInWebWorker=false] - Whether to set the + * state in a nested web worker. Otherwise the same as setStateInWebWorker. + * @param {boolean} [options.expectCandidate=true] - Expect the redirecting site + * to be identified as a bounce tracker (candidate). + * @param {boolean} [options.expectPurge=true] - Expect the redirecting site to + * have its storage purged. * @param {OriginAttributes} [options.originAttributes={}] - Origin attributes * to use for the test. This determines whether the test is run in normal - * browsing, a private window or a container tab. By default the test is run - * in normal browsing. - * @param {function} [options.postBounceCallback] - Optional function to run after the - * bounce has completed. + * browsing, a private window or a container tab. By default the test is run in + * normal browsing. + * @param {function} [options.postBounceCallback] - Optional function to run + * after the bounce has completed. + * @param {boolean} [options.skipSiteDataCleanup=false] - Skip the cleanup of + * site data after the test. When this is enabled the caller is responsible for + * cleaning up site data. */ async function runTestBounce(options = {}) { let { bounceType, setState = null, + setStateSameSiteFrame = false, + setStateInWebWorker = false, + setStateInNestedWebWorker = false, expectCandidate = true, expectPurge = true, originAttributes = {}, postBounceCallback = () => {}, + skipSiteDataCleanup = false, } = options; info(`runTestBounce ${JSON.stringify(options)}`); @@ -191,28 +271,42 @@ async function runTestBounce(options = {}) { win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); } - let tab = win.gBrowser.addTab(getBaseUrl(ORIGIN_A) + "file_start.html", { + let initialURL = getBaseUrl(ORIGIN_A) + "file_start.html"; + let tab = win.gBrowser.addTab(initialURL, { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), userContextId, }); win.gBrowser.selectedTab = tab; let browser = tab.linkedBrowser; - await BrowserTestUtils.browserLoaded(browser); + await BrowserTestUtils.browserLoaded(browser, true, initialURL); let promiseRecordBounces = waitForRecordBounces(browser); // The final destination after the bounce. let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"); + // Wait for the final site to be loaded which complete the BounceTrackingRecord. + let targetURLLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + targetURL + ); + // Navigate through the bounce chain. await navigateLinkClick( browser, - getBounceURL({ bounceType, targetURL, setState }) + getBounceURL({ + bounceType, + targetURL, + setState, + setStateSameSiteFrame, + setStateInWebWorker, + setStateInNestedWebWorker, + }) ); - // Wait for the final site to be loaded which complete the BounceTrackingRecord. - await BrowserTestUtils.browserLoaded(browser, false, targetURL); + await targetURLLoadedPromise; // Navigate again with user gesture which triggers // BounceTrackingProtection::RecordStatefulBounces. We could rely on the @@ -226,9 +320,9 @@ async function runTestBounce(options = {}) { await promiseRecordBounces; Assert.deepEqual( - bounceTrackingProtection.testGetBounceTrackerCandidateHosts( - originAttributes - ), + bounceTrackingProtection + .testGetBounceTrackerCandidateHosts(originAttributes) + .map(entry => entry.siteHost), expectCandidate ? [SITE_TRACKER] : [], `Should ${ expectCandidate ? "" : "not " @@ -237,6 +331,7 @@ async function runTestBounce(options = {}) { Assert.deepEqual( bounceTrackingProtection .testGetUserActivationHosts(originAttributes) + .map(entry => entry.siteHost) .sort(), [SITE_A, SITE_B].sort(), "Should only have user activation for sites where we clicked links." @@ -272,4 +367,7 @@ async function runTestBounce(options = {}) { ); } bounceTrackingProtection.clearAll(); + if (!skipSiteDataCleanup) { + await SiteDataTestUtils.clear(); + } } diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py index afc3239839..3b0fe351e2 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py @@ -48,7 +48,7 @@ class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase): let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( Ci.nsIBounceTrackingProtection ); - return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).map(entry => entry.siteHost).sort(); """, ) self.assertEqual( @@ -64,7 +64,7 @@ class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase): let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( Ci.nsIBounceTrackingProtection ); - return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).sort(); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).map(entry => entry.siteHost).sort(); """, ) self.assertEqual( @@ -109,7 +109,7 @@ class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase): let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( Ci.nsIBounceTrackingProtection ); - return bounceTrackingProtection.testGetUserActivationHosts({}).sort(); + return bounceTrackingProtection.testGetUserActivationHosts({}).map(entry => entry.siteHost).sort(); """, ) self.assertEqual( diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js index 28a1350b3e..a52fb7fd46 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_clearExpiredUserActivation.js @@ -69,13 +69,19 @@ add_task(async function test() { // Assert that expired user activations have been cleared. Assert.deepEqual( - btp.testGetUserActivationHosts({}).sort(), + btp + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost) + .sort(), ["not-expired1.com", "not-expired2.com"], "Expired user activation flags have been cleared for normal browsing." ); Assert.deepEqual( - btp.testGetUserActivationHosts({ privateBrowsingId: 1 }).sort(), + btp + .testGetUserActivationHosts({ privateBrowsingId: 1 }) + .map(entry => entry.siteHost) + .sort(), ["pbm-not-expired.com"], "Expired user activation flags have been cleared for private browsing." ); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_importUserActivationPermissions.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_importUserActivationPermissions.js new file mode 100644 index 0000000000..5150d074c2 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_importUserActivationPermissions.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const DOMAIN_A = "example.com"; +const SUB_DOMAIN_A = "sub." + DOMAIN_A; +const DOMAIN_B = "example.org"; +const DOMAIN_C = "example.net"; + +const ORIGIN_A = "https://" + DOMAIN_A; +const ORIGIN_SUB_A = "https://" + SUB_DOMAIN_A; +const ORIGIN_B = "https://" + DOMAIN_B; +const ORIGIN_C = "https://" + DOMAIN_C; +const ORIGIN_NON_HTTP = "file:///foo/bar.html"; + +const OA_PBM = { privateBrowsingId: 1 }; +const PRINCIPAL_C_PBM = Services.scriptSecurityManager.createContentPrincipal( + Services.io.newURI(ORIGIN_C), + OA_PBM +); + +let btp; +let userActivationLifetimeSec = Services.prefs.getIntPref( + "privacy.bounceTrackingProtection.bounceTrackingActivationLifetimeSec" +); + +function cleanup() { + btp.clearAll(); + Services.perms.removeAll(); + Services.prefs.setBoolPref( + "privacy.bounceTrackingProtection.hasMigratedUserActivationData", + false + ); +} + +add_setup(function () { + // Need a profile to data clearing calls. + do_get_profile(); + + btp = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + + // Clean initial state. + cleanup(); +}); + +add_task(async function test_user_activation_perm_migration() { + // Assert initial test state. + Assert.deepEqual( + btp.testGetUserActivationHosts({}), + [], + "No user activation hosts initially." + ); + Assert.equal( + Services.perms.getAllByTypes(["storageAccessAPI"]).length, + 0, + "No user activation permissions initially." + ); + + info("Add test user activation permissions."); + + let now = Date.now(); + + // Non-expired permissions. + PermissionTestUtils.addWithModificationTime( + ORIGIN_A, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now + ); + PermissionTestUtils.addWithModificationTime( + ORIGIN_C, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now - 1000 + ); + + // A non expired permission for a subdomain of DOMAIN_A that has an older modification time. + PermissionTestUtils.addWithModificationTime( + ORIGIN_SUB_A, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now - 500 + ); + + // An expired permission. + PermissionTestUtils.addWithModificationTime( + ORIGIN_B, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now - userActivationLifetimeSec * 1.2 * 1000 + ); + + // A non-HTTP permission. + PermissionTestUtils.addWithModificationTime( + ORIGIN_NON_HTTP, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now + ); + + // A permission for PBM. Ideally we'd test a more persistent permission type + // here with custom oa, but permission seperation by userContextId isn't + // enabled yet (Bug 1641584). + PermissionTestUtils.addWithModificationTime( + PRINCIPAL_C_PBM, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + now + ); + + info("Trigger migration."); + btp.testMaybeMigrateUserInteractionPermissions(); + + Assert.deepEqual( + btp + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost) + .sort(), + [DOMAIN_A, DOMAIN_C].sort(), + "Should have imported the correct user activation flags." + ); + Assert.deepEqual( + btp.testGetUserActivationHosts(OA_PBM).map(entry => entry.siteHost), + [DOMAIN_C], + "Should have imported the correct user activation flags for PBM." + ); + + info("Reset the BTP user activation store"); + btp.clearAll(); + + info("Trigger migration again."); + btp.testMaybeMigrateUserInteractionPermissions(); + + Assert.deepEqual( + btp.testGetUserActivationHosts({}), + [], + "Should not have imported the user activation flags again." + ); + Assert.deepEqual( + btp.testGetUserActivationHosts(OA_PBM), + [], + "Should not have imported the user activation flags again for PBM." + ); + + cleanup(); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js index 5ede57a08b..2fbd8a0a02 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js @@ -6,6 +6,9 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ const { SiteDataTestUtils } = ChromeUtils.importESModule( "resource://testing-common/SiteDataTestUtils.sys.mjs" ); +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); let btp; let bounceTrackingGracePeriodSec; @@ -138,6 +141,14 @@ add_task(async function test_purge() { message: "Should purge after grace period.", shouldPurge: true, }, + // Don't purge if the site is allowlisted. + "example2.net": { + bounceTime: timestampOutsideGracePeriodFiveSeconds, + userActivationTime: null, + isAllowListed: true, + message: "Should not purge after grace period if allowlisted.", + shouldPurge: false, + }, // Also ensure that clear data calls with IP sites succeed. "1.2.3.4": { bounceTime: timestampOutsideGracePeriodThreeDays, @@ -191,16 +202,30 @@ add_task(async function test_purge() { let expectedBounceTrackerHosts = []; let expectedUserActivationHosts = []; + let allowListedHosts = []; let expiredUserActivationHosts = []; let expectedPurgedHosts = []; // This would normally happen over time while browsing. let initPromises = Object.entries(TEST_TRACKERS).map( - async ([siteHost, { bounceTime, userActivationTime, shouldPurge }]) => { + async ([ + siteHost, + { bounceTime, userActivationTime, isAllowListed, shouldPurge }, + ]) => { // Add site state so we can later assert it has been purged. await addStateForHost(siteHost); + // Add allowlist entry if needed. + if (isAllowListed) { + PermissionTestUtils.add( + `https://${siteHost}`, + "trackingprotection", + Services.perms.ALLOW_ACTION + ); + allowListedHosts.push(siteHost); + } + if (bounceTime != null) { if (userActivationTime != null) { throw new Error( @@ -210,7 +235,7 @@ add_task(async function test_purge() { expectedBounceTrackerHosts.push(siteHost); - // Convert bounceTime timestamp to nanoseconds (PRTime). + // Convert bounceTime timestamp to microseconds (PRTime). info( `Adding bounce. siteHost: ${siteHost}, bounceTime: ${bounceTime} ms` ); @@ -232,7 +257,7 @@ add_task(async function test_purge() { expiredUserActivationHosts.push(siteHost); } - // Convert userActivationTime timestamp to nanoseconds (PRTime). + // Convert userActivationTime timestamp to microseconds (PRTime). info( `Adding user interaction. siteHost: ${siteHost}, userActivationTime: ${userActivationTime} ms` ); @@ -250,12 +275,18 @@ add_task(async function test_purge() { "Check that bounce and user activation data has been correctly recorded." ); Assert.deepEqual( - btp.testGetBounceTrackerCandidateHosts({}).sort(), + btp + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost) + .sort(), expectedBounceTrackerHosts.sort(), "Has added bounce tracker hosts." ); Assert.deepEqual( - btp.testGetUserActivationHosts({}).sort(), + btp + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost) + .sort(), expectedUserActivationHosts.sort(), "Has added user activation hosts." ); @@ -269,17 +300,29 @@ add_task(async function test_purge() { "Should have purged all expected hosts." ); + // After the purge only the bounce trackers that have not been purged should + // remain in the candidate map. Additionally the allowlisted hosts should be + // removed from the candidate map. let expectedBounceTrackerHostsAfterPurge = expectedBounceTrackerHosts - .filter(host => !expectedPurgedHosts.includes(host)) + .filter( + host => + !expectedPurgedHosts.includes(host) && !allowListedHosts.includes(host) + ) .sort(); Assert.deepEqual( - btp.testGetBounceTrackerCandidateHosts({}).sort(), + btp + .testGetBounceTrackerCandidateHosts({}) + .map(entry => entry.siteHost) + .sort(), expectedBounceTrackerHostsAfterPurge.sort(), "After purge the bounce tracker candidate host set should be updated correctly." ); Assert.deepEqual( - btp.testGetUserActivationHosts({}).sort(), + btp + .testGetUserActivationHosts({}) + .map(entry => entry.siteHost) + .sort(), expiredUserActivationHosts.sort(), "After purge any expired user activation records should have been removed" ); @@ -302,6 +345,7 @@ add_task(async function test_purge() { btp.clearAll(); assertEmpty(); - info("Clean up site data."); + info("Clean up site data and permissions."); await SiteDataTestUtils.clear(); + Services.perms.removeAll(); }); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml index c3aeee502f..8195ea7224 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml @@ -3,8 +3,11 @@ prefs = [ "privacy.bounceTrackingProtection.enabled=true", "privacy.bounceTrackingProtection.enableTestMode=true", "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0", + "privacy.bounceTrackingProtection.enableDryRunMode=false", ] ["test_bouncetracking_clearExpiredUserActivation.js"] +["test_bouncetracking_importUserActivationPermissions.js"] + ["test_bouncetracking_purge.js"] |