Bug 1513298 - Fingerprinting url-classifier feature, r=dimi
authorAndrea Marchesini <amarchesini@mozilla.com>
Wed, 09 Jan 2019 12:16:04 +0100
changeset 453038 a11f752f828c
parent 453037 a43422e9e4da
child 453039 d503dc3fd033
push id35341
push usercsabou@mozilla.com
push dateWed, 09 Jan 2019 16:06:01 +0000
treeherdermozilla-central@29b2c2f57879 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdimi
bugs1513298
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1513298 - Fingerprinting url-classifier feature, r=dimi
modules/libpref/init/StaticPrefList.h
netwerk/url-classifier/UrlClassifierCommon.cpp
netwerk/url-classifier/UrlClassifierCommon.h
netwerk/url-classifier/UrlClassifierFeatureFactory.cpp
netwerk/url-classifier/UrlClassifierFeatureFingerprinting.cpp
netwerk/url-classifier/UrlClassifierFeatureFingerprinting.h
netwerk/url-classifier/UrlClassifierFeatureTrackingAnnotation.cpp
netwerk/url-classifier/UrlClassifierFeatureTrackingProtection.cpp
netwerk/url-classifier/moz.build
toolkit/components/antitracking/AntiTrackingCommon.cpp
toolkit/components/antitracking/AntiTrackingCommon.h
toolkit/components/url-classifier/tests/mochitest/mochitest.ini
toolkit/components/url-classifier/tests/mochitest/test_fingerprinting.html
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -1779,16 +1779,23 @@ VARCACHE_PREF(
 
 // Annotate channels based on the tracking protection list in all modes
 VARCACHE_PREF(
   "privacy.trackingprotection.annotate_channels",
    privacy_trackingprotection_annotate_channels,
   bool, true
 )
 
+// Block 3rd party fingerprinting resources.
+VARCACHE_PREF(
+  "privacy.trackingprotection.fingerprinting.enabled",
+   privacy_trackingprotection_fingerprinting_enabled,
+  bool, true
+)
+
 // Lower the priority of network loads for resources on the tracking protection
 // list.  Note that this requires the
 // privacy.trackingprotection.annotate_channels pref to be on in order to have
 // any effect.
 #ifdef NIGHTLY_BUILD
 # define PREF_VALUE true
 #else
 # define PREF_VALUE false
--- a/netwerk/url-classifier/UrlClassifierCommon.cpp
+++ b/netwerk/url-classifier/UrlClassifierCommon.cpp
@@ -82,23 +82,23 @@ LazyLogModule UrlClassifierCommon::sLog(
   RefPtr<dom::Document> doc = docShell->GetDocument();
   NS_ENSURE_TRUE_VOID(doc);
 
   nsCOMPtr<nsIURI> uri;
   aChannel->GetURI(getter_AddRefs(uri));
   pwin->NotifyContentBlockingState(aBlockedReason, aChannel, true, uri);
 }
 
-/* static */ bool
-UrlClassifierCommon::ShouldEnableTrackingProtectionOrAnnotation(
+/* static */ bool UrlClassifierCommon::ShouldEnableClassifier(
     nsIChannel* aChannel,
     AntiTrackingCommon::ContentBlockingAllowListPurpose aBlockingPurpose) {
   MOZ_ASSERT(aChannel);
   MOZ_ASSERT(aBlockingPurpose == AntiTrackingCommon::eTrackingProtection ||
-             aBlockingPurpose == AntiTrackingCommon::eTrackingAnnotations);
+             aBlockingPurpose == AntiTrackingCommon::eTrackingAnnotations ||
+             aBlockingPurpose == AntiTrackingCommon::eFingerprinting);
 
   nsCOMPtr<nsIHttpChannelInternal> channel = do_QueryInterface(aChannel);
   if (!channel) {
     UC_LOG(("nsChannelClassifier: Not an HTTP channel"));
     return false;
   }
 
   nsCOMPtr<nsIURI> chanURI;
--- a/netwerk/url-classifier/UrlClassifierCommon.h
+++ b/netwerk/url-classifier/UrlClassifierCommon.h
@@ -28,17 +28,17 @@ class UrlClassifierCommon final {
   static bool AddonMayLoad(nsIChannel* aChannel, nsIURI* aURI);
 
   static void NotifyTrackingProtectionDisabled(nsIChannel* aChannel);
 
   // aBlockedReason must be one of the nsIWebProgressListener state.
   static void NotifyChannelBlocked(nsIChannel* aChannel,
                                    unsigned aBlockedReason);
 
-  static bool ShouldEnableTrackingProtectionOrAnnotation(
+  static bool ShouldEnableClassifier(
       nsIChannel* aChannel,
       AntiTrackingCommon::ContentBlockingAllowListPurpose aBlockingPurpose);
 
   static nsresult SetBlockedContent(nsIChannel* channel, nsresult aErrorCode,
                                     const nsACString& aList,
                                     const nsACString& aProvider,
                                     const nsACString& aFullHash);
 
--- a/netwerk/url-classifier/UrlClassifierFeatureFactory.cpp
+++ b/netwerk/url-classifier/UrlClassifierFeatureFactory.cpp
@@ -2,16 +2,17 @@
 /* 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 "mozilla/net/UrlClassifierFeatureFactory.h"
 
 // List of Features
+#include "UrlClassifierFeatureFingerprinting.h"
 #include "UrlClassifierFeatureFlash.h"
 #include "UrlClassifierFeatureLoginReputation.h"
 #include "UrlClassifierFeatureTrackingProtection.h"
 #include "UrlClassifierFeatureTrackingAnnotation.h"
 #include "UrlClassifierFeatureCustomTables.h"
 
 #include "nsAppRunner.h"
 
@@ -19,16 +20,17 @@ namespace mozilla {
 namespace net {
 
 /* static */ void UrlClassifierFeatureFactory::Shutdown() {
   // We want to expose Features only in the parent process.
   if (!XRE_IsParentProcess()) {
     return;
   }
 
+  UrlClassifierFeatureFingerprinting::MaybeShutdown();
   UrlClassifierFeatureFlash::MaybeShutdown();
   UrlClassifierFeatureLoginReputation::MaybeShutdown();
   UrlClassifierFeatureTrackingAnnotation::MaybeShutdown();
   UrlClassifierFeatureTrackingProtection::MaybeShutdown();
 }
 
 /* static */ void UrlClassifierFeatureFactory::GetFeaturesFromChannel(
     nsIChannel* aChannel,
@@ -38,16 +40,22 @@ namespace net {
 
   nsCOMPtr<nsIUrlClassifierFeature> feature;
 
   // Note that the order of the features is extremely important! When more than
   // 1 feature classifies the channel, we call ::ProcessChannel() following this
   // feature order, and this could produce different results with a different
   // feature ordering.
 
+  // Fingerprinting
+  feature = UrlClassifierFeatureFingerprinting::MaybeCreate(aChannel);
+  if (feature) {
+    aFeatures.AppendElement(feature);
+  }
+
   // Tracking Protection
   feature = UrlClassifierFeatureTrackingProtection::MaybeCreate(aChannel);
   if (feature) {
     aFeatures.AppendElement(feature);
   }
 
   // Tracking Annotation
   feature = UrlClassifierFeatureTrackingAnnotation::MaybeCreate(aChannel);
@@ -70,16 +78,22 @@ UrlClassifierFeatureFactory::GetFeatureL
 /* static */ already_AddRefed<nsIUrlClassifierFeature>
 UrlClassifierFeatureFactory::GetFeatureByName(const nsACString& aName) {
   if (!XRE_IsParentProcess()) {
     return nullptr;
   }
 
   nsCOMPtr<nsIUrlClassifierFeature> feature;
 
+  // Fingerprinting
+  feature = UrlClassifierFeatureFingerprinting::GetIfNameMatches(aName);
+  if (feature) {
+    return feature.forget();
+  }
+
   // Tracking Protection
   feature = UrlClassifierFeatureTrackingProtection::GetIfNameMatches(aName);
   if (feature) {
     return feature.forget();
   }
 
   // Tracking Annotation
   feature = UrlClassifierFeatureTrackingAnnotation::GetIfNameMatches(aName);
@@ -103,18 +117,24 @@ UrlClassifierFeatureFactory::GetFeatureB
 }
 
 /* static */ void UrlClassifierFeatureFactory::GetFeatureNames(
     nsTArray<nsCString>& aArray) {
   if (!XRE_IsParentProcess()) {
     return;
   }
 
+  // Fingerprinting
+  nsAutoCString name;
+  name.Assign(UrlClassifierFeatureFingerprinting::Name());
+  if (!name.IsEmpty()) {
+    aArray.AppendElement(name);
+  }
+
   // Tracking Protection
-  nsAutoCString name;
   name.Assign(UrlClassifierFeatureTrackingProtection::Name());
   if (!name.IsEmpty()) {
     aArray.AppendElement(name);
   }
 
   // Tracking Annotation
   name.Assign(UrlClassifierFeatureTrackingAnnotation::Name());
   if (!name.IsEmpty()) {
new file mode 100644
--- /dev/null
+++ b/netwerk/url-classifier/UrlClassifierFeatureFingerprinting.cpp
@@ -0,0 +1,177 @@
+/* -*- 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 "UrlClassifierFeatureFingerprinting.h"
+
+#include "mozilla/AntiTrackingCommon.h"
+#include "mozilla/net/UrlClassifierCommon.h"
+#include "mozilla/StaticPrefs.h"
+#include "nsContentUtils.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+namespace net {
+
+namespace {
+
+#define FINGERPRINTING_FEATURE_NAME "fingerprinting"
+
+#define URLCLASSIFIER_FINGERPRINTING_BLACKLIST \
+  "urlclassifier.features.fingerprinting.blacklistTables"
+#define URLCLASSIFIER_FINGERPRINTING_BLACKLIST_TEST_ENTRIES \
+  "urlclassifier.features.fingerprinting.blacklistHosts"
+#define URLCLASSIFIER_FINGERPRINTING_WHITELIST \
+  "urlclassifier.features.fingerprinting.whitelistTables"
+#define URLCLASSIFIER_FINGERPRINTING_WHITELIST_TEST_ENTRIES \
+  "urlclassifier.features.fingerprinting.whitelistHosts"
+#define TABLE_FINGERPRINTING_BLACKLIST_PREF "fingerprinting-blacklist-pref"
+#define TABLE_FINGERPRINTING_WHITELIST_PREF "fingerprinting-whitelist-pref"
+
+StaticRefPtr<UrlClassifierFeatureFingerprinting> gFeatureFingerprinting;
+
+}  // namespace
+
+UrlClassifierFeatureFingerprinting::UrlClassifierFeatureFingerprinting()
+    : UrlClassifierFeatureBase(
+          NS_LITERAL_CSTRING(FINGERPRINTING_FEATURE_NAME),
+          NS_LITERAL_CSTRING(URLCLASSIFIER_FINGERPRINTING_BLACKLIST),
+          NS_LITERAL_CSTRING(URLCLASSIFIER_FINGERPRINTING_WHITELIST),
+          NS_LITERAL_CSTRING(
+              URLCLASSIFIER_FINGERPRINTING_BLACKLIST_TEST_ENTRIES),
+          NS_LITERAL_CSTRING(
+              URLCLASSIFIER_FINGERPRINTING_WHITELIST_TEST_ENTRIES),
+          NS_LITERAL_CSTRING(TABLE_FINGERPRINTING_BLACKLIST_PREF),
+          NS_LITERAL_CSTRING(TABLE_FINGERPRINTING_WHITELIST_PREF),
+          EmptyCString()) {}
+
+/* static */ const char* UrlClassifierFeatureFingerprinting::Name() {
+  return FINGERPRINTING_FEATURE_NAME;
+}
+
+/* static */ void UrlClassifierFeatureFingerprinting::MaybeInitialize() {
+  UC_LOG(("UrlClassifierFeatureFingerprinting: MaybeInitialize"));
+
+  if (!gFeatureFingerprinting) {
+    gFeatureFingerprinting = new UrlClassifierFeatureFingerprinting();
+    gFeatureFingerprinting->InitializePreferences();
+  }
+}
+
+/* static */ void UrlClassifierFeatureFingerprinting::MaybeShutdown() {
+  UC_LOG(("UrlClassifierFeatureFingerprinting: MaybeShutdown"));
+
+  if (gFeatureFingerprinting) {
+    gFeatureFingerprinting->ShutdownPreferences();
+    gFeatureFingerprinting = nullptr;
+  }
+}
+
+/* static */ already_AddRefed<UrlClassifierFeatureFingerprinting>
+UrlClassifierFeatureFingerprinting::MaybeCreate(nsIChannel* aChannel) {
+  MOZ_ASSERT(aChannel);
+
+  UC_LOG(("UrlClassifierFeatureFingerprinting: MaybeCreate for channel %p",
+          aChannel));
+
+  if (!StaticPrefs::privacy_trackingprotection_fingerprinting_enabled()) {
+    return nullptr;
+  }
+
+  nsCOMPtr<nsIURI> chanURI;
+  nsresult rv = aChannel->GetURI(getter_AddRefs(chanURI));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return nullptr;
+  }
+
+  bool isThirdParty =
+      nsContentUtils::IsThirdPartyWindowOrChannel(nullptr, aChannel, chanURI);
+  if (!isThirdParty) {
+    if (UC_LOG_ENABLED()) {
+      nsCString spec = chanURI->GetSpecOrDefault();
+      spec.Truncate(
+          std::min(spec.Length(), UrlClassifierCommon::sMaxSpecLength));
+      UC_LOG(
+          ("UrlClassifierFeatureFingerprinting: Skipping fingerprinting checks "
+           "for first party or top-level load channel[%p] "
+           "with uri %s",
+           aChannel, spec.get()));
+    }
+
+    return nullptr;
+  }
+
+  if (!UrlClassifierCommon::ShouldEnableClassifier(
+          aChannel, AntiTrackingCommon::eFingerprinting)) {
+    return nullptr;
+  }
+
+  MaybeInitialize();
+  MOZ_ASSERT(gFeatureFingerprinting);
+
+  RefPtr<UrlClassifierFeatureFingerprinting> self = gFeatureFingerprinting;
+  return self.forget();
+}
+
+/* static */ already_AddRefed<nsIUrlClassifierFeature>
+UrlClassifierFeatureFingerprinting::GetIfNameMatches(const nsACString& aName) {
+  if (!aName.EqualsLiteral(FINGERPRINTING_FEATURE_NAME)) {
+    return nullptr;
+  }
+
+  MaybeInitialize();
+  MOZ_ASSERT(gFeatureFingerprinting);
+
+  RefPtr<UrlClassifierFeatureFingerprinting> self = gFeatureFingerprinting;
+  return self.forget();
+}
+
+NS_IMETHODIMP
+UrlClassifierFeatureFingerprinting::ProcessChannel(nsIChannel* aChannel,
+                                                   const nsACString& aList,
+                                                   bool* aShouldContinue) {
+  NS_ENSURE_ARG_POINTER(aChannel);
+  NS_ENSURE_ARG_POINTER(aShouldContinue);
+
+  // This is a blocking feature.
+  *aShouldContinue = false;
+
+  UrlClassifierCommon::SetBlockedContent(aChannel, NS_ERROR_TRACKING_URI, aList,
+                                         EmptyCString(), EmptyCString());
+
+  UC_LOG(
+      ("UrlClassifierFeatureFingerprinting::ProcessChannel, cancelling "
+       "channel[%p]",
+       aChannel));
+  nsCOMPtr<nsIHttpChannelInternal> httpChannel = do_QueryInterface(aChannel);
+
+  // FIXME: the way we cancel the channel depends on what the UI wants to show.
+  // This needs to change, at some point.
+  if (httpChannel) {
+    Unused << httpChannel->CancelForTrackingProtection();
+  } else {
+    Unused << aChannel->Cancel(NS_ERROR_TRACKING_URI);
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+UrlClassifierFeatureFingerprinting::GetURIByListType(
+    nsIChannel* aChannel, nsIUrlClassifierFeature::listType aListType,
+    nsIURI** aURI) {
+  NS_ENSURE_ARG_POINTER(aChannel);
+  NS_ENSURE_ARG_POINTER(aURI);
+
+  if (aListType == nsIUrlClassifierFeature::blacklist) {
+    return aChannel->GetURI(aURI);
+  }
+
+  MOZ_ASSERT(aListType == nsIUrlClassifierFeature::whitelist);
+  return UrlClassifierCommon::CreatePairwiseWhiteListURI(aChannel, aURI);
+}
+
+}  // namespace net
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/netwerk/url-classifier/UrlClassifierFeatureFingerprinting.h
@@ -0,0 +1,46 @@
+/* -*- 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_net_UrlClassifierFeatureFingerprinting_h
+#define mozilla_net_UrlClassifierFeatureFingerprinting_h
+
+#include "UrlClassifierFeatureBase.h"
+
+class nsIChannel;
+
+namespace mozilla {
+namespace net {
+
+class UrlClassifierFeatureFingerprinting final
+    : public UrlClassifierFeatureBase {
+ public:
+  static const char* Name();
+
+  static void MaybeShutdown();
+
+  static already_AddRefed<UrlClassifierFeatureFingerprinting> MaybeCreate(
+      nsIChannel* aChannel);
+
+  static already_AddRefed<nsIUrlClassifierFeature> GetIfNameMatches(
+      const nsACString& aName);
+
+  NS_IMETHOD ProcessChannel(nsIChannel* aChannel, const nsACString& aList,
+                            bool* aShouldContinue) override;
+
+  NS_IMETHOD GetURIByListType(nsIChannel* aChannel,
+                              nsIUrlClassifierFeature::listType aListType,
+                              nsIURI** aURI) override;
+
+ private:
+  UrlClassifierFeatureFingerprinting();
+
+  static void MaybeInitialize();
+};
+
+}  // namespace net
+}  // namespace mozilla
+
+#endif  // mozilla_net_UrlClassifierFeatureFingerprinting_h
--- a/netwerk/url-classifier/UrlClassifierFeatureTrackingAnnotation.cpp
+++ b/netwerk/url-classifier/UrlClassifierFeatureTrackingAnnotation.cpp
@@ -149,17 +149,17 @@ UrlClassifierFeatureTrackingAnnotation::
 
   UC_LOG(("UrlClassifierFeatureTrackingAnnotation: MaybeCreate for channel %p",
           aChannel));
 
   if (!StaticPrefs::privacy_trackingprotection_annotate_channels()) {
     return nullptr;
   }
 
-  if (!UrlClassifierCommon::ShouldEnableTrackingProtectionOrAnnotation(
+  if (!UrlClassifierCommon::ShouldEnableClassifier(
           aChannel, AntiTrackingCommon::eTrackingAnnotations)) {
     return nullptr;
   }
 
   MaybeInitialize();
   MOZ_ASSERT(gFeatureTrackingAnnotation);
 
   RefPtr<UrlClassifierFeatureTrackingAnnotation> self =
--- a/netwerk/url-classifier/UrlClassifierFeatureTrackingProtection.cpp
+++ b/netwerk/url-classifier/UrlClassifierFeatureTrackingProtection.cpp
@@ -96,17 +96,17 @@ UrlClassifierFeatureTrackingProtection::
            "protection checks for first party or top-level load channel[%p] "
            "with uri %s",
            aChannel, spec.get()));
     }
 
     return nullptr;
   }
 
-  if (!UrlClassifierCommon::ShouldEnableTrackingProtectionOrAnnotation(
+  if (!UrlClassifierCommon::ShouldEnableClassifier(
           aChannel, AntiTrackingCommon::eTrackingProtection)) {
     return nullptr;
   }
 
   MaybeInitialize();
   MOZ_ASSERT(gFeatureTrackingProtection);
 
   RefPtr<UrlClassifierFeatureTrackingProtection> self =
--- a/netwerk/url-classifier/moz.build
+++ b/netwerk/url-classifier/moz.build
@@ -19,16 +19,17 @@ DEFINES['GOOGLE_PROTOBUF_NO_STATIC_INITI
 
 UNIFIED_SOURCES += [
     'AsyncUrlChannelClassifier.cpp',
     'nsChannelClassifier.cpp',
     'UrlClassifierCommon.cpp',
     'UrlClassifierFeatureBase.cpp',
     'UrlClassifierFeatureCustomTables.cpp',
     'UrlClassifierFeatureFactory.cpp',
+    'UrlClassifierFeatureFingerprinting.cpp',
     'UrlClassifierFeatureFlash.cpp',
     'UrlClassifierFeatureLoginReputation.cpp',
     'UrlClassifierFeatureResult.cpp',
     'UrlClassifierFeatureTrackingAnnotation.cpp',
     'UrlClassifierFeatureTrackingProtection.cpp',
 ]
 
 EXPORTS.mozilla.net += [
--- a/toolkit/components/antitracking/AntiTrackingCommon.cpp
+++ b/toolkit/components/antitracking/AntiTrackingCommon.cpp
@@ -1367,17 +1367,19 @@ nsresult AntiTrackingCommon::IsOnContent
   aIsAllowListed = false;
 
   // For storage checks, check the storage pref, and for annotations checks,
   // check the corresponding pref as well.  This allows each set of checks to
   // be disabled individually if needed.
   if ((aPurpose == eStorageChecks &&
        !StaticPrefs::browser_contentblocking_allowlist_storage_enabled()) ||
       (aPurpose == eTrackingAnnotations &&
-       !StaticPrefs::browser_contentblocking_allowlist_annotations_enabled())) {
+       !StaticPrefs::browser_contentblocking_allowlist_annotations_enabled()) ||
+      (aPurpose == eFingerprinting &&
+       !StaticPrefs::privacy_trackingprotection_fingerprinting_enabled())) {
     LOG(
         ("Attempting to check the content blocking allow list aborted because "
          "the third-party cookies UI has been disabled."));
     return NS_OK;
   }
 
   LOG_SPEC(("Deciding whether the user has overridden content blocking for %s",
             _spec),
--- a/toolkit/components/antitracking/AntiTrackingCommon.h
+++ b/toolkit/components/antitracking/AntiTrackingCommon.h
@@ -115,16 +115,17 @@ class AntiTrackingCommon final {
       nsIPrincipal* aPrincipal, nsIPrincipal* aTrackingPrinciapl,
       const nsCString& aParentOrigin, const nsCString& aGrantedOrigin,
       int aAllowMode);
 
   enum ContentBlockingAllowListPurpose {
     eStorageChecks,
     eTrackingProtection,
     eTrackingAnnotations,
+    eFingerprinting,
   };
 
   // Check whether a top window URI is on the content blocking allow list.
   static nsresult IsOnContentBlockingAllowList(
       nsIURI* aTopWinURI, bool aIsPrivateBrowsing,
       ContentBlockingAllowListPurpose aPurpose, bool& aIsAllowListed);
 
   enum class BlockingDecision {
--- a/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
+++ b/toolkit/components/url-classifier/tests/mochitest/mochitest.ini
@@ -40,8 +40,9 @@ skip-if = (os == 'linux' && debug) #Bug 
 [test_classify_ping.html]
 skip-if = (verify && debug && (os == 'win' || os == 'mac'))
 [test_classify_track.html]
 [test_gethash.html]
 [test_bug1254766.html]
 [test_cachemiss.html]
 skip-if = verify
 [test_annotation_vs_TP.html]
+[test_fingerprinting.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/tests/mochitest/test_fingerprinting.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test the fingerprinting classifier</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script class="testbody" type="text/javascript">
+
+/* eslint-env mozilla/frame-script */
+
+var tests = [
+  // All disabled.
+  { config: [ false, false ], loadExpected: true },
+
+  // Just whitelisted.
+  { config: [ false, true ], loadExpected: true },
+
+  // Just blacklisted.
+  { config: [ true, false ], loadExpected: false },
+
+  // whitelist + blacklist: whitelist wins
+  { config: [ true, true ], loadExpected: true },
+];
+
+function prefValue(value, what) {
+  return value ? what : "";
+}
+
+async function runTest(test) {
+  await SpecialPowers.pushPrefEnv({set: [
+    [ "urlclassifier.features.fingerprinting.blacklistHosts", prefValue(test.config[0], "example.com") ],
+    [ "urlclassifier.features.fingerprinting.whitelistHosts", prefValue(test.config[1], "mochi.test") ],
+    [ "urlclassifier.features.fingerprinting.blacklistTables", prefValue(test.config[0], "mochitest1-track-simple") ],
+    [ "urlclassifier.features.fingerprinting.whitelistTables", "" ],
+    [ "privacy.trackingprotection.enabled", false ],
+    [ "privacy.trackingprotection.annotate_channels", false ],
+    [ "privacy.trackingprotection.fingerprinting.enabled", true ],
+  ]});
+
+  info("Testing: " + test.config.toSource() + "\n");
+
+  // Let's load an image with a random query string, just to avoid network cache.
+  let result = await new Promise(resolve => {
+    let image = new Image();
+    image.src = "http://example.com/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg?" + Math.random();
+    image.onload = _ => resolve(true);
+    image.onerror = _ => resolve(false);
+  });
+
+  is(result, test.loadExpected, "The loading happened correctly");
+
+  // Let's load an image with a random query string, just to avoid network cache.
+  result = await new Promise(resolve => {
+    let image = new Image();
+    image.src = "http://tracking.example.org/tests/toolkit/components/url-classifier/tests/mochitest/raptor.jpg?" + Math.random();
+    image.onload = _ => resolve(true);
+    image.onerror = _ => resolve(false);
+  });
+
+  is(result, test.loadExpected, "The loading happened correctly (by table)");
+}
+
+async function runTests() {
+  let chromeScript = SpecialPowers.loadChromeScript(_ => {
+    ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm");
+
+    addMessageListener("loadTrackers", __ => {
+      UrlClassifierTestUtils.addTestTrackers().then(___ => {
+        sendAsyncMessage("trackersLoaded");
+      });
+    });
+
+    addMessageListener("unloadTrackers", __ => {
+      UrlClassifierTestUtils.cleanupTestTrackers();
+      sendAsyncMessage("trackersUnloaded");
+    });
+  });
+
+  await new Promise(resolve => {
+    chromeScript.addMessageListener("trackersLoaded", resolve);
+    chromeScript.sendAsyncMessage("loadTrackers");
+  });
+
+  for (let test in tests) {
+    await runTest(tests[test]);
+  }
+
+  await new Promise(resolve => {
+    chromeScript.addMessageListener("trackersUnloaded", resolve);
+    chromeScript.sendSyncMessage("unloadTrackers");
+  });
+
+  chromeScript.destroy();
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+runTests();
+
+</script>
+</body>
+</html>