/* -*- 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 "nsTHashSet.h" #include "mozilla/BasePrincipal.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/Preferences.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/glean/AntitrackingMetrics.h" static mozilla::LazyLogModule gFingerprinterDetection("FingerprinterDetection"); namespace mozilla { namespace { StaticAutoPtr 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 ContentBlockingLog::RecordLogParent( const nsACString& aOrigin, uint32_t aType, bool aBlocked, const Maybe& aReason, const nsTArray& aTrackingFullHashes, const Maybe& aCanvasFingerprintingEvent) { 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: case nsIWebProgressListener::STATE_PURGED_BOUNCETRACKER: case nsIWebProgressListener::STATE_COOKIES_PARTITIONED_TRACKER: (void)RecordLogInternal(aOrigin, aType, blockedValue); break; case nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER: case nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER: (void)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: (void)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(), {}, aCanvasFingerprintingEvent); // 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() { MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(NS_IsMainThread()); if (!StaticPrefs::browser_contentblocking_database_enabled()) { return; } if (mLog.IsEmpty()) { return; } nsCOMPtr 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; // Track unique (key, source) combinations to avoid counting duplicates per // page nsTHashSet seenTextSourceCombos; nsTHashSet seenAliasSourceCombos; 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; } else if (logEntry.mCanvasFingerprintingEvent.isSome() == false) { // Little confused about how we could get here, but I did see a crash { nsAutoCString firstPartyOrigin; aFirstPartyPrincipal->GetOriginNoSuffix(firstPartyOrigin); MOZ_LOG(gFingerprinterDetection, LogLevel::Error, ("ContentBlockingLog::ReportCanvasFingerprintingLog: " "logEntry has no CanvasFingerprintingEvent " "(firstPartyOrigin=%s)", firstPartyOrigin.get())); } continue; } hasCanvasFingerprinter = true; auto canvasFingerprintingEvent = logEntry.mCanvasFingerprintingEvent.value(); // ---------------------------------- // First cover canvas_fingerprinting_type_text_by_source_per_tab2 // Only count each unique (text, source) combination once per page if (!canvasFingerprintingEvent.knownTextBitmask) { nsAutoCString key, category; key.AppendLiteral("none"); category.AppendInt(canvasFingerprintingEvent.sourcesBitmask); nsAutoCString comboKey(key + ":"_ns + category); if (!seenTextSourceCombos.Contains(comboKey)) { seenTextSourceCombos.Insert(comboKey); glean::contentblocking:: canvas_fingerprinting_type_text_by_source_per_tab2 .Get(key, category) .Add(); } } else { // Iterate over each set bit in the bitmask for (uint32_t b = canvasFingerprintingEvent.knownTextBitmask; b; b &= (b - 1)) { uint32_t singleSetBit_Text = b & (~b + 1); uint32_t exponent = mozilla::CountTrailingZeroes32(singleSetBit_Text); nsAutoCString key, category; key.AppendInt(exponent); category.AppendInt(canvasFingerprintingEvent.sourcesBitmask); nsAutoCString comboKey(key + ":"_ns + category); if (!seenTextSourceCombos.Contains(comboKey)) { seenTextSourceCombos.Insert(comboKey); glean::contentblocking:: canvas_fingerprinting_type_text_by_source_per_tab2 .Get(key, category) .Add(); } } } // ---------------------------------- // Second, cover canvas_fingerprinting_type_alias_by_source_per_tab2 // Only count each unique (alias, source) combination once per page { nsAutoCString key, category; key.AppendInt(static_cast(canvasFingerprintingEvent.alias)); category.AppendInt(canvasFingerprintingEvent.sourcesBitmask); nsAutoCString comboKey(key + ":"_ns + category); if (!seenAliasSourceCombos.Contains(comboKey)) { seenAliasSourceCombos.Insert(comboKey); glean::contentblocking:: canvas_fingerprinting_type_alias_by_source_per_tab2 .Get(key, category) .Add(); } } } } // ---------------------------------- // Finally, cover the overall 'was there any canvas fingerprinting' metric // canvas_fingerprinting_per_tab2 if (!hasCanvasFingerprinter) { // Increment the global 'did the page have any' metric glean::contentblocking::canvas_fingerprinting_per_tab2 .EnumGet( glean::contentblocking::CanvasFingerprintingPerTab2Label::eNotFound) .Add(); } else { // Increment the global 'did the page have any' metric glean::contentblocking::canvas_fingerprinting_per_tab2 .EnumGet( glean::contentblocking::CanvasFingerprintingPerTab2Label::eFound) .Add(); } } 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; } } glean::contentblocking::font_fingerprinting_per_tab .EnumGet( static_cast( hasFontFingerprinter)) .Add(); } 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 tldService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); if (!tldService) { return; } nsTHashtable level1SiteSet; nsTHashtable 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 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) { (void)level1SiteSet.EnsureInserted(baseDomain); } else { (void)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(); glean::contentblocking::email_tracker_embedded_per_tab .EnumGet(isTopEmailWebApp ? glean::contentblocking::EmailTrackerEmbeddedPerTabLabel:: eBaseEmailapp : glean::contentblocking::EmailTrackerEmbeddedPerTabLabel:: eBaseNormal) .AccumulateSingleSample(level1Count); glean::contentblocking::email_tracker_embedded_per_tab .EnumGet(isTopEmailWebApp ? glean::contentblocking::EmailTrackerEmbeddedPerTabLabel:: eContentEmailapp : glean::contentblocking::EmailTrackerEmbeddedPerTabLabel:: eContentNormal) .AccumulateSingleSample(level2Count); glean::contentblocking::email_tracker_embedded_per_tab .EnumGet(isTopEmailWebApp ? glean::contentblocking::EmailTrackerEmbeddedPerTabLabel:: eAllEmailapp : glean::contentblocking::EmailTrackerEmbeddedPerTabLabel:: eAllNormal) .AccumulateSingleSample(level1Count + level2Count); } ContentBlockingLog::OriginEntry* ContentBlockingLog::RecordLogInternal( const nsACString& aOrigin, uint32_t aType, bool aBlocked, const Maybe& aReason, const nsTArray& aTrackingFullHashes, const Maybe& aCanvasFingerprintingEvent) { DebugOnly 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.mCanvasFingerprintingEvent == aCanvasFingerprintingEvent) { ++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(), aCanvasFingerprintingEvent}); // 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 = 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(), aCanvasFingerprintingEvent}); // 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) { entry->mData->mHasSuspiciousFingerprintingActivity = true; } } return entry; } } // namespace mozilla