Bug 1933479 - Add tab close button on hover to vertical tabs when sidebar is collapse...
[gecko.git] / toolkit / components / antitracking / ContentBlockingLog.cpp
blobbb77d68b920b0c6f0dec3a11554ce9e5eaa06fa2
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "AntiTrackingLog.h"
8 #include "ContentBlockingLog.h"
10 #include "nsIEffectiveTLDService.h"
11 #include "nsITrackingDBService.h"
12 #include "nsIWebProgressListener.h"
13 #include "nsNetCID.h"
14 #include "nsNetUtil.h"
15 #include "nsRFPService.h"
16 #include "nsServiceManagerUtils.h"
17 #include "nsTArray.h"
18 #include "mozilla/BasePrincipal.h"
19 #include "mozilla/ClearOnShutdown.h"
20 #include "mozilla/HashFunctions.h"
21 #include "mozilla/Preferences.h"
22 #include "mozilla/RandomNum.h"
23 #include "mozilla/ReverseIterator.h"
24 #include "mozilla/StaticPrefs_browser.h"
25 #include "mozilla/StaticPrefs_privacy.h"
26 #include "mozilla/StaticPrefs_telemetry.h"
27 #include "mozilla/StaticPtr.h"
28 #include "mozilla/Telemetry.h"
29 #include "mozilla/XorShift128PlusRNG.h"
31 namespace mozilla {
33 namespace {
35 StaticAutoPtr<nsCString> gEmailWebAppDomainsPref;
36 static constexpr char kEmailWebAppDomainPrefName[] =
37 "privacy.trackingprotection.emailtracking.webapp.domains";
39 void EmailWebAppDomainPrefChangeCallback(const char* aPrefName, void*) {
40 MOZ_ASSERT(NS_IsMainThread());
41 MOZ_ASSERT(!strcmp(aPrefName, kEmailWebAppDomainPrefName));
42 MOZ_ASSERT(gEmailWebAppDomainsPref);
44 Preferences::GetCString(kEmailWebAppDomainPrefName, *gEmailWebAppDomainsPref);
47 } // namespace
49 Maybe<uint32_t> ContentBlockingLog::RecordLogParent(
50 const nsACString& aOrigin, uint32_t aType, bool aBlocked,
51 const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
52 aReason,
53 const nsTArray<nsCString>& aTrackingFullHashes,
54 const Maybe<ContentBlockingNotifier::CanvasFingerprinter>&
55 aCanvasFingerprinter,
56 const Maybe<bool> aCanvasFingerprinterKnownText) {
57 MOZ_ASSERT(XRE_IsParentProcess());
59 uint32_t events = GetContentBlockingEventsInLog();
61 bool blockedValue = aBlocked;
62 bool unblocked = false;
63 OriginEntry* entry;
65 switch (aType) {
66 case nsIWebProgressListener::STATE_COOKIES_LOADED:
67 MOZ_ASSERT(!aBlocked,
68 "We don't expected to see blocked STATE_COOKIES_LOADED");
69 [[fallthrough]];
71 case nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER:
72 MOZ_ASSERT(
73 !aBlocked,
74 "We don't expected to see blocked STATE_COOKIES_LOADED_TRACKER");
75 [[fallthrough]];
77 case nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER:
78 MOZ_ASSERT(!aBlocked,
79 "We don't expected to see blocked "
80 "STATE_COOKIES_LOADED_SOCIALTRACKER");
81 // Note that the logic in these branches are the logical negation of the
82 // logic in other branches, since the Document API we have is phrased
83 // in "loaded" terms as opposed to "blocked" terms.
84 blockedValue = !aBlocked;
85 [[fallthrough]];
87 case nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT:
88 case nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT:
89 case nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT:
90 case nsIWebProgressListener::STATE_BLOCKED_FINGERPRINTING_CONTENT:
91 case nsIWebProgressListener::STATE_LOADED_FINGERPRINTING_CONTENT:
92 case nsIWebProgressListener::STATE_BLOCKED_CRYPTOMINING_CONTENT:
93 case nsIWebProgressListener::STATE_LOADED_CRYPTOMINING_CONTENT:
94 case nsIWebProgressListener::STATE_BLOCKED_SOCIALTRACKING_CONTENT:
95 case nsIWebProgressListener::STATE_LOADED_SOCIALTRACKING_CONTENT:
96 case nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION:
97 case nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL:
98 case nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN:
99 case nsIWebProgressListener::STATE_BLOCKED_EMAILTRACKING_CONTENT:
100 case nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT:
101 case nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_2_CONTENT:
102 case nsIWebProgressListener::STATE_PURGED_BOUNCETRACKER:
103 case nsIWebProgressListener::STATE_COOKIES_PARTITIONED_TRACKER:
104 Unused << RecordLogInternal(aOrigin, aType, blockedValue);
105 break;
107 case nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER:
108 case nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER:
109 Unused << RecordLogInternal(aOrigin, aType, blockedValue, aReason,
110 aTrackingFullHashes);
111 break;
113 case nsIWebProgressListener::STATE_REPLACED_FINGERPRINTING_CONTENT:
114 case nsIWebProgressListener::STATE_ALLOWED_FINGERPRINTING_CONTENT:
115 case nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT:
116 case nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT:
117 Unused << RecordLogInternal(aOrigin, aType, blockedValue, aReason,
118 aTrackingFullHashes);
119 break;
120 case nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING:
121 MOZ_ASSERT(!aBlocked,
122 "We don't expected to see blocked "
123 "STATE_ALLOWED_FONT_FINGERPRINTING");
124 entry = RecordLogInternal(aOrigin, aType, blockedValue);
126 // Replace the flag using the suspicious fingerprinting event so that we
127 // can report the event if we detect suspicious fingerprinting.
128 aType = nsIWebProgressListener::STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING;
130 // Report blocking if we detect suspicious fingerprinting activity.
131 if (entry && entry->mData->mHasSuspiciousFingerprintingActivity) {
132 blockedValue = true;
134 break;
136 case nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING:
137 MOZ_ASSERT(!aBlocked,
138 "We don't expected to see blocked "
139 "STATE_ALLOWED_CANVAS_FINGERPRINTING");
140 entry = RecordLogInternal(aOrigin, aType, blockedValue, Nothing(), {},
141 aCanvasFingerprinter,
142 aCanvasFingerprinterKnownText);
144 // Replace the flag using the suspicious fingerprinting event so that we
145 // can report the event if we detect suspicious fingerprinting.
146 aType = nsIWebProgressListener::STATE_BLOCKED_SUSPICIOUS_FINGERPRINTING;
148 // Report blocking if we detect suspicious fingerprinting activity.
149 if (entry && entry->mData->mHasSuspiciousFingerprintingActivity) {
150 blockedValue = true;
152 break;
154 default:
155 // Ignore nsIWebProgressListener::STATE_BLOCKED_UNSAFE_CONTENT;
156 break;
159 if (!aBlocked) {
160 unblocked = (events & aType) != 0;
163 const uint32_t oldEvents = events;
164 if (blockedValue) {
165 events |= aType;
166 } else if (unblocked) {
167 events &= ~aType;
170 if (events == oldEvents
171 #ifdef ANDROID
172 // GeckoView always needs to notify about blocked trackers,
173 // since the GeckoView API always needs to report the URI and
174 // type of any blocked tracker. We use a platform-dependent code
175 // path here because reporting this notification on desktop
176 // platforms isn't necessary and doing so can have a big
177 // performance cost.
178 && aType != nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT
179 #endif
181 // Avoid dispatching repeated notifications when nothing has
182 // changed
183 return Nothing();
186 return Some(events);
189 void ContentBlockingLog::ReportLog() {
190 MOZ_ASSERT(XRE_IsParentProcess());
191 MOZ_ASSERT(NS_IsMainThread());
193 if (!StaticPrefs::browser_contentblocking_database_enabled()) {
194 return;
197 if (mLog.IsEmpty()) {
198 return;
201 nsCOMPtr<nsITrackingDBService> trackingDBService =
202 do_GetService("@mozilla.org/tracking-db-service;1");
203 if (NS_WARN_IF(!trackingDBService)) {
204 return;
207 trackingDBService->RecordContentBlockingLog(Stringify());
210 void ContentBlockingLog::ReportCanvasFingerprintingLog(
211 nsIPrincipal* aFirstPartyPrincipal) {
212 MOZ_ASSERT(XRE_IsParentProcess());
213 MOZ_ASSERT(NS_IsMainThread());
214 MOZ_ASSERT(aFirstPartyPrincipal);
216 // We don't need to report if the first party is not a content.
217 if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) {
218 return;
221 bool hasCanvasFingerprinter = false;
222 bool canvasFingerprinterKnownText = false;
223 Maybe<ContentBlockingNotifier::CanvasFingerprinter> canvasFingerprinter;
224 for (const auto& originEntry : mLog) {
225 if (!originEntry.mData) {
226 continue;
229 for (const auto& logEntry : Reversed(originEntry.mData->mLogs)) {
230 if (logEntry.mType !=
231 nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING) {
232 continue;
235 // Select the log entry with the highest fingerprinting likelihood,
236 // that primarily means preferring those with a FingerprinterKnownText.
237 if (!hasCanvasFingerprinter ||
238 (!canvasFingerprinterKnownText &&
239 *logEntry.mCanvasFingerprinterKnownText) ||
240 (!canvasFingerprinterKnownText && canvasFingerprinter.isNothing() &&
241 logEntry.mCanvasFingerprinter.isSome())) {
242 hasCanvasFingerprinter = true;
243 canvasFingerprinterKnownText = *logEntry.mCanvasFingerprinterKnownText;
244 canvasFingerprinter = logEntry.mCanvasFingerprinter;
249 if (!hasCanvasFingerprinter) {
250 Telemetry::Accumulate(Telemetry::CANVAS_FINGERPRINTING_PER_TAB,
251 "unknown"_ns, 0);
252 } else {
253 int32_t fingerprinter =
254 canvasFingerprinter.isSome() ? (*canvasFingerprinter + 1) : 0;
255 Telemetry::Accumulate(
256 Telemetry::CANVAS_FINGERPRINTING_PER_TAB,
257 canvasFingerprinterKnownText ? "known_text"_ns : "unknown"_ns,
258 fingerprinter);
262 void ContentBlockingLog::ReportFontFingerprintingLog(
263 nsIPrincipal* aFirstPartyPrincipal) {
264 MOZ_ASSERT(XRE_IsParentProcess());
265 MOZ_ASSERT(NS_IsMainThread());
266 MOZ_ASSERT(aFirstPartyPrincipal);
268 // We don't need to report if the first party is not a content.
269 if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) {
270 return;
273 bool hasFontFingerprinter = false;
274 for (const auto& originEntry : mLog) {
275 if (!originEntry.mData) {
276 continue;
279 for (const auto& logEntry : originEntry.mData->mLogs) {
280 if (logEntry.mType !=
281 nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) {
282 continue;
285 hasFontFingerprinter = true;
288 if (hasFontFingerprinter) {
289 break;
293 Telemetry::Accumulate(Telemetry::FONT_FINGERPRINTING_PER_TAB,
294 hasFontFingerprinter);
297 void ContentBlockingLog::ReportEmailTrackingLog(
298 nsIPrincipal* aFirstPartyPrincipal) {
299 MOZ_ASSERT(XRE_IsParentProcess());
300 MOZ_ASSERT(NS_IsMainThread());
301 MOZ_ASSERT(aFirstPartyPrincipal);
303 // We don't need to report if the first party is not a content.
304 if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) {
305 return;
308 nsCOMPtr<nsIEffectiveTLDService> tldService =
309 do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
311 if (!tldService) {
312 return;
315 nsTHashtable<nsCStringHashKey> level1SiteSet;
316 nsTHashtable<nsCStringHashKey> level2SiteSet;
318 for (const auto& originEntry : mLog) {
319 if (!originEntry.mData) {
320 continue;
323 bool isLevel1EmailTracker = false;
324 bool isLevel2EmailTracker = false;
326 for (const auto& logEntry : Reversed(originEntry.mData->mLogs)) {
327 // Check if the email tracking related event had been filed for the given
328 // origin entry. Note that we currently only block level 1 email trackers,
329 // so blocking event represents the page has embedded a level 1 tracker.
330 if (logEntry.mType ==
331 nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_2_CONTENT) {
332 isLevel2EmailTracker = true;
333 break;
336 if (logEntry.mType ==
337 nsIWebProgressListener::STATE_BLOCKED_EMAILTRACKING_CONTENT ||
338 logEntry.mType == nsIWebProgressListener::
339 STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT) {
340 isLevel1EmailTracker = true;
341 break;
345 if (isLevel1EmailTracker || isLevel2EmailTracker) {
346 nsCOMPtr<nsIURI> uri;
347 nsresult rv = NS_NewURI(getter_AddRefs(uri), originEntry.mOrigin);
349 if (NS_FAILED(rv)) {
350 continue;
353 nsAutoCString baseDomain;
354 rv = tldService->GetBaseDomain(uri, 0, baseDomain);
356 if (NS_FAILED(rv)) {
357 continue;
360 if (isLevel1EmailTracker) {
361 Unused << level1SiteSet.EnsureInserted(baseDomain);
362 } else {
363 Unused << level2SiteSet.EnsureInserted(baseDomain);
368 // Cache the email webapp domains pref value and register the callback
369 // function to update the cached value when the pref changes.
370 if (!gEmailWebAppDomainsPref) {
371 gEmailWebAppDomainsPref = new nsCString();
373 Preferences::RegisterCallbackAndCall(EmailWebAppDomainPrefChangeCallback,
374 kEmailWebAppDomainPrefName);
375 RunOnShutdown([]() {
376 Preferences::UnregisterCallback(EmailWebAppDomainPrefChangeCallback,
377 kEmailWebAppDomainPrefName);
378 gEmailWebAppDomainsPref = nullptr;
382 bool isTopEmailWebApp =
383 aFirstPartyPrincipal->IsURIInList(*gEmailWebAppDomainsPref);
384 uint32_t level1Count = level1SiteSet.Count();
385 uint32_t level2Count = level2SiteSet.Count();
387 Telemetry::Accumulate(
388 Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB,
389 isTopEmailWebApp ? "base_emailapp"_ns : "base_normal"_ns, level1Count);
390 Telemetry::Accumulate(
391 Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB,
392 isTopEmailWebApp ? "content_emailapp"_ns : "content_normal"_ns,
393 level2Count);
394 Telemetry::Accumulate(Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB,
395 isTopEmailWebApp ? "all_emailapp"_ns : "all_normal"_ns,
396 level1Count + level2Count);
399 ContentBlockingLog::OriginEntry* ContentBlockingLog::RecordLogInternal(
400 const nsACString& aOrigin, uint32_t aType, bool aBlocked,
401 const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
402 aReason,
403 const nsTArray<nsCString>& aTrackingFullHashes,
404 const Maybe<ContentBlockingNotifier::CanvasFingerprinter>&
405 aCanvasFingerprinter,
406 const Maybe<bool> aCanvasFingerprinterKnownText) {
407 DebugOnly<bool> isCookiesBlockedTracker =
408 aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER ||
409 aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER;
410 MOZ_ASSERT_IF(aBlocked, aReason.isNothing());
411 MOZ_ASSERT_IF(!isCookiesBlockedTracker, aReason.isNothing());
412 MOZ_ASSERT_IF(isCookiesBlockedTracker && !aBlocked, aReason.isSome());
414 if (aOrigin.IsVoid()) {
415 return nullptr;
417 auto index = mLog.IndexOf(aOrigin, 0, Comparator());
418 if (index != OriginDataTable::NoIndex) {
419 OriginEntry& entry = mLog[index];
420 if (!entry.mData) {
421 return nullptr;
424 if (RecordLogEntryInCustomField(aType, entry, aBlocked)) {
425 return &entry;
427 if (!entry.mData->mLogs.IsEmpty()) {
428 auto& last = entry.mData->mLogs.LastElement();
429 if (last.mType == aType && last.mBlocked == aBlocked &&
430 last.mCanvasFingerprinter == aCanvasFingerprinter &&
431 last.mCanvasFingerprinterKnownText == aCanvasFingerprinterKnownText) {
432 ++last.mRepeatCount;
433 // Don't record recorded events. This helps compress our log.
434 // We don't care about if the the reason is the same, just keep the
435 // first one.
436 // Note: {aReason, aTrackingFullHashes} are not compared here and we
437 // simply keep the first for the reason, and merge hashes to make sure
438 // they can be correctly recorded.
439 for (const auto& hash : aTrackingFullHashes) {
440 if (!last.mTrackingFullHashes.Contains(hash)) {
441 last.mTrackingFullHashes.AppendElement(hash);
444 return &entry;
447 if (entry.mData->mLogs.Length() ==
448 std::max(1u, StaticPrefs::browser_contentblocking_originlog_length())) {
449 // Cap the size at the maximum length adjustable by the pref
450 entry.mData->mLogs.RemoveElementAt(0);
452 entry.mData->mLogs.AppendElement(
453 LogEntry{aType, 1u, aBlocked, aReason, aTrackingFullHashes.Clone(),
454 aCanvasFingerprinter, aCanvasFingerprinterKnownText});
456 // Check suspicious fingerprinting activities if the origin hasn't already
457 // been marked.
458 // TODO(Bug 1864909): Moving the suspicious fingerprinting detection call
459 // out of here.
460 if ((aType == nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING ||
461 aType == nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) &&
462 !entry.mData->mHasSuspiciousFingerprintingActivity &&
463 nsRFPService::CheckSuspiciousFingerprintingActivity(
464 entry.mData->mLogs)) {
465 entry.mData->mHasSuspiciousFingerprintingActivity = true;
467 return &entry;
470 // The entry has not been found.
471 OriginEntry* entry = mLog.AppendElement();
472 if (NS_WARN_IF(!entry || !entry->mData)) {
473 return nullptr;
476 entry->mOrigin = aOrigin;
478 if (aType == nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) {
479 entry->mData->mHasLevel1TrackingContentLoaded = aBlocked;
480 } else if (aType ==
481 nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT) {
482 entry->mData->mHasLevel2TrackingContentLoaded = aBlocked;
483 } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) {
484 MOZ_ASSERT(entry->mData->mHasCookiesLoaded.isNothing());
485 entry->mData->mHasCookiesLoaded.emplace(aBlocked);
486 } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) {
487 MOZ_ASSERT(entry->mData->mHasTrackerCookiesLoaded.isNothing());
488 entry->mData->mHasTrackerCookiesLoaded.emplace(aBlocked);
489 } else if (aType ==
490 nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) {
491 MOZ_ASSERT(entry->mData->mHasSocialTrackerCookiesLoaded.isNothing());
492 entry->mData->mHasSocialTrackerCookiesLoaded.emplace(aBlocked);
493 } else {
494 entry->mData->mLogs.AppendElement(
495 LogEntry{aType, 1u, aBlocked, aReason, aTrackingFullHashes.Clone(),
496 aCanvasFingerprinter, aCanvasFingerprinterKnownText});
498 // Check suspicious fingerprinting activities if the origin hasn't been
499 // marked.
500 // TODO(Bug 1864909): Moving the suspicious fingerprinting detection call
501 // out of here.
502 if ((aType == nsIWebProgressListener::STATE_ALLOWED_CANVAS_FINGERPRINTING ||
503 aType == nsIWebProgressListener::STATE_ALLOWED_FONT_FINGERPRINTING) &&
504 nsRFPService::CheckSuspiciousFingerprintingActivity(
505 entry->mData->mLogs)) {
506 entry->mData->mHasSuspiciousFingerprintingActivity = true;
510 return entry;
513 } // namespace mozilla