Bug 1011799: Integrate nsIRedirectChannel redirects into application reputation check
authorMonica Chew <mmc@mozilla.com>
Wed, 28 May 2014 16:06:21 -0700
changeset 185489 566a41f1dbaa074096fcad2e7e84a1781a6da982
parent 185488 d034c61a73182c7b30f611d5f0bc6e46df919cd0
child 185490 f92292ad5bea0ea4a10b29dbe051dce9fa0b8076
push id44097
push usermchew@mozilla.com
push dateWed, 28 May 2014 23:06:55 +0000
treeherdermozilla-inbound@566a41f1dbaa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1011799
milestone32.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 1011799: Integrate nsIRedirectChannel redirects into application reputation check
toolkit/components/downloads/ApplicationReputation.cpp
toolkit/components/downloads/nsDownloadManager.cpp
toolkit/components/downloads/nsDownloadManager.h
toolkit/components/downloads/nsDownloadProxy.h
toolkit/components/downloads/nsIApplicationReputation.idl
toolkit/components/downloads/nsIDownload.idl
toolkit/components/downloads/test/unit/test_app_rep.js
toolkit/components/downloads/test/unit/test_app_rep_windows.js
toolkit/components/jsdownloads/src/DownloadCore.jsm
toolkit/components/jsdownloads/src/DownloadIntegration.jsm
toolkit/components/jsdownloads/src/DownloadLegacy.js
uriloader/base/nsITransfer.idl
uriloader/exthandler/nsExternalHelperAppService.cpp
uriloader/exthandler/nsExternalHelperAppService.h
--- a/toolkit/components/downloads/ApplicationReputation.cpp
+++ b/toolkit/components/downloads/ApplicationReputation.cpp
@@ -42,18 +42,19 @@
 #include "nsTArray.h"
 #include "nsThreadUtils.h"
 #include "nsXPCOMStrings.h"
 
 using mozilla::Preferences;
 using mozilla::TimeStamp;
 using mozilla::Telemetry::Accumulate;
 using safe_browsing::ClientDownloadRequest;
+using safe_browsing::ClientDownloadRequest_CertificateChain;
+using safe_browsing::ClientDownloadRequest_Resource;
 using safe_browsing::ClientDownloadRequest_SignatureInfo;
-using safe_browsing::ClientDownloadRequest_CertificateChain;
 
 // Preferences that we need to initialize the query.
 #define PREF_SB_APP_REP_URL "browser.safebrowsing.appRepURL"
 #define PREF_SB_MALWARE_ENABLED "browser.safebrowsing.malware.enabled"
 #define PREF_GENERAL_LOCALE "general.useragent.locale"
 #define PREF_DOWNLOAD_BLOCK_TABLE "urlclassifier.downloadBlockTable"
 #define PREF_DOWNLOAD_ALLOW_TABLE "urlclassifier.downloadAllowTable"
 
@@ -113,19 +114,21 @@ private:
   // the downloaded file.
   nsTArray<nsCString> mAllowlistSpecs;
   // The source URI of the download, the referrer and possibly any redirects.
   nsTArray<nsCString> mAnylistSpecs;
 
   // When we started this query
   TimeStamp mStartTime;
 
-  // The protocol buffer used to store signature information extracted using
-  // the Windows Authenticode API, if the binary is signed.
-  ClientDownloadRequest_SignatureInfo mSignatureInfo;
+  // A protocol buffer for storing things we need in the remote request. We
+  // store the resource chain (redirect information) as well as signature
+  // information extracted using the Windows Authenticode API, if the binary is
+  // signed.
+  ClientDownloadRequest mRequest;
 
   // The response from the application reputation query. This is read in chunks
   // as part of our nsIStreamListener implementation and may contain embedded
   // NULLs.
   nsCString mResponse;
 
   // Returns true if the file is likely to be binary on Windows.
   bool IsBinaryFile();
@@ -157,23 +160,24 @@ private:
   nsresult GenerateWhitelistStringsForChain(
     const ClientDownloadRequest_CertificateChain& aChain);
 
   // For signed binaries, generate strings of the form:
   // http://sb-ssl.google.com/safebrowsing/csd/certificate/
   //   <issuer_cert_sha1_fingerprint>[/CN=<cn>][/O=<org>][/OU=<unit>]
   // for each (cert, issuer) pair in each chain of certificates that is
   // associated with the binary.
-  nsresult GenerateWhitelistStrings(
-    const ClientDownloadRequest_SignatureInfo& aSignatureInfo);
+  nsresult GenerateWhitelistStrings();
 
   // Parse the XPCOM certificate lists and stick them into the protocol buffer
   // version.
-  nsresult ParseCertificates(nsIArray* aSigArray,
-                             ClientDownloadRequest_SignatureInfo* aSigInfo);
+  nsresult ParseCertificates(nsIArray* aSigArray);
+
+  // Adds the redirects to mAnylistSpecs to be looked up.
+  nsresult AddRedirects(nsIArray* aRedirects);
 
   // Helper function to ensure that we call PendingLookup::LookupNext or
   // PendingLookup::OnComplete.
   nsresult DoLookupInternal();
 
   // Looks up all the URIs that may be responsible for allowlisting or
   // blocklisting the downloaded file. These URIs may include whitelist strings
   // generated by certificates verifying the binary as well as the target URI
