diff options
Diffstat (limited to 'toolkit/components/antitracking/ContentBlockingLog.cpp')
-rw-r--r-- | toolkit/components/antitracking/ContentBlockingLog.cpp | 512 |
1 files changed, 512 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/ContentBlockingLog.cpp b/toolkit/components/antitracking/ContentBlockingLog.cpp new file mode 100644 index 0000000000..6797d2bd2b --- /dev/null +++ b/toolkit/components/antitracking/ContentBlockingLog.cpp @@ -0,0 +1,512 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AntiTrackingLog.h" +#include "ContentBlockingLog.h" + +#include "nsIEffectiveTLDService.h" +#include "nsITrackingDBService.h" +#include "nsIWebProgressListener.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsRFPService.h" +#include "nsServiceManagerUtils.h" +#include "nsTArray.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/Preferences.h" +#include "mozilla/RandomNum.h" +#include "mozilla/ReverseIterator.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StaticPrefs_telemetry.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Telemetry.h" +#include "mozilla/XorShift128PlusRNG.h" + +namespace mozilla { + +namespace { + +StaticAutoPtr<nsCString> gEmailWebAppDomainsPref; +static constexpr char kEmailWebAppDomainPrefName[] = + "privacy.trackingprotection.emailtracking.webapp.domains"; + +void EmailWebAppDomainPrefChangeCallback(const char* aPrefName, void*) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aPrefName, kEmailWebAppDomainPrefName)); + MOZ_ASSERT(gEmailWebAppDomainsPref); + + Preferences::GetCString(kEmailWebAppDomainPrefName, *gEmailWebAppDomainsPref); +} + +} // namespace + +Maybe<uint32_t> ContentBlockingLog::RecordLogParent( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason, + const nsTArray<nsCString>& aTrackingFullHashes, + const Maybe<ContentBlockingNotifier::CanvasFingerprinter>& + aCanvasFingerprinter, + const Maybe<bool> aCanvasFingerprinterKnownText) { + MOZ_ASSERT(XRE_IsParentProcess()); + + uint32_t events = GetContentBlockingEventsInLog(); + + bool blockedValue = aBlocked; + bool unblocked = false; + OriginEntry* entry; + + switch (aType) { + case nsIWebProgressListener::STATE_COOKIES_LOADED: + MOZ_ASSERT(!aBlocked, + "We don't expected to see blocked STATE_COOKIES_LOADED"); + [[fallthrough]]; + + case nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER: + MOZ_ASSERT( + !aBlocked, + "We don't expected to see blocked STATE_COOKIES_LOADED_TRACKER"); + [[fallthrough]]; + + case nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER: + MOZ_ASSERT(!aBlocked, + "We don't expected to see blocked " + "STATE_COOKIES_LOADED_SOCIALTRACKER"); + // Note that the logic in these branches are the logical negation of the + // logic in other branches, since the Document API we have is phrased + // in "loaded" terms as opposed to "blocked" terms. + blockedValue = !aBlocked; + [[fallthrough]]; + + case nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT: + case nsIWebProgressListener::STATE_BLOCKED_FINGERPRINTING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_FINGERPRINTING_CONTENT: + case nsIWebProgressListener::STATE_BLOCKED_CRYPTOMINING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_CRYPTOMINING_CONTENT: + case nsIWebProgressListener::STATE_BLOCKED_SOCIALTRACKING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_SOCIALTRACKING_CONTENT: + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION: + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL: + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN: + case nsIWebProgressListener::STATE_BLOCKED_EMAILTRACKING_CONTENT: + case nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT: + case nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_2_CONTENT: + Unused << RecordLogInternal(aOrigin, aType, blockedValue); + break; + + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER: + case nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER: + Unused << RecordLogInternal(aOrigin, aType, blockedValue, aReason, + aTrackingFullHashes); + break; + + case nsIWebProgressListener::STATE_REPLACED_FINGERPRINTING_CONTENT: + case nsIWebProgressListener::STATE_ALLOWED_FINGERPRINTING_CONTENT: + case nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT: + case nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT: + Unused << RecordLogInternal(aOrigin, aType, blockedValue, aReason, + aTrackingFullHashes); + break; + case nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING: + MOZ_ASSERT(!aBlocked, + "We don't expected to see blocked " + "STATE_ALLOWED_FONT_FINGERPRINTING"); + entry = RecordLogInternal(aOrigin, aType, blockedValue); + + // Replace the flag using the suspicious fingerprinting event so that we + // can report the event if we detect suspicious fingerprinting. + aType = nsIWebProgressListener::STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING; + + // Report blocking if we detect suspicious fingerprinting activity. + if (entry && entry->mData->mHasSuspiciousFingerprintingActivity) { + blockedValue = true; + } + break; + + case nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING: + MOZ_ASSERT(!aBlocked, + "We don't expected to see blocked " + "STATE_ALLOWED_CANVAS_FINGERPRINTING"); + entry = RecordLogInternal(aOrigin, aType, blockedValue, Nothing(), {}, + aCanvasFingerprinter, + aCanvasFingerprinterKnownText); + + // Replace the flag using the suspicious fingerprinting event so that we + // can report the event if we detect suspicious fingerprinting. + aType = nsIWebProgressListener::STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING; + + // Report blocking if we detect suspicious fingerprinting activity. + if (entry && entry->mData->mHasSuspiciousFingerprintingActivity) { + blockedValue = true; + } + break; + + default: + // Ignore nsIWebProgressListener::STATE_BLOCKED_UNSAFE_CONTENT; + break; + } + + if (!aBlocked) { + unblocked = (events & aType) != 0; + } + + const uint32_t oldEvents = events; + if (blockedValue) { + events |= aType; + } else if (unblocked) { + events &= ~aType; + } + + if (events == oldEvents +#ifdef ANDROID + // GeckoView always needs to notify about blocked trackers, + // since the GeckoView API always needs to report the URI and + // type of any blocked tracker. We use a platform-dependent code + // path here because reporting this notification on desktop + // platforms isn't necessary and doing so can have a big + // performance cost. + && aType != nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT +#endif + ) { + // Avoid dispatching repeated notifications when nothing has + // changed + return Nothing(); + } + + return Some(events); +} + +void ContentBlockingLog::ReportLog(nsIPrincipal* aFirstPartyPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aFirstPartyPrincipal); + + if (!StaticPrefs::browser_contentblocking_database_enabled()) { + return; + } + + if (mLog.IsEmpty()) { + return; + } + + nsCOMPtr<nsITrackingDBService> trackingDBService = + do_GetService("@mozilla.org/tracking-db-service;1"); + if (NS_WARN_IF(!trackingDBService)) { + return; + } + + trackingDBService->RecordContentBlockingLog(Stringify()); +} + +void ContentBlockingLog::ReportCanvasFingerprintingLog( + nsIPrincipal* aFirstPartyPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aFirstPartyPrincipal); + + // We don't need to report if the first party is not a content. + if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) { + return; + } + + bool hasCanvasFingerprinter = false; + bool canvasFingerprinterKnownText = false; + Maybe<ContentBlockingNotifier::CanvasFingerprinter> canvasFingerprinter; + for (const auto& originEntry : mLog) { + if (!originEntry.mData) { + continue; + } + + for (const auto& logEntry : Reversed(originEntry.mData->mLogs)) { + if (logEntry.mType != + nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING) { + continue; + } + + // Select the log entry with the highest fingerprinting likelihood, + // that primarily means preferring those with a FingerprinterKnownText. + if (!hasCanvasFingerprinter || + (!canvasFingerprinterKnownText && + *logEntry.mCanvasFingerprinterKnownText) || + (!canvasFingerprinterKnownText && canvasFingerprinter.isNothing() && + logEntry.mCanvasFingerprinter.isSome())) { + hasCanvasFingerprinter = true; + canvasFingerprinterKnownText = *logEntry.mCanvasFingerprinterKnownText; + canvasFingerprinter = logEntry.mCanvasFingerprinter; + } + } + } + + if (!hasCanvasFingerprinter) { + Telemetry::Accumulate(Telemetry::CANVAS_FINGERPRINTING_PER_TAB, + "unknown"_ns, 0); + } else { + int32_t fingerprinter = + canvasFingerprinter.isSome() ? (*canvasFingerprinter + 1) : 0; + Telemetry::Accumulate( + Telemetry::CANVAS_FINGERPRINTING_PER_TAB, + canvasFingerprinterKnownText ? "known_text"_ns : "unknown"_ns, + fingerprinter); + } +} + +void ContentBlockingLog::ReportFontFingerprintingLog( + nsIPrincipal* aFirstPartyPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aFirstPartyPrincipal); + + // We don't need to report if the first party is not a content. + if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) { + return; + } + + bool hasFontFingerprinter = false; + for (const auto& originEntry : mLog) { + if (!originEntry.mData) { + continue; + } + + for (const auto& logEntry : originEntry.mData->mLogs) { + if (logEntry.mType != + nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) { + continue; + } + + hasFontFingerprinter = true; + } + + if (hasFontFingerprinter) { + break; + } + } + + Telemetry::Accumulate(Telemetry::FONT_FINGERPRINTING_PER_TAB, + hasFontFingerprinter); +} + +void ContentBlockingLog::ReportEmailTrackingLog( + nsIPrincipal* aFirstPartyPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aFirstPartyPrincipal); + + // We don't need to report if the first party is not a content. + if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) { + return; + } + + nsCOMPtr<nsIEffectiveTLDService> tldService = + do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + + if (!tldService) { + return; + } + + nsTHashtable<nsCStringHashKey> level1SiteSet; + nsTHashtable<nsCStringHashKey> level2SiteSet; + + for (const auto& originEntry : mLog) { + if (!originEntry.mData) { + continue; + } + + bool isLevel1EmailTracker = false; + bool isLevel2EmailTracker = false; + + for (const auto& logEntry : Reversed(originEntry.mData->mLogs)) { + // Check if the email tracking related event had been filed for the given + // origin entry. Note that we currently only block level 1 email trackers, + // so blocking event represents the page has embedded a level 1 tracker. + if (logEntry.mType == + nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_2_CONTENT) { + isLevel2EmailTracker = true; + break; + } + + if (logEntry.mType == + nsIWebProgressListener::STATE_BLOCKED_EMAILTRACKING_CONTENT || + logEntry.mType == nsIWebProgressListener:: + STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT) { + isLevel1EmailTracker = true; + break; + } + } + + if (isLevel1EmailTracker || isLevel2EmailTracker) { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), originEntry.mOrigin); + + if (NS_FAILED(rv)) { + continue; + } + + nsAutoCString baseDomain; + rv = tldService->GetBaseDomain(uri, 0, baseDomain); + + if (NS_FAILED(rv)) { + continue; + } + + if (isLevel1EmailTracker) { + Unused << level1SiteSet.EnsureInserted(baseDomain); + } else { + Unused << level2SiteSet.EnsureInserted(baseDomain); + } + } + } + + // Cache the email webapp domains pref value and register the callback + // function to update the cached value when the pref changes. + if (!gEmailWebAppDomainsPref) { + gEmailWebAppDomainsPref = new nsCString(); + + Preferences::RegisterCallbackAndCall(EmailWebAppDomainPrefChangeCallback, + kEmailWebAppDomainPrefName); + RunOnShutdown([]() { + Preferences::UnregisterCallback(EmailWebAppDomainPrefChangeCallback, + kEmailWebAppDomainPrefName); + gEmailWebAppDomainsPref = nullptr; + }); + } + + bool isTopEmailWebApp = + aFirstPartyPrincipal->IsURIInList(*gEmailWebAppDomainsPref); + uint32_t level1Count = level1SiteSet.Count(); + uint32_t level2Count = level2SiteSet.Count(); + + Telemetry::Accumulate( + Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB, + isTopEmailWebApp ? "base_emailapp"_ns : "base_normal"_ns, level1Count); + Telemetry::Accumulate( + Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB, + isTopEmailWebApp ? "content_emailapp"_ns : "content_normal"_ns, + level2Count); + Telemetry::Accumulate(Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB, + isTopEmailWebApp ? "all_emailapp"_ns : "all_normal"_ns, + level1Count + level2Count); +} + +ContentBlockingLog::OriginEntry* ContentBlockingLog::RecordLogInternal( + const nsACString& aOrigin, uint32_t aType, bool aBlocked, + const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>& + aReason, + const nsTArray<nsCString>& aTrackingFullHashes, + const Maybe<ContentBlockingNotifier::CanvasFingerprinter>& + aCanvasFingerprinter, + const Maybe<bool> aCanvasFingerprinterKnownText) { + DebugOnly<bool> isCookiesBlockedTracker = + aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER || + aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER; + MOZ_ASSERT_IF(aBlocked, aReason.isNothing()); + MOZ_ASSERT_IF(!isCookiesBlockedTracker, aReason.isNothing()); + MOZ_ASSERT_IF(isCookiesBlockedTracker && !aBlocked, aReason.isSome()); + + if (aOrigin.IsVoid()) { + return nullptr; + } + auto index = mLog.IndexOf(aOrigin, 0, Comparator()); + if (index != OriginDataTable::NoIndex) { + OriginEntry& entry = mLog[index]; + if (!entry.mData) { + return nullptr; + } + + if (RecordLogEntryInCustomField(aType, entry, aBlocked)) { + return &entry; + } + if (!entry.mData->mLogs.IsEmpty()) { + auto& last = entry.mData->mLogs.LastElement(); + if (last.mType == aType && last.mBlocked == aBlocked && + last.mCanvasFingerprinter == aCanvasFingerprinter && + last.mCanvasFingerprinterKnownText == aCanvasFingerprinterKnownText) { + ++last.mRepeatCount; + // Don't record recorded events. This helps compress our log. + // We don't care about if the the reason is the same, just keep the + // first one. + // Note: {aReason, aTrackingFullHashes} are not compared here and we + // simply keep the first for the reason, and merge hashes to make sure + // they can be correctly recorded. + for (const auto& hash : aTrackingFullHashes) { + if (!last.mTrackingFullHashes.Contains(hash)) { + last.mTrackingFullHashes.AppendElement(hash); + } + } + return &entry; + } + } + if (entry.mData->mLogs.Length() == + std::max(1u, StaticPrefs::browser_contentblocking_originlog_length())) { + // Cap the size at the maximum length adjustable by the pref + entry.mData->mLogs.RemoveElementAt(0); + } + entry.mData->mLogs.AppendElement( + LogEntry{aType, 1u, aBlocked, aReason, aTrackingFullHashes.Clone(), + aCanvasFingerprinter, aCanvasFingerprinterKnownText}); + + // Check suspicious fingerprinting activities if the origin hasn't already + // been marked. + // TODO(Bug 1864909): Moving the suspicious fingerprinting detection call + // out of here. + if ((aType == nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING || + aType == nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) && + !entry.mData->mHasSuspiciousFingerprintingActivity && + nsRFPService::CheckSuspiciousFingerprintingActivity( + entry.mData->mLogs)) { + entry.mData->mHasSuspiciousFingerprintingActivity = true; + } + return &entry; + } + + // The entry has not been found. + OriginEntry* entry = mLog.AppendElement(); + if (NS_WARN_IF(!entry || !entry->mData)) { + return nullptr; + } + + entry->mOrigin = aOrigin; + + if (aType == nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) { + entry->mData->mHasLevel1TrackingContentLoaded = aBlocked; + } else if (aType == + nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT) { + entry->mData->mHasLevel2TrackingContentLoaded = aBlocked; + } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) { + MOZ_ASSERT(entry->mData->mHasCookiesLoaded.isNothing()); + entry->mData->mHasCookiesLoaded.emplace(aBlocked); + } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) { + MOZ_ASSERT(entry->mData->mHasTrackerCookiesLoaded.isNothing()); + entry->mData->mHasTrackerCookiesLoaded.emplace(aBlocked); + } else if (aType == + nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) { + MOZ_ASSERT(entry->mData->mHasSocialTrackerCookiesLoaded.isNothing()); + entry->mData->mHasSocialTrackerCookiesLoaded.emplace(aBlocked); + } else { + entry->mData->mLogs.AppendElement( + LogEntry{aType, 1u, aBlocked, aReason, aTrackingFullHashes.Clone(), + aCanvasFingerprinter, aCanvasFingerprinterKnownText}); + + // Check suspicious fingerprinting activities if the origin hasn't been + // marked. + // TODO(Bug 1864909): Moving the suspicious fingerprinting detection call + // out of here. + if ((aType == nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING || + aType == nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) && + nsRFPService::CheckSuspiciousFingerprintingActivity( + entry->mData->mLogs)) { + entry->mData->mHasSuspiciousFingerprintingActivity = true; + } + } + + return entry; +} + +} // namespace mozilla |