@@ -524,22 +528,65 @@ PendingLookup::GenerateWhitelistStringsF
 
     nsresult rv = GenerateWhitelistStringsForPair(signer, issuer);
     NS_ENSURE_SUCCESS(rv, rv);
   }
   return NS_OK;
 }
 
 nsresult
-PendingLookup::GenerateWhitelistStrings(
-  const safe_browsing::ClientDownloadRequest_SignatureInfo& aSignatureInfo)
+PendingLookup::GenerateWhitelistStrings()
+{
+  for (int i = 0; i < mRequest.signature().certificate_chain_size(); ++i) {
+    nsresult rv = GenerateWhitelistStringsForChain(
+      mRequest.signature().certificate_chain(i));
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+  return NS_OK;
+}
+
+nsresult
+PendingLookup::AddRedirects(nsIArray* aRedirects)
 {
-  for (int i = 0; i < aSignatureInfo.certificate_chain_size(); ++i) {
-    nsresult rv = GenerateWhitelistStringsForChain(
-      aSignatureInfo.certificate_chain(i));
+  uint32_t length = 0;
+  aRedirects->GetLength(&length);
+  LOG(("ApplicationReputation: Got %u redirects", length));
+  nsCOMPtr<nsISimpleEnumerator> iter;
+  nsresult rv = aRedirects->Enumerate(getter_AddRefs(iter));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  bool hasMoreRedirects = false;
+  rv = iter->HasMoreElements(&hasMoreRedirects);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  while (hasMoreRedirects) {
+    nsCOMPtr<nsISupports> supports;
+    rv = iter->GetNext(getter_AddRefs(supports));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIPrincipal> principal = do_QueryInterface(supports, &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIURI> uri;
+    rv = principal->GetURI(getter_AddRefs(uri));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Add the spec to our list of local lookups.
+    nsCString spec;
+    rv = uri->GetSpec(spec);
+    NS_ENSURE_SUCCESS(rv, rv);
+    mAnylistSpecs.AppendElement(spec);
+    LOG(("ApplicationReputation: Appending redirect %s\n", spec.get()));
+
+    // Store the redirect information in the remote request.
+    ClientDownloadRequest_Resource* resource = mRequest.add_resources();
+    resource->set_url(spec.get());
+    resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT);
+
+    rv = iter->HasMoreElements(&hasMoreRedirects);
     NS_ENSURE_SUCCESS(rv, rv);
   }
   return NS_OK;
 }
 
 nsresult
 PendingLookup::StartLookup()
 {
@@ -549,47 +596,59 @@ PendingLookup::StartLookup()
     return OnComplete(false, NS_OK);
   };
   return rv;
 }
 
 nsresult
 PendingLookup::DoLookupInternal()
 {
-  // We want to check the target URI against the local lists.
+  // We want to check the target URI, its referrer, and associated redirects
+  // against the local lists.
   nsCOMPtr<nsIURI> uri;
   nsresult rv = mQuery->GetSourceURI(getter_AddRefs(uri));
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCString spec;
   rv = uri->GetSpec(spec);
   NS_ENSURE_SUCCESS(rv, rv);
   mAnylistSpecs.AppendElement(spec);
+  ClientDownloadRequest_Resource* resource = mRequest.add_resources();
+  resource->set_url(spec.get());
+  resource->set_type(ClientDownloadRequest::DOWNLOAD_URL);
 
   nsCOMPtr<nsIURI> referrer = nullptr;
   rv = mQuery->GetReferrerURI(getter_AddRefs(referrer));
   if (referrer) {
     nsCString spec;
     rv = referrer->GetSpec(spec);
     NS_ENSURE_SUCCESS(rv, rv);
     mAnylistSpecs.AppendElement(spec);
+    resource->set_referrer(spec.get());
+  }
+  nsCOMPtr<nsIArray> redirects;
+  rv = mQuery->GetRedirects(getter_AddRefs(redirects));
+  if (redirects) {
+    AddRedirects(redirects);
+  } else {
+    LOG(("ApplicationReputation: Got no redirects"));
   }
 
   // Extract the signature and parse certificates so we can use it to check
   // whitelists.
   nsCOMPtr<nsIArray> sigArray;
   rv = mQuery->GetSignatureInfo(getter_AddRefs(sigArray));
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (sigArray) {
-    rv = ParseCertificates(sigArray, &mSignatureInfo);
+    rv = ParseCertificates(sigArray);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  rv = GenerateWhitelistStrings(mSignatureInfo);
+  rv = GenerateWhitelistStrings();
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Start the call chain.
   return LookupNext();
 }
 
 nsresult
 PendingLookup::OnComplete(bool shouldBlock, nsresult rv)
@@ -605,19 +664,17 @@ PendingLookup::OnComplete(bool shouldBlo
   } else {
     LOG(("Application Reputation check passed in %f ms [this = %p]", t, this));
   }
   nsresult res = mCallback->OnComplete(shouldBlock, rv);
   return res;
 }
 
 nsresult
-PendingLookup::ParseCertificates(
-  nsIArray* aSigArray,
-  ClientDownloadRequest_SignatureInfo* aSignatureInfo)
+PendingLookup::ParseCertificates(nsIArray* aSigArray)
 {
   // If we haven't been set for any reason, bail.
   NS_ENSURE_ARG_POINTER(aSigArray);
 
   // Binaries may be signed by multiple chains of certificates. If there are no
   // chains, the binary is unsigned (or we were unable to extract signature
   // information on a non-Windows platform)
   nsCOMPtr<nsISimpleEnumerator> chains;
@@ -632,17 +689,17 @@ PendingLookup::ParseCertificates(
     nsCOMPtr<nsISupports> supports;
     rv = chains->GetNext(getter_AddRefs(supports));
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsCOMPtr<nsIX509CertList> certList = do_QueryInterface(supports, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
 
     safe_browsing::ClientDownloadRequest_CertificateChain* certChain =
-      aSignatureInfo->add_certificate_chain();
+      mRequest.mutable_signature()->add_certificate_chain();
     nsCOMPtr<nsISimpleEnumerator> chainElt;
     rv = certList->GetEnumerator(getter_AddRefs(chainElt));
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Each chain may have multiple certificates.
     bool hasMoreCerts = false;
     rv = chainElt->HasMoreElements(&hasMoreCerts);
     while (hasMoreCerts) {
@@ -663,18 +720,18 @@ PendingLookup::ParseCertificates(
       nsMemory::Free(data);
 
       rv = chainElt->HasMoreElements(&hasMoreCerts);
       NS_ENSURE_SUCCESS(rv, rv);
     }
     rv = chains->HasMoreElements(&hasMoreChains);
     NS_ENSURE_SUCCESS(rv, rv);
   }
-  if (aSignatureInfo->certificate_chain_size() > 0) {
-    aSignatureInfo->set_trusted(true);
+  if (mRequest.signature().certificate_chain_size() > 0) {
+    mRequest.mutable_signature()->set_trusted(true);
   }
   return NS_OK;
 }
 
 nsresult
 PendingLookup::SendRemoteQuery()
 {
   nsresult rv = SendRemoteQueryInternal();
@@ -687,60 +744,58 @@ PendingLookup::SendRemoteQuery()
 }
 
 nsresult
 PendingLookup::SendRemoteQueryInternal()
 {
   LOG(("Sending remote query for application reputation [this = %p]", this));
   // We did not find a local result, so fire off the query to the application
   // reputation service.
-  safe_browsing::ClientDownloadRequest req;
   nsCOMPtr<nsIURI> uri;
   nsresult rv;
   rv = mQuery->GetSourceURI(getter_AddRefs(uri));
   NS_ENSURE_SUCCESS(rv, rv);
   nsCString spec;
   rv = uri->GetSpec(spec);
   NS_ENSURE_SUCCESS(rv, rv);
-  req.set_url(spec.get());
+  mRequest.set_url(spec.get());
 
   uint32_t fileSize;
   rv = mQuery->GetFileSize(&fileSize);
   NS_ENSURE_SUCCESS(rv, rv);
-  req.set_length(fileSize);
+  mRequest.set_length(fileSize);
   // We have no way of knowing whether or not a user initiated the download.
-  req.set_user_initiated(false);
+  mRequest.set_user_initiated(false);
 
   nsCString locale;
   NS_ENSURE_SUCCESS(Preferences::GetCString(PREF_GENERAL_LOCALE, &locale),
                     NS_ERROR_NOT_AVAILABLE);
-  req.set_locale(locale.get());
+  mRequest.set_locale(locale.get());
   nsCString sha256Hash;
   rv = mQuery->GetSha256Hash(sha256Hash);
   NS_ENSURE_SUCCESS(rv, rv);
-  req.mutable_digests()->set_sha256(sha256Hash.Data());
+  mRequest.mutable_digests()->set_sha256(sha256Hash.Data());
   nsString fileName;
   rv = mQuery->GetSuggestedFileName(fileName);
   NS_ENSURE_SUCCESS(rv, rv);
-  req.set_file_basename(NS_ConvertUTF16toUTF8(fileName).get());
-  req.mutable_signature()->CopyFrom(mSignatureInfo);
+  mRequest.set_file_basename(NS_ConvertUTF16toUTF8(fileName).get());
 
-  if (req.signature().trusted()) {
+  if (mRequest.signature().trusted()) {
     LOG(("Got signed binary for remote application reputation check "
          "[this = %p]", this));
   } else {
     LOG(("Got unsigned binary for remote application reputation check "
          "[this = %p]", this));
   }
 
   // Serialize the protocol buffer to a string. This can only fail if we are
   // out of memory, or if the protocol buffer req is missing required fields
   // (only the URL for now).
   std::string serialized;
-  if (!req.SerializeToString(&serialized)) {
+  if (!mRequest.SerializeToString(&serialized)) {
     return NS_ERROR_UNEXPECTED;
   }
 
   // Set the input stream to the serialized protocol buffer
   nsCOMPtr<nsIStringInputStream> sstream =
     do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -915,16 +970,17 @@ ApplicationReputationService::Applicatio
 ApplicationReputationService::~ApplicationReputationService() {
   LOG(("Application reputation service shutting down"));
 }
 
 NS_IMETHODIMP
 ApplicationReputationService::QueryReputation(
     nsIApplicationReputationQuery* aQuery,
     nsIApplicationReputationCallback* aCallback) {
+  LOG(("Starting application reputation check"));
   NS_ENSURE_ARG_POINTER(aQuery);
   NS_ENSURE_ARG_POINTER(aCallback);
 
   Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_COUNT, true);
   nsresult rv = QueryReputationInternal(aQuery, aCallback);
   if (NS_FAILED(rv)) {
     Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SHOULD_BLOCK,
       false);
--- a/toolkit/components/downloads/nsDownloadManager.cpp
+++ b/toolkit/components/downloads/nsDownloadManager.cpp
@@ -2639,16 +2639,23 @@ NS_IMETHODIMP nsDownload::SetSha256Hash(
 
 NS_IMETHODIMP nsDownload::SetSignatureInfo(nsIArray* aSignatureInfo) {
   MOZ_ASSERT(NS_IsMainThread(), "Must call SetSignatureInfo on main thread");
   // This will be used later to query the application reputation service.
   mSignatureInfo = aSignatureInfo;
   return NS_OK;
 }
 
+NS_IMETHODIMP nsDownload::SetRedirects(nsIArray* aRedirects) {
+  MOZ_ASSERT(NS_IsMainThread(), "Must call SetRedirects on main thread");
+  // This will be used later to query the application reputation service.
+  mRedirects = aRedirects;
+  return NS_OK;
+}
+
 #ifdef MOZ_ENABLE_GIO
 static void gio_set_metadata_done(GObject *source_obj, GAsyncResult *res, gpointer user_data)
 {
   GError *err = nullptr;
   g_file_set_attributes_finish(G_FILE(source_obj), res, nullptr, &err);
   if (err) {
 #ifdef DEBUG
     NS_DebugBreak(NS_DEBUG_WARNING, "Set file metadata failed: ", err->message, __FILE__, __LINE__);
--- a/toolkit/components/downloads/nsDownloadManager.h
+++ b/toolkit/components/downloads/nsDownloadManager.h
@@ -429,12 +429,18 @@ private:
   nsAutoCString mHash;
 
   /**
    * Stores the certificate chains in an nsIArray of nsIX509CertList of
    * nsIX509Cert, if this binary is signed.
    */
   nsCOMPtr<nsIArray> mSignatureInfo;
 
+  /**
+   * Stores the redirects that led to this download in an nsIArray of
+   * nsIPrincipal.
+   */
+  nsCOMPtr<nsIArray> mRedirects;
+
   friend class nsDownloadManager;
 };
 
 #endif
--- a/toolkit/components/downloads/nsDownloadProxy.h
+++ b/toolkit/components/downloads/nsDownloadProxy.h
@@ -155,16 +155,22 @@ public:
   }
 
   NS_IMETHODIMP SetSignatureInfo(nsIArray* aSignatureInfo)
   {
     NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
     return mInner->SetSignatureInfo(aSignatureInfo);
   }
 
+  NS_IMETHODIMP SetRedirects(nsIArray* aRedirects)
+  {
+    NS_ENSURE_TRUE(mInner, NS_ERROR_NOT_INITIALIZED);
+    return mInner->SetRedirects(aRedirects);
+  }
+
 private:
   nsCOMPtr<nsIDownload> mInner;
 };
 
 NS_IMPL_ISUPPORTS(nsDownloadProxy, nsITransfer,
                   nsIWebProgressListener, nsIWebProgressListener2)
 
 #endif
--- a/toolkit/components/downloads/nsIApplicationReputation.idl
+++ b/toolkit/components/downloads/nsIApplicationReputation.idl
@@ -76,16 +76,21 @@ interface nsIApplicationReputationQuery 
    */
   readonly attribute ACString sha256Hash;
 
   /*
    * The nsIArray of nsIX509CertList of nsIX509Cert that verify for this
    * binary, if it is signed.
    */
   readonly attribute nsIArray signatureInfo;
+
+  /*
+   * The nsIArray of nsIPrincipal of redirects that lead to this download.
+   */
+  readonly attribute nsIArray redirects;
 };
 
 [scriptable, function, uuid(9a228470-cfe5-11e2-8b8b-0800200c9a66)]
 interface nsIApplicationReputationCallback : nsISupports {
   /**
    * Callback for the result of the application reputation query.
    * @param aStatus
    *        NS_OK if and only if the query succeeded. If it did, then
--- a/toolkit/components/downloads/nsIDownload.idl
+++ b/toolkit/components/downloads/nsIDownload.idl
@@ -19,17 +19,17 @@ interface nsIMIMEInfo;
  *       Completed states are the following:  
  *       nsIDownloadManager::DOWNLOAD_FINISHED  
  *       nsIDownloadManager::DOWNLOAD_FAILED  
  *       nsIDownloadManager::DOWNLOAD_CANCELED 
  *       nsIDownloadManager::DOWNLOAD_BLOCKED_PARENTAL 
  *       nsIDownloadManager::DOWNLOAD_DIRTY 
  *       nsIDownloadManager::DOWNLOAD_BLOCKED_POLICY 
  */
-[scriptable, uuid(b02be33b-d47c-4bd3-afd9-402a942426b0)]
+[scriptable, uuid(2258f465-656e-4566-87cb-f791dbaf0322)]
 interface nsIDownload : nsITransfer {
     
     /**
      * The target of a download is always a file on the local file system.
      */
     readonly attribute nsIFile targetFile;
 
     /**
--- a/toolkit/components/downloads/test/unit/test_app_rep.js
+++ b/toolkit/components/downloads/test/unit/test_app_rep.js
@@ -11,16 +11,20 @@ const gAppRep = Cc["@mozilla.org/downloa
                   getService(Ci.nsIApplicationReputationService);
 let gHttpServ = null;
 let gTables = {};
 
 let ALLOW_LIST = 0;
 let BLOCK_LIST = 1;
 let NO_LIST = 2;
 
+let whitelistedURI = createURI("http://whitelisted.com");
+let exampleURI = createURI("http://example.com");
+let blocklistedURI = createURI("http://blocklisted.com");
+
 function readFileToString(aFilename) {
   let f = do_get_file(aFilename);
   let stream = Cc["@mozilla.org/network/file-input-stream;1"]
                  .createInstance(Ci.nsIFileInputStream);
   stream.init(f, -1, 0, 0);
   let buf = NetUtil.readInputStreamToString(stream, stream.available());
   return buf;
 }
@@ -221,70 +225,98 @@ add_test(function test_local_list() {
 
 add_test(function test_unlisted() {
   Services.prefs.setCharPref("browser.safebrowsing.appRepURL",
                              "http://localhost:4444/download");
   let counts = get_telemetry_counts();
   let listCounts = counts.listCounts;
   listCounts[NO_LIST]++;
   gAppRep.queryReputation({
-    sourceURI: createURI("http://example.com"),
+    sourceURI: exampleURI,
     fileSize: 12,
   }, function onComplete(aShouldBlock, aStatus) {
     do_check_eq(Cr.NS_OK, aStatus);
     do_check_false(aShouldBlock);
     check_telemetry(counts.total + 1, counts.shouldBlock, listCounts);
     run_next_test();
   });
 });
 
 add_test(function test_local_blacklist() {
   Services.prefs.setCharPref("browser.safebrowsing.appRepURL",
                              "http://localhost:4444/download");
   let counts = get_telemetry_counts();
   let listCounts = counts.listCounts;
   listCounts[BLOCK_LIST]++;
   gAppRep.queryReputation({
-    sourceURI: createURI("http://blocklisted.com"),
+    sourceURI: blocklistedURI,
     fileSize: 12,
   }, function onComplete(aShouldBlock, aStatus) {
     do_check_eq(Cr.NS_OK, aStatus);
     do_check_true(aShouldBlock);
     check_telemetry(counts.total + 1, counts.shouldBlock + 1, listCounts);
     run_next_test();
   });
 });
 
 add_test(function test_referer_blacklist() {
   Services.prefs.setCharPref("browser.safebrowsing.appRepURL",
                              "http://localhost:4444/download");
   let counts = get_telemetry_counts();
   let listCounts = counts.listCounts;
   listCounts[BLOCK_LIST]++;
   gAppRep.queryReputation({
-    sourceURI: createURI("http://example.com"),
-    referrerURI: createURI("http://blocklisted.com"),
+    sourceURI: exampleURI,
+    referrerURI: blocklistedURI,
     fileSize: 12,
   }, function onComplete(aShouldBlock, aStatus) {
     do_check_eq(Cr.NS_OK, aStatus);
     do_check_true(aShouldBlock);
     check_telemetry(counts.total + 1, counts.shouldBlock + 1, listCounts);
     run_next_test();
   });
 });
 
 add_test(function test_blocklist_trumps_allowlist() {
   Services.prefs.setCharPref("browser.safebrowsing.appRepURL",
                              "http://localhost:4444/download");
   let counts = get_telemetry_counts();
   let listCounts = counts.listCounts;
   listCounts[BLOCK_LIST]++;
   gAppRep.queryReputation({
-    sourceURI: createURI("http://whitelisted.com"),
-    referrerURI: createURI("http://blocklisted.com"),
+    sourceURI: whitelistedURI,
+    referrerURI: blocklistedURI,
     fileSize: 12,
   }, function onComplete(aShouldBlock, aStatus) {
     do_check_eq(Cr.NS_OK, aStatus);
     do_check_true(aShouldBlock);
     check_telemetry(counts.total + 1, counts.shouldBlock + 1, listCounts);
     run_next_test();
   });
 });
+
+add_test(function test_redirect_on_blocklist() {
+  Services.prefs.setCharPref("browser.safebrowsing.appRepURL",
+                             "http://localhost:4444/download");
+  let counts = get_telemetry_counts();
+  let listCounts = counts.listCounts;
+  listCounts[BLOCK_LIST]++;
+  let secman = Services.scriptSecurityManager;
+  let badRedirects = Cc["@mozilla.org/array;1"]
+                       .createInstance(Ci.nsIMutableArray);
+  badRedirects.appendElement(secman.getNoAppCodebasePrincipal(whitelistedURI),
+                             false);
+  badRedirects.appendElement(secman.getNoAppCodebasePrincipal(exampleURI),
+                             false);
+  badRedirects.appendElement(secman.getNoAppCodebasePrincipal(blocklistedURI),
+                             false);
+  gAppRep.queryReputation({
+    sourceURI: whitelistedURI,
+    referrerURI: exampleURI,
+    redirects: badRedirects,
+    fileSize: 12,
+  }, function onComplete(aShouldBlock, aStatus) {
+    do_check_eq(Cr.NS_OK, aStatus);
+    do_check_true(aShouldBlock);
+    check_telemetry(counts.total + 1, counts.shouldBlock + 1, listCounts);
+    run_next_test();
+  });
+});
--- a/toolkit/components/downloads/test/unit/test_app_rep_windows.js
+++ b/toolkit/components/downloads/test/unit/test_app_rep_windows.js
@@ -211,20 +211,20 @@ add_task(function test_setup()
       request.bodyInputStream,
       request.bodyInputStream.available());
     do_print("Request length: " + buf.length);
     // A garbage response. By default this produces NS_CANNOT_CONVERT_DATA as
     // the callback status.
     let blob = "this is not a serialized protocol buffer";
     // We can't actually parse the protocol buffer here, so just switch on the
     // length instead of inspecting the contents.
-    if (buf.length == 45) {
+    if (buf.length == 65) {
       // evil.com
       blob = createVerdict(true);
-    } else if (buf.length == 48) {
+    } else if (buf.length == 71) {
       // mozilla.com
       blob = createVerdict(false);
     }
     response.bodyOutputStream.write(blob, blob.length);
   });
 
   gHttpServer.start(4444);
 });
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -1478,16 +1478,21 @@ this.DownloadCopySaver.prototype = {
   /**
    * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert
    * if the file is signed. This is empty if the file is unsigned, and null
    * unless BackgroundFileSaver has successfully completed saving the file.
    */
   _signatureInfo: null,
 
   /**
+   * Save the redirects chain as an nsIArray of nsIPrincipal.
+   */
+  _redirects: null,
+
+  /**
    * True if the associated download has already been added to browsing history.
    */
   alreadyAddedToHistory: false,
 
   /**
    * String corresponding to the entityID property of the nsIResumableChannel
    * used to execute the download, or null if the channel was not resumable or
    * the saver was instructed not to keep partially downloaded data.
@@ -1554,16 +1559,17 @@ this.DownloadCopySaver.prototype = {
           backgroundFileSaver.observer = {
             onTargetChange: function () { },
             onSaveComplete: (aSaver, aStatus) => {
               // Send notifications now that we can restart if needed.
               if (Components.isSuccessCode(aStatus)) {
                 // Save the hash before freeing backgroundFileSaver.
                 this._sha256Hash = aSaver.sha256Hash;
                 this._signatureInfo = aSaver.signatureInfo;
+                this._redirects = aSaver.redirects;
                 deferSaveComplete.resolve();
               } else {
                 // Infer the origin of the error from the failure code, because
                 // BackgroundFileSaver does not provide more specific data.
                 let properties = { result: aStatus, inferCause: true };
                 deferSaveComplete.reject(new DownloadError(properties));
               }
               // Free the reference cycle, to release resources earlier.
@@ -1812,16 +1818,24 @@ this.DownloadCopySaver.prototype = {
   },
 
   /*
    * Implements DownloadSaver.getSignatureInfo.
    */
   getSignatureInfo: function ()
   {
     return this._signatureInfo;
+  },
+
+  /*
+   * Implements DownloadSaver.getRedirects.
+   */
+  getRedirects: function ()
+  {
+    return this._redirects;
   }
 };
 
 /**
  * Creates a new DownloadCopySaver object, with its initial state derived from
  * its serializable representation.
  *
  * @param aSerializable
@@ -1868,16 +1882,21 @@ this.DownloadLegacySaver.prototype = {
   /**
    * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert
    * if the file is signed. This is empty if the file is unsigned, and null
    * unless BackgroundFileSaver has successfully completed saving the file.
    */
   _signatureInfo: null,
 
   /**
+   * Save the redirect chain as an nsIArray of nsIPrincipal.
+   */
+  _redirects: null,
+
+  /**
    * nsIRequest object associated to the status and progress updates we
    * received.  This object is null before we receive the first status and
    * progress update, and is also reset to null when the download is stopped.
    */
   request: null,
 
   /**
    * This deferred object contains a promise that is resolved as soon as this
@@ -2162,16 +2181,36 @@ this.DownloadLegacySaver.prototype = {
 
   /**
    * Called by the nsITransfer implementation when the hash is available.
    */
   setSignatureInfo: function (signatureInfo)
   {
     this._signatureInfo = signatureInfo;
   },
+
+  /**
+   * Implements "DownloadSaver.getRedirects".
+   */
+  getRedirects: function ()
+  {
+    if (this.copySaver) {
+      return this.copySaver.getRedirects();
+    }
+    return this._redirects;
+  },
+
+  /**
+   * Called by the nsITransfer implementation when the redirect chain is
+   * available.
+   */
+  setRedirects: function (redirects)
+  {
+    this._redirects = redirects;
+  },
 };
 
 /**
  * Returns a new DownloadLegacySaver object.  This saver type has a
  * deserializable form only when creating a new object in memory, because it
  * cannot be serialized to disk.
  */
 this.DownloadLegacySaver.fromSerializable = function () {
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -495,37 +495,40 @@ this.DownloadIntegration = {
    * @resolves The boolean indicates to block downloads or not.
    */
   shouldBlockForReputationCheck: function (aDownload) {
     if (this.dontCheckApplicationReputation) {
       return Promise.resolve(this.shouldBlockInTestForApplicationReputation);
     }
     let hash;
     let sigInfo;
+    let channelRedirects;
     try {
       hash = aDownload.saver.getSha256Hash();
       sigInfo = aDownload.saver.getSignatureInfo();
+      channelRedirects = aDownload.saver.getRedirects();
     } catch (ex) {
-      // Bail if DownloadSaver doesn't have a hash.
+      // Bail if DownloadSaver doesn't have a hash or signature info.
       return Promise.resolve(false);
     }
     if (!hash || !sigInfo) {
       return Promise.resolve(false);
     }
     let deferred = Promise.defer();
     let aReferrer = null;
     if (aDownload.source.referrer) {
       aReferrer: NetUtil.newURI(aDownload.source.referrer);
     }
     gApplicationReputationService.queryReputation({
       sourceURI: NetUtil.newURI(aDownload.source.url),
       referrerURI: aReferrer,
       fileSize: aDownload.currentBytes,
       sha256Hash: hash,
-      signatureInfo: sigInfo },
+      signatureInfo: sigInfo,
+      redirects: channelRedirects },
       function onComplete(aShouldBlock, aRv) {
         deferred.resolve(aShouldBlock);
       });
     return deferred.promise;
   },
 
 #ifdef XP_WIN
   /**
--- a/toolkit/components/jsdownloads/src/DownloadLegacy.js
+++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js
@@ -134,16 +134,17 @@ DownloadLegacyTransfer.prototype = {
       // The last file has been received, or the download failed.  Wait for the
       // associated Download object to be available before notifying.
       this._deferDownload.promise.then(download => {
         // At this point, the hash has been set and we need to copy it to the
         // DownloadSaver.
         if (Components.isSuccessCode(aStatus)) {
           download.saver.setSha256Hash(this._sha256Hash);
           download.saver.setSignatureInfo(this._signatureInfo);
+          download.saver.setRedirects(this._redirects);
         }
         download.saver.onTransferFinished(aRequest, aStatus);
       }).then(null, Cu.reportError);
 
       // Release the reference to the component executing the download.
       this._cancelable = null;
     }
   },
@@ -257,16 +258,21 @@ DownloadLegacyTransfer.prototype = {
     this._sha256Hash = hash;
   },
 
   setSignatureInfo: function (signatureInfo)
   {
     this._signatureInfo = signatureInfo;
   },
 
+  setRedirects: function (redirects)
+  {
+    this._redirects = redirects;
+  },
+
   //////////////////////////////////////////////////////////////////////////////
   //// Private methods and properties
 
   /**
    * This deferred object contains a promise that is resolved with the Download
    * object associated with this nsITransfer instance, when it is available.
    */
   _deferDownload: null,
--- a/uriloader/base/nsITransfer.idl
+++ b/uriloader/base/nsITransfer.idl
@@ -6,17 +6,17 @@
 #include "nsIWebProgressListener2.idl"
 
 interface nsIArray;
 interface nsIURI;
 interface nsICancelable;
 interface nsIMIMEInfo;
 interface nsIFile;
 
-[scriptable, uuid(0c81b265-34b8-472d-be98-099b2512e3ec)]
+[scriptable, uuid(37ec75d3-97ad-4da8-afaa-eabe5b4afd73)]
 interface nsITransfer : nsIWebProgressListener2 {
 
     /**
      * Initializes the transfer with certain properties.  This function must
      * be called prior to accessing any properties on this interface.
      *
      * @param aSource The source URI of the transfer. Must not be null.
      *
@@ -71,16 +71,24 @@ interface nsITransfer : nsIWebProgressLi
      * Used to notify the transfer object of the signature of the downloaded
      * file.  Must be called on the main thread, only after the download has
      * finished successfully.
      * @param aSignatureInfo The nsIArray of nsIX509CertList of nsIX509Cert
      *        certificates of the downloaded file.
      */
     void setSignatureInfo(in nsIArray aSignatureInfo);
 
+    /*
+     * Used to notify the transfer object of the redirects associated with the
+     * channel that terminated in the downloaded file.  Must be called on the
+     * main thread, only after the download has finished successfully.
+     * @param aRedirects The nsIArray of nsIPrincipal of redirected URIs
+     *        associated with the downloaded file.
+     */
+    void setRedirects(in nsIArray aRedirects);
 };
 
 %{C++
 /**
  * A component with this contract ID will be created each time a download is
  * started, and nsITransfer::Init will be called on it and an observer will be set.
  *
  * Notifications of the download progress will happen via
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -20,16 +20,17 @@
 
 #include "nsExternalHelperAppService.h"
 #include "nsCExternalHandlerService.h"
 #include "nsIURI.h"
 #include "nsIURL.h"
 #include "nsIFile.h"
 #include "nsIFileURL.h"
 #include "nsIChannel.h"
+#include "nsIRedirectHistory.h"
 #include "nsIDirectoryService.h"
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsICategoryManager.h"
 #include "nsDependentSubstring.h"
 #include "nsXPIDLString.h"
 #include "nsUnicharUtils.h"
 #include "nsIStringEnumerator.h"
 #include "nsMemory.h"
@@ -1950,23 +1951,35 @@ NS_IMETHODIMP
 nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver *aSaver,
                                      nsresult aStatus)
 {
   LOG(("nsExternalAppHandler::OnSaveComplete\n"
        "  aSaver=0x%p, aStatus=0x%08X, mCanceled=%d, mTransfer=0x%p\n",
        aSaver, aStatus, mCanceled, mTransfer.get()));
 
   if (!mCanceled) {
-    // Save the hash
+    // Save the hash and signature information
     (void)mSaver->GetSha256Hash(mHash);
     (void)mSaver->GetSignatureInfo(getter_AddRefs(mSignatureInfo));
+
     // Free the reference that the saver keeps on us, even if we couldn't get
     // the hash.
     mSaver = nullptr;
-  
+
+    // Save the redirect information.
+    nsCOMPtr<nsIRedirectHistory> history = do_QueryInterface(mRequest);
+    if (history) {
+      (void)history->GetRedirects(getter_AddRefs(mRedirects));
+      uint32_t length = 0;
+      mRedirects->GetLength(&length);
+      LOG(("nsExternalAppHandler: Got %u redirects\n", length));
+    } else {
+      LOG(("nsExternalAppHandler: No redirects\n"));
+    }
+
     if (NS_FAILED(aStatus)) {
       nsAutoString path;
       mTempFile->GetPath(path);
 
       // It may happen when e10s is enabled that there will be no transfer
       // object available to communicate status as expected by the system.
       // Let's try and create a temporary transfer object to take care of this
       // for us, we'll fall back to using the prompt service if we absolutely
@@ -1999,16 +2012,17 @@ void nsExternalAppHandler::NotifyTransfe
   MOZ_ASSERT(NS_IsMainThread(), "Must notify on main thread");
   MOZ_ASSERT(mTransfer, "We must have an nsITransfer");
 
   LOG(("Notifying progress listener"));
 
   if (NS_SUCCEEDED(aStatus)) {
     (void)mTransfer->SetSha256Hash(mHash);
     (void)mTransfer->SetSignatureInfo(mSignatureInfo);
+    (void)mTransfer->SetRedirects(mRedirects);
     (void)mTransfer->OnProgressChange64(nullptr, nullptr, mProgress,
       mContentLength, mProgress, mContentLength);
   }
 
   (void)mTransfer->OnStateChange(nullptr, nullptr,
     nsIWebProgressListener::STATE_STOP |
     nsIWebProgressListener::STATE_IS_REQUEST |
     nsIWebProgressListener::STATE_IS_NETWORK, aStatus);
--- a/uriloader/exthandler/nsExternalHelperAppService.h
+++ b/uriloader/exthandler/nsExternalHelperAppService.h
@@ -324,16 +324,20 @@ protected:
   nsAutoCString mHash;
   /**
    * Stores the signature information of the downloaded file in an nsIArray of
    * nsIX509CertList of nsIX509Cert. If the file is unsigned this will be
    * empty.
    */
   nsCOMPtr<nsIArray> mSignatureInfo;
   /**
+   * Stores the redirect information associated with the channel.
+   */
+  nsCOMPtr<nsIArray> mRedirects;
+  /**
    * Creates the temporary file for the download and an output stream for it.
    * Upon successful return, both mTempFile and mSaver will be valid.
    */
   nsresult SetUpTempFile(nsIChannel * aChannel);
   /**
    * When we download a helper app, we are going to retarget all load
    * notifications into our own docloader and load group instead of
    * using the window which initiated the load....RetargetLoadNotifications