summaryrefslogtreecommitdiffstats
path: root/toolkit/components/reputationservice/ApplicationReputation.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/reputationservice/ApplicationReputation.cpp')
-rw-r--r--toolkit/components/reputationservice/ApplicationReputation.cpp1987
1 files changed, 1987 insertions, 0 deletions
diff --git a/toolkit/components/reputationservice/ApplicationReputation.cpp b/toolkit/components/reputationservice/ApplicationReputation.cpp
new file mode 100644
index 0000000000..cc4045d5ad
--- /dev/null
+++ b/toolkit/components/reputationservice/ApplicationReputation.cpp
@@ -0,0 +1,1987 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=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/. */
+// See
+// https://wiki.mozilla.org/Security/Features/Application_Reputation_Design_Doc
+// for a description of Chrome's implementation of this feature.
+#include "ApplicationReputation.h"
+#include "chrome/common/safe_browsing/csd.pb.h"
+
+#include "nsIArray.h"
+#include "nsIApplicationReputation.h"
+#include "nsIChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIIOService.h"
+#include "nsIObserverService.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIStreamListener.h"
+#include "nsIStringStream.h"
+#include "nsITimer.h"
+#include "nsIUploadChannel2.h"
+#include "nsIURI.h"
+#include "nsIURL.h"
+#include "nsIUrlClassifierDBService.h"
+#include "nsIURLFormatter.h"
+#include "nsIX509Cert.h"
+#include "nsIX509CertDB.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Components.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/LoadContext.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/intl/LocaleService.h"
+
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsLocalFileCommon.h"
+#include "nsNetCID.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+
+#include "nsIContentPolicy.h"
+#include "nsICryptoHash.h"
+#include "nsILoadInfo.h"
+#include "nsContentUtils.h"
+#include "nsWeakReference.h"
+#include "nsIRedirectHistoryEntry.h"
+
+#include "ApplicationReputationTelemetryUtils.h"
+
+using mozilla::ArrayLength;
+using mozilla::BasePrincipal;
+using mozilla::OriginAttributes;
+using mozilla::Preferences;
+using mozilla::TimeStamp;
+using mozilla::intl::LocaleService;
+using mozilla::Telemetry::Accumulate;
+using mozilla::Telemetry::AccumulateCategorical;
+using safe_browsing::ClientDownloadRequest;
+using safe_browsing::ClientDownloadRequest_CertificateChain;
+using safe_browsing::ClientDownloadRequest_Resource;
+using safe_browsing::ClientDownloadRequest_SignatureInfo;
+
+// Preferences that we need to initialize the query.
+#define PREF_SB_APP_REP_URL "browser.safebrowsing.downloads.remote.url"
+#define PREF_SB_MALWARE_ENABLED "browser.safebrowsing.malware.enabled"
+#define PREF_SB_DOWNLOADS_ENABLED "browser.safebrowsing.downloads.enabled"
+#define PREF_SB_DOWNLOADS_REMOTE_ENABLED \
+ "browser.safebrowsing.downloads.remote.enabled"
+#define PREF_SB_DOWNLOADS_REMOTE_TIMEOUT \
+ "browser.safebrowsing.downloads.remote.timeout_ms"
+#define PREF_DOWNLOAD_BLOCK_TABLE "urlclassifier.downloadBlockTable"
+#define PREF_DOWNLOAD_ALLOW_TABLE "urlclassifier.downloadAllowTable"
+
+// Preferences that are needed to action the verdict.
+#define PREF_BLOCK_DANGEROUS \
+ "browser.safebrowsing.downloads.remote.block_dangerous"
+#define PREF_BLOCK_DANGEROUS_HOST \
+ "browser.safebrowsing.downloads.remote.block_dangerous_host"
+#define PREF_BLOCK_POTENTIALLY_UNWANTED \
+ "browser.safebrowsing.downloads.remote.block_potentially_unwanted"
+#define PREF_BLOCK_UNCOMMON \
+ "browser.safebrowsing.downloads.remote.block_uncommon"
+
+// MOZ_LOG=ApplicationReputation:5
+mozilla::LazyLogModule ApplicationReputationService::prlog(
+ "ApplicationReputation");
+#define LOG(args) \
+ MOZ_LOG(ApplicationReputationService::prlog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(ApplicationReputationService::prlog, mozilla::LogLevel::Debug)
+
+/**
+ * Our detection of executable/binary files uses 3 lists:
+ * - kNonBinaryExecutables (below)
+ * - kBinaryFileExtensions (below)
+ * - sExecutableExts (in nsLocalFileCommon)
+ *
+ * On Windows, the `sExecutableExts` list is used to determine whether files
+ * count as executable. For executable files, we will not offer an "open with"
+ * option when downloading, only "save as".
+ *
+ * On all platforms, the combination of these lists is used to determine
+ * whether files should be subject to application reputation checks.
+ * Specifically, all files with extensions that:
+ * - are in kBinaryFileExtensions, or
+ * - are in sExecutableExts **and not in kNonBinaryExecutables**
+ *
+ * will be subject to checks.
+ *
+ * There are tests that verify that these lists are sorted and that extensions
+ * never appear in both the sExecutableExts and kBinaryFileExtensions lists.
+ *
+ * When adding items to any lists:
+ * - please prefer adding to sExecutableExts unless it is imperative users can
+ * (potentially automatically!) open such files with a helper application
+ * without first saving them (and that outweighs any associated risk).
+ * - if adding executable items that shouldn't be submitted to apprep servers,
+ * add them to sExecutableExts and also to kNonBinaryExecutables.
+ * - always add an associated comment in the kBinaryFileExtensions list. Add
+ * a commented-out entry with an `exec` annotation if you add the actual
+ * entry in sExecutableExts.
+ *
+ * When removing items please consider whether items should still be in the
+ * sExecutableExts list even if removing them from the kBinaryFileExtensions
+ * list, and vice versa.
+ *
+ * Note that there is a GTest that does its best to check some of these
+ * invariants that you'll likely need to update if you're modifying these
+ * lists.
+ */
+
+// Items that are in sExecutableExts but shouldn't be submitted for application
+// reputation checks.
+/* static */
+const char* const ApplicationReputationService::kNonBinaryExecutables[] = {
+ // clang-format off
+ ".ad",
+ ".afploc",
+ ".air",
+ ".atloc",
+ ".ftploc",
+ // clang-format on
+};
+
+// Items that should be submitted for application reputation checks that users
+// are able to open immediately (without first saving and then finding the
+// file). If users shouldn't be able to open them immediately, add to
+// sExecutableExts instead (see also the docstring comment above!).
+/* static */
+const char* const ApplicationReputationService::kBinaryFileExtensions[] = {
+ // Originally extracted from the "File Type Policies" Chrome extension
+ // Items listed with an `exec` comment are in the sExecutableExts list in
+ // nsLocalFileCommon.h .
+ //".001",
+ //".7z",
+ //".ace",
+ //".accda", exec // MS Access database
+ //".accdb", exec // MS Access database
+ //".accde", exec // MS Access database
+ //".accdr", exec // MS Access database
+ ".action", // Mac script
+ //".ad", exec // Windows
+ //".ade", exec // MS Access
+ //".adp", exec // MS Access
+ //".air", exec // Adobe AIR installer; excluded from apprep checks.
+ ".apk", // Android package
+ //".app", exec // Executable application
+ ".applescript",
+ //".application", exec // MS ClickOnce
+ //".appref-ms", exec // MS ClickOnce
+ //".appx", exec
+ //".appxbundle", exec
+ //".arc",
+ //".arj",
+ ".as", // Mac archive
+ //".asp", exec // Windows Server script
+ ".asx", // Windows Media Player
+ //".b64",
+ //".balz",
+ //".bas", exec // Basic script
+ ".bash", // Linux shell
+ //".bat", exec // Windows shell
+ //".bhx",
+ ".bin",
+ ".btapp", // uTorrent and Transmission
+ ".btinstall", // uTorrent and Transmission
+ ".btkey", // uTorrent and Transmission
+ ".btsearch", // uTorrent and Transmission
+ ".btskin", // uTorrent and Transmission
+ ".bz", // Linux archive (bzip)
+ ".bz2", // Linux archive (bzip2)
+ ".bzip2", // Linux archive (bzip2)
+ ".cab", // Windows archive
+ ".caction", // Automator action
+ ".cdr", // Mac disk image
+ //".cer", exec // Signed certificate file
+ ".cfg", // Windows
+ ".chi", // Windows Help
+ //".chm", exec // Windows Help
+ ".class", // Java
+ //".cmd", exec // Windows executable
+ //".com", exec // Windows executable
+ ".command", // Mac script
+ ".configprofile", // Configuration file for Apple systems
+ ".cpgz", // Mac archive
+ ".cpi", // Control Panel Item. Executable used for adding icons
+ // to Control Panel
+ //".cpio",
+ //".cpl", exec // Windows executable
+ //".crt", exec // Windows signed certificate
+ ".crx", // Chrome extensions
+ ".csh", // Linux shell
+ //".csv",
+ ".dart", // Mac disk image
+ ".dc42", // Apple DiskCopy Image
+ ".deb", // Linux package
+ ".definition", // Automator action
+ ".desktop", // A shortcut that runs other files
+ //".der", exec // Signed certificate
+ ".dex", // Android
+ ".dht", // HTML
+ ".dhtm", // HTML
+ ".dhtml", // HTML
+ //".diagcab", exec // Executable windows archive, like .cab
+ ".diskcopy42", // Apple DiskCopy Image
+ ".dll", // Windows executable
+ ".dmg", // Mac disk image
+ ".dmgpart", // Mac disk image
+ ".doc", // MS Office
+ ".docb", // MS Office
+ ".docm", // MS Word
+ ".docx", // MS Word
+ ".dot", // MS Word
+ ".dotm", // MS Word
+ ".dott", // MS Office
+ ".dotx", // MS Word
+ ".drv", // Windows driver
+ ".dvdr", // Mac Disk image
+ ".dylib", // Mach object dynamic library file
+ ".efi", // Firmware
+ ".eml", // MS Outlook
+ //".exe", exec // Windows executable
+ //".fat",
+ //".fileloc", exec // Apple finder internet location data file
+ ".fon", // Windows font
+ //".fxp", exec // MS FoxPro
+ ".gadget", // Windows
+ //".gif",
+ ".grp", // Windows
+ ".gz", // Linux archive (gzip)
+ ".gzip", // Linux archive (gzip)
+ ".hfs", // Mac disk image
+ //".hlp", exec // Windows Help
+ ".hqx", // Mac archive
+ //".hta", exec // HTML trusted application
+ ".htm", ".html",
+ ".htt", // MS HTML template
+ //".ica",
+ ".img", // Mac disk image
+ ".imgpart", // Mac disk image
+ //".inf", exec // Windows installer
+ //".inetloc", exec // Apple finder internet location data file
+ ".ini", // Generic config file
+ //".ins", exec // IIS config
+ ".internetconnect", // Configuration file for Apple system
+ //".inx", // InstallShield
+ ".iso", // CD image
+ //".isp", exec // IIS config
+ //".isu", // InstallShield
+ //".jar", exec // Java
+#ifndef MOZ_ESR
+//".jnlp", exec // Java
+#endif
+ //".job", // Windows
+ //".jpg",
+ //".jpeg",
+ //".js", exec // JavaScript script
+ //".jse", exec // JScript
+ ".ksh", // Linux shell
+ //".lha",
+ //".lnk", exec // Windows
+ ".local", // Windows
+ //".lpaq1",
+ //".lpaq5",
+ //".lpaq8",
+ //".lzh",
+ //".lzma",
+ //".mad", exec // MS Access
+ //".maf", exec // MS Access
+ //".mag", exec // MS Access
+ //".mam", exec // MS Access
+ ".manifest", // Windows
+ //".maq", exec // MS Access
+ //".mar", exec // MS Access
+ //".mas", exec // MS Access
+ //".mat", exec // MS Access
+ //".mau", exec // Media attachment
+ //".mav", exec // MS Access
+ //".maw", exec // MS Access
+ //".mda", exec // MS Access
+ //".mdb", exec // MS Access
+ //".mde", exec // MS Access
+ //".mdt", exec // MS Access
+ //".mdw", exec // MS Access
+ //".mdz", exec // MS Access
+ ".mht", // MS HTML
+ ".mhtml", // MS HTML
+ ".mim", // MS Mail
+ //".mkv",
+ ".mmc", // MS Office
+ ".mobileconfig", // Configuration file for Apple systems
+ ".mof", // Windows
+ //".mov",
+ //".mp3",
+ //".mp4",
+ ".mpkg", // Mac installer
+ //".msc", exec // Windows executable
+ ".msg", // MS Outlook
+ //".msh", exec // Windows shell
+ //".msh1", exec // Windows shell
+ //".msh1xml", exec // Windows shell
+ //".msh2", exec // Windows shell
+ //".msh2xml", exec // Windows shell
+ //".mshxml", exec // Windows
+ //".msi", exec // Windows installer
+ //".msix", exec // Windows installer
+ //".msixbundle", exec // Windows installer
+ //".msp", exec // Windows installer
+ //".mst", exec // Windows installer
+ ".ndif", // Mac disk image
+ ".networkconnect", // Configuration file for Apple systems
+ //".ntfs", // 7z
+ ".ocx", // ActiveX
+ //".ops", exec // MS Office
+ ".osas", // AppleScript
+ ".osax", // AppleScript
+ //".out", // Linux binary
+ ".oxt", // OpenOffice extension, can execute arbitrary code
+ //".package",
+ //".paf", // PortableApps package
+ //".paq8f",
+ //".paq8jd",
+ //".paq8l",
+ //".paq8o",
+ ".partial", // Downloads
+ ".pax", // Mac archive
+ //".pcd", exec // Microsoft Visual Test
+ ".pdf", // Adobe Acrobat
+ //".pea",
+ ".pet", // Linux package
+ //".pif", exec // Windows
+ ".pkg", // Mac installer
+ ".pl", // Perl script
+ //".plg", exec // MS Visual Studio
+ //".png",
+ ".pot", // MS PowerPoint
+ ".potm", // MS PowerPoint
+ ".potx", // MS PowerPoint
+ ".ppam", // MS PowerPoint
+ ".pps", // MS PowerPoint
+ ".ppsm", // MS PowerPoint
+ ".ppsx", // MS PowerPoint
+ ".ppt", // MS PowerPoint
+ ".pptm", // MS PowerPoint
+ ".pptx", // MS PowerPoint
+ //".prf", exec // MS Outlook
+ //".prg", exec // Windows
+ ".ps1", // Windows shell
+ ".ps1xml", // Windows shell
+ ".ps2", // Windows shell
+ ".ps2xml", // Windows shell
+ ".psc1", // Windows shell
+ ".psc2", // Windows shell
+ //".pst", exec // MS Outlook
+ ".pup", // Linux package
+ ".py", // Python script
+ ".pyc", // Python binary
+ ".pyd", // Equivalent of a DLL, for python libraries
+ ".pyo", // Compiled python code
+ ".pyw", // Python GUI
+ //".quad",
+ //".r00",
+ //".r01",
+ //".r02",
+ //".r03",
+ //".r04",
+ //".r05",
+ //".r06",
+ //".r07",
+ //".r08",
+ //".r09",
+ //".r10",
+ //".r11",
+ //".r12",
+ //".r13",
+ //".r14",
+ //".r15",
+ //".r16",
+ //".r17",
+ //".r18",
+ //".r19",
+ //".r20",
+ //".r21",
+ //".r22",
+ //".r23",
+ //".r24",
+ //".r25",
+ //".r26",
+ //".r27",
+ //".r28",
+ //".r29",
+ //".rar",
+ ".rb", // Ruby script
+ //".reg", exec // Windows Registry
+ ".rels", // MS Office
+ //".rgs", // Windows Registry
+ ".rpm", // Linux package
+ ".rtf", // MS Office
+ //".run", // Linux shell
+ //".scf", exec // Windows shell
+ ".scpt", // AppleScript
+ ".scptd", // AppleScript
+ //".scr", exec // Windows
+ //".sct", exec // Windows shell
+ ".search-ms", // Windows
+ ".seplugin", // AppleScript
+ ".service", // Systemd service unit file
+ //".settingcontent-ms", exec // Windows settings
+ ".sh", // Linux shell
+ ".shar", // Linux shell
+ //".shb", exec // Windows
+ //".shs", exec // Windows shell
+ ".sht", // HTML
+ ".shtm", // HTML
+ ".shtml", // HTML
+ ".sldm", // MS PowerPoint
+ ".sldx", // MS PowerPoint
+ ".slk", // MS Excel
+ ".slp", // Linux package
+ ".smi", // Mac disk image
+ ".sparsebundle", // Mac disk image
+ ".sparseimage", // Mac disk image
+ ".spl", // Adobe Flash
+ //".squashfs",
+ ".svg",
+ ".swf", // Adobe Flash
+ ".swm", // Windows Imaging
+ ".sys", // Windows
+ ".tar", // Linux archive
+ ".taz", // Linux archive (bzip2)
+ ".tbz", // Linux archive (bzip2)
+ ".tbz2", // Linux archive (bzip2)
+ ".tcsh", // Linux shell
+ //".tif",
+ ".tgz", // Linux archive (gzip)
+ //".toast", // Roxio disk image
+ ".torrent", // Bittorrent
+ ".tpz", // Linux archive (gzip)
+ //".txt",
+ ".txz", // Linux archive (xz)
+ ".tz", // Linux archive (gzip)
+ //".u3p", // U3 Smart Apps
+ ".udf", // MS Excel
+ ".udif", // Mac disk image
+ //".url", exec // Windows
+ //".uu",
+ //".uue",
+ //".vb", exec // Visual Basic script
+ //".vbe", exec // Visual Basic script
+ //".vbs", exec // Visual Basic script
+ //".vbscript", // Visual Basic script
+ //".vdx", exec // MS Visio
+ ".vhd", // Windows virtual hard drive
+ ".vhdx", // Windows virtual hard drive
+ ".vmdk", // VMware virtual disk
+ //".vsd", exec // MS Visio
+ //".vsdm", exec // MS Visio
+ //".vsdx", exec // MS Visio
+ //".vsmacros", exec // MS Visual Studio
+ //".vss", exec // MS Visio
+ //".vssm", exec // MS Visio
+ //".vssx", exec // MS Visio
+ //".vst", exec // MS Visio
+ //".vstm", exec // MS Visio
+ //".vstx", exec // MS Visio
+ //".vsw", exec // MS Visio
+ //".vsx", exec // MS Visio
+ //".vtx", exec // MS Visio
+ //".wav",
+ //".webloc", // MacOS website location file
+ //".webp",
+ ".website", // Windows
+ ".wflow", // Automator action
+ ".wim", // Windows Imaging
+ ".workflow", // Mac Automator
+ //".wrc", // FreeArc archive
+ //".ws", exec // Windows script
+ //".wsc", exec // Windows script
+ //".wsf", exec // Windows script
+ //".wsh", exec // Windows script
+ ".xar", // MS Excel
+ ".xbap", // XAML Browser Application
+ ".xht", ".xhtm", ".xhtml",
+ ".xip", // Mac archive
+ ".xla", // MS Excel
+ ".xlam", // MS Excel
+ ".xldm", // MS Excel
+ //".xll", exec // MS Excel
+ ".xlm", // MS Excel
+ ".xls", // MS Excel
+ ".xlsb", // MS Excel
+ ".xlsm", // MS Excel
+ ".xlsx", // MS Excel
+ ".xlt", // MS Excel
+ ".xltm", // MS Excel
+ ".xltx", // MS Excel
+ ".xlw", // MS Excel
+ ".xml", // MS Excel
+ ".xnk", // MS Exchange
+ ".xrm-ms", // Windows
+ ".xsd", // XML schema definition
+ ".xsl", // XML Stylesheet
+ //".xxe",
+ ".xz", // Linux archive (xz)
+ ".z", // InstallShield
+#ifdef XP_WIN // disable on Mac/Linux, see 1167493
+ ".zip", // Generic archive
+#endif
+ ".zipx", // WinZip
+ //".zpaq",
+};
+
+static const char* const kMozNonBinaryExecutables[] = {
+ ".001", ".7z", ".ace", ".arc", ".arj", ".b64", ".balz",
+ ".bhx", ".cpio", ".fat", ".lha", ".lpaq1", ".lpaq5", ".lpaq8",
+ ".lzh", ".lzma", ".ntfs", ".paq8f", ".paq8jd", ".paq8l", ".paq8o",
+ ".pea", ".quad", ".r00", ".r01", ".r02", ".r03", ".r04",
+ ".r05", ".r06", ".r07", ".r08", ".r09", ".r10", ".r11",
+ ".r12", ".r13", ".r14", ".r15", ".r16", ".r17", ".r18",
+ ".r19", ".r20", ".r21", ".r22", ".r23", ".r24", ".r25",
+ ".r26", ".r27", ".r28", ".r29", ".rar", ".squashfs", ".uu",
+ ".uue", ".wrc", ".xxe", ".zpaq", ".toast",
+};
+
+static const char* const kSafeFileExtensions[] = {
+ ".jpg", ".jpeg", ".mp3", ".mp4", ".png", ".csv", ".ica",
+ ".gif", ".txt", ".package", ".tif", ".webp", ".mkv", ".wav",
+ ".mov", ".paf", ".vbscript", ".ad", ".inx", ".isu", ".job",
+ ".rgs", ".u3p", ".out", ".run", ".bmp", ".css", ".ehtml",
+ ".flac", ".ico", ".jfif", ".m4a", ".m4v", ".mpeg", ".mpg",
+ ".oga", ".ogg", ".ogm", ".ogv", ".opus", ".pjp", ".pjpeg",
+ ".svgz", ".text", ".tiff", ".weba", ".webm", ".xbm",
+};
+
+enum class LookupType { AllowlistOnly, BlocklistOnly, BothLists };
+
+// Define the reasons that download protection service accepts or blocks this
+// download. This is now used for telemetry purposes and xpcshell test. Please
+// also update the xpcshell-test if a reason is added.
+//
+// LocalWhitelist : URL is found in the local whitelist
+// LocalBlocklist : URL is found in the local blocklist
+// NonBinary : The downloaded non-binary file is not found in the
+// local blocklist VerdictSafe : Remote lookup reports the download is
+// safe VerdictUnknown : Remote lookup reports unknown, we treat this as a
+// safe download VerdictDangerous : Remote lookup reports the download is
+// dangerous VerdictDangerousHost : Remote lookup reports the download is from a
+// dangerous host VerdictUnwanted : Remote lookup reports the download is
+// potentially unwatned VerdictUncommon : Remote lookup reports the
+// download is uncommon VerdictUnrecognized : The verdict type from remote
+// lookup is not defined in the csd.proto DangerousPrefOff : The download is
+// dangerous, but the corresponding preference is off DangerousHostPrefOff : The
+// download is from a dangerous host, but the corresponding preference is off
+// UnwantedPrefOff : The download is potentially unwanted, but the
+// corresponding preference is off UncommonPrefOff : The download us
+// uncommon, but the coressponding preference is off NetworkError :
+// There is an error while requesting remote lookup RemoteLookupDisabled :
+// Remote lookup is disabled or the remote lookup URL is empty InternalError :
+// An unexpected internal error DPDisabled : Download protection is
+// disabled
+using Reason = mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_REASON;
+
+class PendingDBLookup;
+
+// A single use class private to ApplicationReputationService encapsulating an
+// nsIApplicationReputationQuery and an nsIApplicationReputationCallback. Once
+// created by ApplicationReputationService, it is guaranteed to call mCallback.
+// This class is private to ApplicationReputationService.
+class PendingLookup final : public nsIStreamListener,
+ public nsITimerCallback,
+ public nsINamed,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+ NS_DECL_NSIOBSERVER
+
+ // Constructor and destructor.
+ PendingLookup(nsIApplicationReputationQuery* aQuery,
+ nsIApplicationReputationCallback* aCallback);
+
+ // Start the lookup. The lookup may have 2 parts: local and remote. In the
+ // local lookup, PendingDBLookups are created to query the local allow and
+ // blocklists for various URIs associated with this downloaded file. In the
+ // event that no results are found, a remote lookup is sent to the Application
+ // Reputation server.
+ nsresult StartLookup();
+
+ private:
+ ~PendingLookup();
+
+ friend class PendingDBLookup;
+
+ // Telemetry states.
+ // Status of the remote response (valid or not).
+ enum SERVER_RESPONSE_TYPES {
+ SERVER_RESPONSE_VALID = 0,
+ SERVER_RESPONSE_FAILED = 1,
+ SERVER_RESPONSE_INVALID = 2,
+ };
+
+ // The target filename for the downloaded file.
+ nsCString mFileName;
+
+ // True if extension of this file matches any extension in the
+ // kBinaryFileExtensions or sExecutableExts list.
+ bool mIsBinaryFile;
+
+ // Number of blocklist and allowlist hits we have seen.
+ uint32_t mBlocklistCount;
+ uint32_t mAllowlistCount;
+
+ // The query containing metadata about the downloaded file.
+ nsCOMPtr<nsIApplicationReputationQuery> mQuery;
+
+ // The callback with which to report the verdict.
+ nsCOMPtr<nsIApplicationReputationCallback> mCallback;
+
+ // An array of strings created from certificate information used to whitelist
+ // the downloaded file.
+ nsTArray<nsCString> mAllowlistSpecs;
+ // The source URI of the download (i.e. final URI after any redirects).
+ nsTArray<nsCString> mAnylistSpecs;
+ // The referrer and possibly any redirects.
+ nsTArray<nsCString> mBlocklistSpecs;
+
+ // When we started this query
+ TimeStamp mStartTime;
+
+ // The channel used to talk to the remote lookup server
+ nsCOMPtr<nsIChannel> mChannel;
+
+ // Timer to abort this lookup if it takes too long
+ nsCOMPtr<nsITimer> mTimeoutTimer;
+
+ // 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;
+
+ // The clock records the start time of a remote lookup request, used by
+ // telemetry.
+ PRIntervalTime mTelemetryRemoteRequestStartMs;
+
+ // Returns the type of download binary for the file.
+ ClientDownloadRequest::DownloadType GetDownloadType(
+ const nsACString& aFilename);
+
+ // Clean up and call the callback. PendingLookup must not be used after this
+ // function is called.
+ nsresult OnComplete(uint32_t aVerdict, Reason aReason, nsresult aRv);
+
+ // Wrapper function for nsIStreamListener.onStopRequest to make it easy to
+ // guarantee calling the callback
+ nsresult OnStopRequestInternal(nsIRequest* aRequest, nsresult aResult,
+ uint32_t& aVerdict, Reason& aReason);
+
+ // Return the hex-encoded hash of the whole URI.
+ nsresult GetSpecHash(nsACString& aSpec, nsACString& hexEncodedHash);
+
+ // Strip url parameters, fragments, and user@pass fields from the URI spec
+ // using nsIURL. Hash data URIs and return blob URIs unfiltered.
+ nsresult GetStrippedSpec(nsIURI* aUri, nsACString& spec);
+
+ // Escape '/' and '%' in certificate attribute values.
+ nsCString EscapeCertificateAttribute(const nsACString& aAttribute);
+
+ // Escape ':' in fingerprint values.
+ nsCString EscapeFingerprint(const nsACString& aAttribute);
+
+ // Generate whitelist strings for the given certificate pair from the same
+ // certificate chain.
+ nsresult GenerateWhitelistStringsForPair(nsIX509Cert* certificate,
+ nsIX509Cert* issuer);
+
+ // Generate whitelist strings for the given certificate chain, which starts
+ // with the signer and may go all the way to the root cert.
+ 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();
+
+ // Parse the XPCOM certificate lists and stick them into the protocol buffer
+ // version.
+ nsresult ParseCertificates(
+ const nsTArray<nsTArray<nsTArray<uint8_t>>>& aSigArray);
+
+ // Adds the redirects to mBlocklistSpecs 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
+ // from which the file was downloaded.
+ nsresult LookupNext();
+
+ // Sends a query to the remote application reputation service. Returns NS_OK
+ // on success.
+ nsresult SendRemoteQuery();
+
+ // Helper function to ensure that we always call the callback.
+ nsresult SendRemoteQueryInternal(Reason& aReason);
+};
+
+// A single-use class for looking up a single URI in the safebrowsing DB. This
+// class is private to PendingLookup.
+class PendingDBLookup final : public nsIUrlClassifierCallback {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIERCALLBACK
+
+ // Constructor and destructor
+ explicit PendingDBLookup(PendingLookup* aPendingLookup);
+
+ // Look up the given URI in the safebrowsing DBs, optionally on both the allow
+ // list and the blocklist. If there is a match, call
+ // PendingLookup::OnComplete. Otherwise, call PendingLookup::LookupNext.
+ nsresult LookupSpec(const nsACString& aSpec, const LookupType& aLookupType);
+
+ private:
+ ~PendingDBLookup();
+
+ // The download appeared on the allowlist, blocklist, or no list (and thus
+ // could trigger a remote query.
+ enum LIST_TYPES {
+ ALLOW_LIST = 0,
+ BLOCK_LIST = 1,
+ NO_LIST = 2,
+ };
+
+ nsCString mSpec;
+ LookupType mLookupType;
+ RefPtr<PendingLookup> mPendingLookup;
+ nsresult LookupSpecInternal(const nsACString& aSpec);
+};
+
+NS_IMPL_ISUPPORTS(PendingDBLookup, nsIUrlClassifierCallback)
+
+PendingDBLookup::PendingDBLookup(PendingLookup* aPendingLookup)
+ : mLookupType(LookupType::BothLists), mPendingLookup(aPendingLookup) {
+ LOG(("Created pending DB lookup [this = %p]", this));
+}
+
+PendingDBLookup::~PendingDBLookup() {
+ LOG(("Destroying pending DB lookup [this = %p]", this));
+ mPendingLookup = nullptr;
+}
+
+nsresult PendingDBLookup::LookupSpec(const nsACString& aSpec,
+ const LookupType& aLookupType) {
+ LOG(("Checking principal %s [this=%p]", aSpec.Data(), this));
+ mSpec = aSpec;
+ mLookupType = aLookupType;
+ nsresult rv = LookupSpecInternal(aSpec);
+ if (NS_FAILED(rv)) {
+ nsAutoCString errorName;
+ mozilla::GetErrorName(rv, errorName);
+ LOG(("Error in LookupSpecInternal() [rv = %s, this = %p]", errorName.get(),
+ this));
+ return mPendingLookup->LookupNext(); // ignore this lookup and move to next
+ }
+ // LookupSpecInternal has called nsIUrlClassifierCallback.lookup, which is
+ // guaranteed to call HandleEvent.
+ return rv;
+}
+
+nsresult PendingDBLookup::LookupSpecInternal(const nsACString& aSpec) {
+ nsresult rv;
+
+ nsCOMPtr<nsIURI> uri;
+ nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
+ rv = ios->NewURI(aSpec, nullptr, nullptr, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ OriginAttributes attrs;
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(uri, attrs);
+ if (!principal) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check local lists to see if the URI has already been whitelisted or
+ // blacklisted.
+ LOG(("Checking DB service for principal %s [this = %p]", mSpec.get(), this));
+ nsCOMPtr<nsIUrlClassifierDBService> dbService =
+ mozilla::components::UrlClassifierDB::Service(&rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString tables;
+ nsAutoCString allowlist;
+ Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE, allowlist);
+ if ((mLookupType != LookupType::BlocklistOnly) && !allowlist.IsEmpty()) {
+ tables.Append(allowlist);
+ }
+ nsAutoCString blocklist;
+ Preferences::GetCString(PREF_DOWNLOAD_BLOCK_TABLE, blocklist);
+ if ((mLookupType != LookupType::AllowlistOnly) && !blocklist.IsEmpty()) {
+ if (!tables.IsEmpty()) {
+ tables.Append(',');
+ }
+ tables.Append(blocklist);
+ }
+ return dbService->Lookup(principal, tables, this);
+}
+
+NS_IMETHODIMP
+PendingDBLookup::HandleEvent(const nsACString& tables) {
+ // HandleEvent is guaranteed to call either:
+ // 1) PendingLookup::OnComplete if the URL matches the blocklist, or
+ // 2) PendingLookup::LookupNext if the URL does not match the blocklist.
+ // Blocklisting trumps allowlisting.
+ nsAutoCString blockList;
+ Preferences::GetCString(PREF_DOWNLOAD_BLOCK_TABLE, blockList);
+ if ((mLookupType != LookupType::AllowlistOnly) &&
+ FindInReadable(blockList, tables)) {
+ mPendingLookup->mBlocklistCount++;
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_LOCAL, BLOCK_LIST);
+ LOG(("Found principal %s on blocklist [this = %p]", mSpec.get(), this));
+ return mPendingLookup->OnComplete(
+ nsIApplicationReputationService::VERDICT_DANGEROUS,
+ Reason::LocalBlocklist, NS_OK);
+ }
+
+ nsAutoCString allowList;
+ Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE, allowList);
+ if ((mLookupType != LookupType::BlocklistOnly) &&
+ FindInReadable(allowList, tables)) {
+ mPendingLookup->mAllowlistCount++;
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_LOCAL, ALLOW_LIST);
+ LOG(("Found principal %s on allowlist [this = %p]", mSpec.get(), this));
+ // Don't call onComplete, since blocklisting trumps allowlisting
+ return mPendingLookup->LookupNext();
+ }
+
+ LOG(("Didn't find principal %s on any list [this = %p]", mSpec.get(), this));
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_LOCAL, NO_LIST);
+ return mPendingLookup->LookupNext();
+}
+
+NS_IMPL_ISUPPORTS(PendingLookup, nsIStreamListener, nsIRequestObserver,
+ nsIObserver, nsISupportsWeakReference, nsITimerCallback,
+ nsINamed)
+
+PendingLookup::PendingLookup(nsIApplicationReputationQuery* aQuery,
+ nsIApplicationReputationCallback* aCallback)
+ : mIsBinaryFile(false),
+ mBlocklistCount(0),
+ mAllowlistCount(0),
+ mQuery(aQuery),
+ mCallback(aCallback) {
+ LOG(("Created pending lookup [this = %p]", this));
+}
+
+PendingLookup::~PendingLookup() {
+ LOG(("Destroying pending lookup [this = %p]", this));
+}
+
+static const char* const kDmgFileExtensions[] = {
+ ".cdr", ".dart", ".dc42", ".diskcopy42",
+ ".dmg", ".dmgpart", ".dvdr", ".img",
+ ".imgpart", ".iso", ".ndif", ".smi",
+ ".sparsebundle", ".sparseimage", ".toast", ".udif",
+};
+
+static const char* const kRarFileExtensions[] = {
+ ".r00", ".r01", ".r02", ".r03", ".r04", ".r05", ".r06", ".r07",
+ ".r08", ".r09", ".r10", ".r11", ".r12", ".r13", ".r14", ".r15",
+ ".r16", ".r17", ".r18", ".r19", ".r20", ".r21", ".r22", ".r23",
+ ".r24", ".r25", ".r26", ".r27", ".r28", ".r29", ".rar",
+};
+
+static const char* const kZipFileExtensions[] = {
+ ".zip", // Generic archive
+ ".zipx", // WinZip
+};
+
+static const char* GetFileExt(const nsACString& aFilename,
+ const char* const aFileExtensions[],
+ const size_t aLength) {
+ for (size_t i = 0; i < aLength; ++i) {
+ if (StringEndsWith(aFilename, nsDependentCString(aFileExtensions[i]))) {
+ return aFileExtensions[i];
+ }
+ }
+ return nullptr;
+}
+
+static const char* GetFileExt(const nsACString& aFilename) {
+#define _GetFileExt(_f, _l) GetFileExt(_f, _l, ArrayLength(_l))
+ const char* ext = _GetFileExt(
+ aFilename, ApplicationReputationService::kBinaryFileExtensions);
+ if (ext == nullptr &&
+ !_GetFileExt(aFilename,
+ ApplicationReputationService::kNonBinaryExecutables)) {
+ ext = _GetFileExt(aFilename, sExecutableExts);
+ }
+ return ext;
+}
+
+// Returns true if the file extension matches one in the given array.
+static bool IsFileType(const nsACString& aFilename,
+ const char* const aFileExtensions[],
+ const size_t aLength) {
+ return GetFileExt(aFilename, aFileExtensions, aLength) != nullptr;
+}
+
+static bool IsBinary(const nsACString& aFilename) {
+ return IsFileType(aFilename,
+ ApplicationReputationService::kBinaryFileExtensions,
+ ArrayLength(
+ ApplicationReputationService::kBinaryFileExtensions)) ||
+ (!IsFileType(
+ aFilename, ApplicationReputationService::kNonBinaryExecutables,
+ ArrayLength(
+ ApplicationReputationService::kNonBinaryExecutables)) &&
+ IsFileType(aFilename, sExecutableExts, ArrayLength(sExecutableExts)));
+}
+
+ClientDownloadRequest::DownloadType PendingLookup::GetDownloadType(
+ const nsACString& aFilename) {
+ MOZ_ASSERT(IsBinary(aFilename));
+
+ // From
+ // https://cs.chromium.org/chromium/src/chrome/common/safe_browsing/download_protection_util.cc?l=17
+ if (StringEndsWith(aFilename, ".zip"_ns)) {
+ return ClientDownloadRequest::ZIPPED_EXECUTABLE;
+ } else if (StringEndsWith(aFilename, ".apk"_ns)) {
+ return ClientDownloadRequest::ANDROID_APK;
+ } else if (StringEndsWith(aFilename, ".app"_ns) ||
+ StringEndsWith(aFilename, ".applescript"_ns) ||
+ StringEndsWith(aFilename, ".cdr"_ns) ||
+ StringEndsWith(aFilename, ".dart"_ns) ||
+ StringEndsWith(aFilename, ".dc42"_ns) ||
+ StringEndsWith(aFilename, ".diskcopy42"_ns) ||
+ StringEndsWith(aFilename, ".dmg"_ns) ||
+ StringEndsWith(aFilename, ".dmgpart"_ns) ||
+ StringEndsWith(aFilename, ".dvdr"_ns) ||
+ StringEndsWith(aFilename, ".img"_ns) ||
+ StringEndsWith(aFilename, ".imgpart"_ns) ||
+ StringEndsWith(aFilename, ".iso"_ns) ||
+ StringEndsWith(aFilename, ".mpkg"_ns) ||
+ StringEndsWith(aFilename, ".ndif"_ns) ||
+ StringEndsWith(aFilename, ".osas"_ns) ||
+ StringEndsWith(aFilename, ".osax"_ns) ||
+ StringEndsWith(aFilename, ".pkg"_ns) ||
+ StringEndsWith(aFilename, ".scpt"_ns) ||
+ StringEndsWith(aFilename, ".scptd"_ns) ||
+ StringEndsWith(aFilename, ".seplugin"_ns) ||
+ StringEndsWith(aFilename, ".smi"_ns) ||
+ StringEndsWith(aFilename, ".sparsebundle"_ns) ||
+ StringEndsWith(aFilename, ".sparseimage"_ns) ||
+ StringEndsWith(aFilename, ".toast"_ns) ||
+ StringEndsWith(aFilename, ".udif"_ns)) {
+ return ClientDownloadRequest::MAC_EXECUTABLE;
+ }
+
+ return ClientDownloadRequest::WIN_EXECUTABLE; // default to Windows binaries
+}
+
+nsresult PendingLookup::LookupNext() {
+ // We must call LookupNext or SendRemoteQuery upon return.
+ // Look up all of the URLs that could allow or block this download.
+ // Blocklist first.
+
+ // If a url is in blocklist we should call PendingLookup::OnComplete directly.
+ MOZ_ASSERT(mBlocklistCount == 0);
+
+ nsCString spec;
+ if (!mAnylistSpecs.IsEmpty()) {
+ // Check the source URI only.
+ spec = mAnylistSpecs.PopLastElement();
+ RefPtr<PendingDBLookup> lookup(new PendingDBLookup(this));
+
+ // We don't need to check whitelist if the file is not a binary file.
+ auto type =
+ mIsBinaryFile ? LookupType::BothLists : LookupType::BlocklistOnly;
+ return lookup->LookupSpec(spec, type);
+ }
+
+ if (!mBlocklistSpecs.IsEmpty()) {
+ // Check the referrer and redirect chain.
+ spec = mBlocklistSpecs.PopLastElement();
+ RefPtr<PendingDBLookup> lookup(new PendingDBLookup(this));
+ return lookup->LookupSpec(spec, LookupType::BlocklistOnly);
+ }
+
+ // Now that we've looked up all of the URIs against the blocklist,
+ // if any of mAnylistSpecs or mAllowlistSpecs matched the allowlist,
+ // go ahead and pass.
+ if (mAllowlistCount > 0) {
+ return OnComplete(nsIApplicationReputationService::VERDICT_SAFE,
+ Reason::LocalWhitelist, NS_OK);
+ }
+
+ MOZ_ASSERT_IF(!mIsBinaryFile, mAllowlistSpecs.Length() == 0);
+
+ // Only binary signatures remain.
+ if (!mAllowlistSpecs.IsEmpty()) {
+ spec = mAllowlistSpecs.PopLastElement();
+ LOG(("PendingLookup::LookupNext: checking %s on allowlist", spec.get()));
+ RefPtr<PendingDBLookup> lookup(new PendingDBLookup(this));
+ return lookup->LookupSpec(spec, LookupType::AllowlistOnly);
+ }
+
+ if (!mFileName.IsEmpty()) {
+ if (IsBinary(mFileName)) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_BINARY_TYPE::
+ BinaryFile);
+ } else if (IsFileType(mFileName, kSafeFileExtensions,
+ ArrayLength(kSafeFileExtensions))) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_BINARY_TYPE::
+ NonBinaryFile);
+ } else if (IsFileType(mFileName, kMozNonBinaryExecutables,
+ ArrayLength(kMozNonBinaryExecutables))) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_BINARY_TYPE::
+ MozNonBinaryFile);
+ } else {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_BINARY_TYPE::
+ UnknownFile);
+ }
+ } else {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_BINARY_TYPE::
+ MissingFilename);
+ }
+
+ if (IsFileType(mFileName, kDmgFileExtensions,
+ ArrayLength(kDmgFileExtensions))) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_BINARY_ARCHIVE::
+ DmgFile);
+ } else if (IsFileType(mFileName, kRarFileExtensions,
+ ArrayLength(kRarFileExtensions))) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_BINARY_ARCHIVE::
+ RarFile);
+ } else if (IsFileType(mFileName, kZipFileExtensions,
+ ArrayLength(kZipFileExtensions))) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_BINARY_ARCHIVE::
+ ZipFile);
+ } else if (mIsBinaryFile) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_BINARY_ARCHIVE::
+ OtherBinaryFile);
+ }
+
+ // There are no more URIs to check against local list. If the file is
+ // not eligible for remote lookup, bail.
+ if (!mIsBinaryFile) {
+ LOG(("Not eligible for remote lookups [this=%p]", this));
+ return OnComplete(nsIApplicationReputationService::VERDICT_SAFE,
+ Reason::NonBinaryFile, NS_OK);
+ }
+
+ nsresult rv = SendRemoteQuery();
+ if (NS_FAILED(rv)) {
+ return OnComplete(nsIApplicationReputationService::VERDICT_SAFE,
+ Reason::InternalError, rv);
+ }
+ return NS_OK;
+}
+
+nsCString PendingLookup::EscapeCertificateAttribute(
+ const nsACString& aAttribute) {
+ // Escape '/' because it's a field separator, and '%' because Chrome does
+ nsCString escaped;
+ escaped.SetCapacity(aAttribute.Length());
+ for (unsigned int i = 0; i < aAttribute.Length(); ++i) {
+ if (aAttribute.Data()[i] == '%') {
+ escaped.AppendLiteral("%25");
+ } else if (aAttribute.Data()[i] == '/') {
+ escaped.AppendLiteral("%2F");
+ } else if (aAttribute.Data()[i] == ' ') {
+ escaped.AppendLiteral("%20");
+ } else {
+ escaped.Append(aAttribute.Data()[i]);
+ }
+ }
+ return escaped;
+}
+
+nsCString PendingLookup::EscapeFingerprint(const nsACString& aFingerprint) {
+ // Google's fingerprint doesn't have colons
+ nsCString escaped;
+ escaped.SetCapacity(aFingerprint.Length());
+ for (unsigned int i = 0; i < aFingerprint.Length(); ++i) {
+ if (aFingerprint.Data()[i] != ':') {
+ escaped.Append(aFingerprint.Data()[i]);
+ }
+ }
+ return escaped;
+}
+
+nsresult PendingLookup::GenerateWhitelistStringsForPair(
+ nsIX509Cert* certificate, nsIX509Cert* issuer) {
+ // The whitelist paths have format:
+ // http://sb-ssl.google.com/safebrowsing/csd/certificate/<issuer_cert_fingerprint>[/CN=<cn>][/O=<org>][/OU=<unit>]
+ // Any of CN, O, or OU may be omitted from the whitelist entry. Unfortunately
+ // this is not publicly documented, but the Chrome implementation can be found
+ // here:
+ // https://code.google.com/p/chromium/codesearch#search/&q=GetCertificateWhitelistStrings
+ nsCString whitelistString(
+ "http://sb-ssl.google.com/safebrowsing/csd/certificate/");
+
+ nsString fingerprint;
+ nsresult rv = issuer->GetSha1Fingerprint(fingerprint);
+ NS_ENSURE_SUCCESS(rv, rv);
+ whitelistString.Append(EscapeFingerprint(NS_ConvertUTF16toUTF8(fingerprint)));
+
+ nsString commonName;
+ rv = certificate->GetCommonName(commonName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!commonName.IsEmpty()) {
+ whitelistString.AppendLiteral("/CN=");
+ whitelistString.Append(
+ EscapeCertificateAttribute(NS_ConvertUTF16toUTF8(commonName)));
+ }
+
+ nsString organization;
+ rv = certificate->GetOrganization(organization);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!organization.IsEmpty()) {
+ whitelistString.AppendLiteral("/O=");
+ whitelistString.Append(
+ EscapeCertificateAttribute(NS_ConvertUTF16toUTF8(organization)));
+ }
+
+ nsString organizationalUnit;
+ rv = certificate->GetOrganizationalUnit(organizationalUnit);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!organizationalUnit.IsEmpty()) {
+ whitelistString.AppendLiteral("/OU=");
+ whitelistString.Append(
+ EscapeCertificateAttribute(NS_ConvertUTF16toUTF8(organizationalUnit)));
+ }
+ LOG(("Whitelisting %s", whitelistString.get()));
+
+ mAllowlistSpecs.AppendElement(whitelistString);
+ return NS_OK;
+}
+
+nsresult PendingLookup::GenerateWhitelistStringsForChain(
+ const safe_browsing::ClientDownloadRequest_CertificateChain& aChain) {
+ // We need a signing certificate and an issuer to construct a whitelist
+ // entry.
+ if (aChain.element_size() < 2) {
+ return NS_OK;
+ }
+
+ // Get the signer.
+ nsresult rv;
+ nsCOMPtr<nsIX509CertDB> certDB = do_GetService(NS_X509CERTDB_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIX509Cert> signer;
+ nsTArray<uint8_t> signerBytes;
+ signerBytes.AppendElements(aChain.element(0).certificate().data(),
+ aChain.element(0).certificate().size());
+ rv = certDB->ConstructX509(signerBytes, getter_AddRefs(signer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (int i = 1; i < aChain.element_size(); ++i) {
+ // Get the issuer.
+ nsCOMPtr<nsIX509Cert> issuer;
+ nsTArray<uint8_t> issuerBytes;
+ issuerBytes.AppendElements(aChain.element(i).certificate().data(),
+ aChain.element(i).certificate().size());
+ rv = certDB->ConstructX509(issuerBytes, getter_AddRefs(issuer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GenerateWhitelistStringsForPair(signer, issuer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult 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) {
+ 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<nsIRedirectHistoryEntry> redirectEntry =
+ do_QueryInterface(supports, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrincipal> principal;
+ rv = redirectEntry->GetPrincipal(getter_AddRefs(principal));
+ auto* basePrin = BasePrincipal::Cast(principal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri;
+ rv = basePrin->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add the spec to our list of local lookups. The most recent redirect is
+ // the last element.
+ nsCString spec;
+ rv = GetStrippedSpec(uri, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mBlocklistSpecs.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() {
+ mStartTime = TimeStamp::Now();
+ nsresult rv = DoLookupInternal();
+ if (NS_FAILED(rv)) {
+ return OnComplete(nsIApplicationReputationService::VERDICT_SAFE,
+ Reason::InternalError, NS_OK);
+ }
+ return rv;
+}
+
+nsresult PendingLookup::GetSpecHash(nsACString& aSpec,
+ nsACString& hexEncodedHash) {
+ nsCOMPtr<nsICryptoHash> cryptoHash;
+ nsresult rv =
+ NS_NewCryptoHash(nsICryptoHash::SHA256, getter_AddRefs(cryptoHash));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cryptoHash->Update(
+ reinterpret_cast<const uint8_t*>(aSpec.BeginReading()), aSpec.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString binaryHash;
+ rv = cryptoHash->Finish(false, binaryHash);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // This needs to match HexEncode() in Chrome's
+ // src/base/strings/string_number_conversions.cc
+ static const char* const hex = "0123456789ABCDEF";
+ hexEncodedHash.SetCapacity(2 * binaryHash.Length());
+ for (size_t i = 0; i < binaryHash.Length(); ++i) {
+ auto c = static_cast<unsigned char>(binaryHash[i]);
+ hexEncodedHash.Append(hex[(c >> 4) & 0x0F]);
+ hexEncodedHash.Append(hex[c & 0x0F]);
+ }
+
+ return NS_OK;
+}
+
+nsresult PendingLookup::GetStrippedSpec(nsIURI* aUri, nsACString& escaped) {
+ if (NS_WARN_IF(!aUri)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv;
+ rv = aUri->GetScheme(escaped);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (escaped.EqualsLiteral("blob")) {
+ aUri->GetSpec(escaped);
+ LOG(
+ ("PendingLookup::GetStrippedSpec(): blob URL left unstripped as '%s' "
+ "[this = %p]",
+ PromiseFlatCString(escaped).get(), this));
+ return NS_OK;
+ }
+
+ if (escaped.EqualsLiteral("data")) {
+ // Replace URI with "data:<everything before comma>,SHA256(<whole URI>)"
+ aUri->GetSpec(escaped);
+ int32_t comma = escaped.FindChar(',');
+ if (comma > -1 &&
+ static_cast<nsCString::size_type>(comma) < escaped.Length() - 1) {
+ MOZ_ASSERT(comma > 4, "Data URIs start with 'data:'");
+ nsAutoCString hexEncodedHash;
+ rv = GetSpecHash(escaped, hexEncodedHash);
+ if (NS_SUCCEEDED(rv)) {
+ escaped.Truncate(comma + 1);
+ escaped.Append(hexEncodedHash);
+ }
+ }
+
+ LOG(
+ ("PendingLookup::GetStrippedSpec(): data URL stripped to '%s' [this = "
+ "%p]",
+ PromiseFlatCString(escaped).get(), this));
+ return NS_OK;
+ }
+
+ // If aURI is not an nsIURL, we do not want to check the lists or send a
+ // remote query.
+ nsCOMPtr<nsIURL> url = do_QueryInterface(aUri, &rv);
+ if (NS_FAILED(rv)) {
+ LOG(
+ ("PendingLookup::GetStrippedSpec(): scheme '%s' is not supported [this "
+ "= %p]",
+ PromiseFlatCString(escaped).get(), this));
+ return rv;
+ }
+
+ nsCString temp;
+ rv = url->GetHostPort(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ escaped.AppendLiteral("://");
+ escaped.Append(temp);
+
+ rv = url->GetFilePath(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // nsIUrl.filePath starts with '/'
+ escaped.Append(temp);
+
+ LOG(("PendingLookup::GetStrippedSpec(): URL stripped to '%s' [this = %p]",
+ PromiseFlatCString(escaped).get(), this));
+ return NS_OK;
+}
+
+nsresult PendingLookup::DoLookupInternal() {
+ // 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 sourceSpec;
+ rv = GetStrippedSpec(uri, sourceSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mAnylistSpecs.AppendElement(sourceSpec);
+
+ ClientDownloadRequest_Resource* resource = mRequest.add_resources();
+ resource->set_url(sourceSpec.get());
+ resource->set_type(ClientDownloadRequest::DOWNLOAD_URL);
+
+ nsCOMPtr<nsIReferrerInfo> referrerInfo;
+ mozilla::Unused << mQuery->GetReferrerInfo(getter_AddRefs(referrerInfo));
+ nsCOMPtr<nsIURI> referrer;
+ // It is quite possible that referrer header is omitted due to security reason
+ // (for example navigation from https-> http). Hence we should use the
+ // original referrer which has not applied referrer policy yet, to make sure
+ // we don't mistakenly allow unsafe download.
+ if (referrerInfo) {
+ referrer = referrerInfo->GetOriginalReferrer();
+ }
+
+ if (referrer) {
+ nsCString referrerSpec;
+ rv = GetStrippedSpec(referrer, referrerSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mBlocklistSpecs.AppendElement(referrerSpec);
+ resource->set_referrer(referrerSpec.get());
+ }
+
+ nsCOMPtr<nsIArray> redirects;
+ rv = mQuery->GetRedirects(getter_AddRefs(redirects));
+ if (redirects) {
+ AddRedirects(redirects);
+ } else {
+ LOG(("ApplicationReputation: Got no redirects [this=%p]", this));
+ }
+
+ rv = mQuery->GetSuggestedFileName(mFileName);
+ if (NS_SUCCEEDED(rv) && !mFileName.IsEmpty()) {
+ mIsBinaryFile = IsBinary(mFileName);
+ LOG(("Suggested filename: %s [binary = %d, this = %p]", mFileName.get(),
+ mIsBinaryFile, this));
+ } else {
+ nsAutoCString errorName;
+ mozilla::GetErrorName(rv, errorName);
+ LOG(("No suggested filename [rv = %s, this = %p]", errorName.get(), this));
+ mFileName.Truncate();
+ }
+
+ // We can skip parsing certificate for non-binary files because we only
+ // check local block list for them.
+ if (mIsBinaryFile) {
+ nsTArray<nsTArray<nsTArray<uint8_t>>> sigArray;
+ rv = mQuery->GetSignatureInfo(sigArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!sigArray.IsEmpty()) {
+ rv = ParseCertificates(sigArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = GenerateWhitelistStrings();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Start the call chain.
+ return LookupNext();
+}
+
+nsresult PendingLookup::OnComplete(uint32_t aVerdict, Reason aReason,
+ nsresult aRv) {
+ if (NS_FAILED(aRv)) {
+ nsAutoCString errorName;
+ mozilla::GetErrorName(aRv, errorName);
+ LOG(
+ ("Failed sending remote query for application reputation "
+ "[rv = %s, this = %p]",
+ errorName.get(), this));
+ }
+
+ if (mTimeoutTimer) {
+ mTimeoutTimer->Cancel();
+ mTimeoutTimer = nullptr;
+ }
+
+ bool shouldBlock = true;
+ switch (aVerdict) {
+ case nsIApplicationReputationService::VERDICT_DANGEROUS:
+ if (!Preferences::GetBool(PREF_BLOCK_DANGEROUS, true)) {
+ shouldBlock = false;
+ aReason = Reason::DangerousPrefOff;
+ }
+ break;
+ case nsIApplicationReputationService::VERDICT_UNCOMMON:
+ if (!Preferences::GetBool(PREF_BLOCK_UNCOMMON, true)) {
+ shouldBlock = false;
+ aReason = Reason::UncommonPrefOff;
+ }
+ break;
+ case nsIApplicationReputationService::VERDICT_POTENTIALLY_UNWANTED:
+ if (!Preferences::GetBool(PREF_BLOCK_POTENTIALLY_UNWANTED, true)) {
+ shouldBlock = false;
+ aReason = Reason::UnwantedPrefOff;
+ }
+ break;
+ case nsIApplicationReputationService::VERDICT_DANGEROUS_HOST:
+ if (!Preferences::GetBool(PREF_BLOCK_DANGEROUS_HOST, true)) {
+ shouldBlock = false;
+ aReason = Reason::DangerousHostPrefOff;
+ }
+ break;
+ default:
+ shouldBlock = false;
+ break;
+ }
+
+ AccumulateCategorical(aReason);
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SHOULD_BLOCK,
+ shouldBlock);
+
+ double t = (TimeStamp::Now() - mStartTime).ToMilliseconds();
+ LOG(("Application Reputation verdict is %u, obtained in %f ms [this = %p]",
+ aVerdict, t, this));
+ if (shouldBlock) {
+ LOG(("Application Reputation check failed, blocking bad binary [this = %p]",
+ this));
+ } else {
+ LOG(("Application Reputation check passed [this = %p]", this));
+ }
+
+ nsresult res = mCallback->OnComplete(shouldBlock, aRv, aVerdict);
+ return res;
+}
+
+nsresult PendingLookup::ParseCertificates(
+ const nsTArray<nsTArray<nsTArray<uint8_t>>>& 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)
+
+ // Each chain may have multiple certificates.
+ for (const auto& certList : aSigArray) {
+ safe_browsing::ClientDownloadRequest_CertificateChain* certChain =
+ mRequest.mutable_signature()->add_certificate_chain();
+ for (const auto& cert : certList) {
+ // Add this certificate to the protobuf to send remotely.
+ certChain->add_element()->set_certificate(cert.Elements(), cert.Length());
+ }
+ }
+ if (mRequest.signature().certificate_chain_size() > 0) {
+ mRequest.mutable_signature()->set_trusted(true);
+ }
+ return NS_OK;
+}
+
+nsresult PendingLookup::SendRemoteQuery() {
+ MOZ_ASSERT(!IsFileType(
+ mFileName, ApplicationReputationService::kNonBinaryExecutables,
+ ArrayLength(ApplicationReputationService::kNonBinaryExecutables)));
+ Reason reason = Reason::NotSet;
+ nsresult rv = SendRemoteQueryInternal(reason);
+ if (NS_FAILED(rv)) {
+ return OnComplete(nsIApplicationReputationService::VERDICT_SAFE, reason,
+ rv);
+ }
+ // SendRemoteQueryInternal has fired off the query and we call OnComplete in
+ // the nsIStreamListener.onStopRequest.
+ return rv;
+}
+
+nsresult PendingLookup::SendRemoteQueryInternal(Reason& aReason) {
+ auto scopeExit = mozilla::MakeScopeExit([&aReason]() {
+ if (aReason == Reason::NotSet) {
+ aReason = Reason::InternalError;
+ }
+ });
+
+ // If we aren't supposed to do remote lookups, bail.
+ if (!Preferences::GetBool(PREF_SB_DOWNLOADS_REMOTE_ENABLED, false)) {
+ LOG(("Remote lookups are disabled [this = %p]", this));
+ aReason = Reason::RemoteLookupDisabled;
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ // If the remote lookup URL is empty or absent, bail.
+ nsString serviceUrl;
+ nsCOMPtr<nsIURLFormatter> formatter(
+ do_GetService("@mozilla.org/toolkit/URLFormatterService;1"));
+ if (!formatter ||
+ NS_FAILED(formatter->FormatURLPref(
+ NS_ConvertASCIItoUTF16(PREF_SB_APP_REP_URL), serviceUrl)) ||
+ serviceUrl.IsEmpty() || u"about:blank"_ns.Equals(serviceUrl)) {
+ LOG(("Remote lookup URL is empty or absent [this = %p]", this));
+ aReason = Reason::RemoteLookupDisabled;
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ 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.
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv;
+ rv = mQuery->GetSourceURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString spec;
+ rv = GetStrippedSpec(uri, spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mRequest.set_url(spec.get());
+
+ uint32_t fileSize;
+ rv = mQuery->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mRequest.set_length(fileSize);
+ // We have no way of knowing whether or not a user initiated the
+ // download. Set it to true to lessen the chance of false positives.
+ mRequest.set_user_initiated(true);
+
+ nsCString locale;
+ rv = LocaleService::GetInstance()->GetAppLocaleAsBCP47(locale);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mRequest.set_locale(locale.get());
+ nsCString sha256Hash;
+ rv = mQuery->GetSha256Hash(sha256Hash);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mRequest.mutable_digests()->set_sha256(
+ std::string(sha256Hash.Data(), sha256Hash.Length()));
+ mRequest.set_file_basename(mFileName.get());
+ mRequest.set_download_type(GetDownloadType(mFileName));
+
+ 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 (!mRequest.SerializeToString(&serialized)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (LOG_ENABLED()) {
+ nsAutoCString serializedStr(serialized.c_str(), serialized.length());
+ serializedStr.ReplaceSubstring("\0"_ns, "\\0"_ns);
+
+ LOG(("Serialized protocol buffer [this = %p]: (length=%zd) %s", this,
+ serializedStr.Length(), serializedStr.get()));
+ }
+
+ // 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);
+
+ rv = sstream->SetData(serialized.c_str(), serialized.length());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set up the channel to transmit the request to the service.
+ nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
+ rv = ios->NewChannel(NS_ConvertUTF16toUTF8(serviceUrl), nullptr, nullptr,
+ nullptr, // aLoadingNode
+ nsContentUtils::GetSystemPrincipal(),
+ nullptr, // aTriggeringPrincipal
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER, getter_AddRefs(mChannel));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mChannel->SetLoadFlags(nsIChannel::LOAD_BYPASS_URL_CLASSIFIER);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo();
+ mozilla::OriginAttributes attrs;
+ attrs.mFirstPartyDomain.AssignLiteral(NECKO_SAFEBROWSING_FIRST_PARTY_DOMAIN);
+ loadInfo->SetOriginAttributes(attrs);
+
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mozilla::Unused << httpChannel;
+
+ // Upload the protobuf to the application reputation service.
+ nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = uploadChannel->ExplicitSetUploadStream(
+ sstream, "application/octet-stream"_ns, serialized.size(), "POST"_ns,
+ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t timeoutMs =
+ Preferences::GetUint(PREF_SB_DOWNLOADS_REMOTE_TIMEOUT, 10000);
+ NS_NewTimerWithCallback(getter_AddRefs(mTimeoutTimer), this, timeoutMs,
+ nsITimer::TYPE_ONE_SHOT);
+
+ mTelemetryRemoteRequestStartMs = PR_IntervalNow();
+
+ rv = mChannel->AsyncOpen(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PendingLookup::Notify(nsITimer* aTimer) {
+ LOG(("Remote lookup timed out [this = %p]", this));
+ MOZ_ASSERT(aTimer == mTimeoutTimer);
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT,
+ true);
+ mChannel->Cancel(NS_ERROR_NET_TIMEOUT_EXTERNAL);
+ mTimeoutTimer->Cancel();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PendingLookup::GetName(nsACString& aName) {
+ aName.AssignLiteral("PendingLookup");
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// nsIObserver implementation
+NS_IMETHODIMP
+PendingLookup::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, "quit-application")) {
+ if (mTimeoutTimer) {
+ mTimeoutTimer->Cancel();
+ mTimeoutTimer = nullptr;
+ }
+ if (mChannel) {
+ mChannel->Cancel(NS_ERROR_ABORT);
+ }
+ }
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIStreamListener
+static nsresult AppendSegmentToString(nsIInputStream* inputStream,
+ void* closure, const char* rawSegment,
+ uint32_t toOffset, uint32_t count,
+ uint32_t* writeCount) {
+ nsAutoCString* decodedData = static_cast<nsAutoCString*>(closure);
+ decodedData->Append(rawSegment, count);
+ *writeCount = count;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PendingLookup::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
+ uint64_t offset, uint32_t count) {
+ uint32_t read;
+ return aStream->ReadSegments(AppendSegmentToString, &mResponse, count, &read);
+}
+
+NS_IMETHODIMP
+PendingLookup::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }
+
+NS_IMETHODIMP
+PendingLookup::OnStopRequest(nsIRequest* aRequest, nsresult aResult) {
+ NS_ENSURE_STATE(mCallback);
+
+ if (aResult != NS_ERROR_NET_TIMEOUT_EXTERNAL) {
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT,
+ false);
+
+ MOZ_ASSERT(mTelemetryRemoteRequestStartMs > 0);
+ int32_t msecs = PR_IntervalToMilliseconds(PR_IntervalNow() -
+ mTelemetryRemoteRequestStartMs);
+
+ MOZ_ASSERT(msecs >= 0);
+ mozilla::Telemetry::Accumulate(
+ mozilla::Telemetry::APPLICATION_REPUTATION_REMOTE_LOOKUP_RESPONSE_TIME,
+ msecs);
+ }
+
+ uint32_t verdict = nsIApplicationReputationService::VERDICT_SAFE;
+ Reason reason = Reason::NotSet;
+ nsresult rv = OnStopRequestInternal(aRequest, aResult, verdict, reason);
+ OnComplete(verdict, reason, rv);
+ return rv;
+}
+
+nsresult PendingLookup::OnStopRequestInternal(nsIRequest* aRequest,
+ nsresult aResult,
+ uint32_t& aVerdict,
+ Reason& aReason) {
+ auto scopeExit = mozilla::MakeScopeExit([&aReason]() {
+ // If |aReason| is not set while exiting, there must be an error.
+ if (aReason == Reason::NotSet) {
+ aReason = Reason::NetworkError;
+ }
+ });
+
+ if (NS_FAILED(aResult)) {
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_FAILED);
+ AccumulateCategorical(NSErrorToLabel(aResult));
+ return aResult;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv);
+ if (NS_FAILED(rv)) {
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_FAILED);
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_SERVER_2::
+ FailGetChannel);
+ return rv;
+ }
+
+ uint32_t status = 0;
+ rv = channel->GetResponseStatus(&status);
+ if (NS_FAILED(rv)) {
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_FAILED);
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_SERVER_2::
+ FailGetResponse);
+ return rv;
+ }
+
+ if (status != 200) {
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_FAILED);
+ AccumulateCategorical(HTTPStatusToLabel(status));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ std::string buf(mResponse.Data(), mResponse.Length());
+ safe_browsing::ClientDownloadResponse response;
+ if (!response.ParseFromString(buf)) {
+ LOG(("Invalid protocol buffer response [this = %p]: %s", this,
+ buf.c_str()));
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_INVALID);
+ return NS_ERROR_CANNOT_CONVERT_DATA;
+ }
+
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER,
+ SERVER_RESPONSE_VALID);
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_APPLICATION_REPUTATION_SERVER_2::
+ ResponseValid);
+
+ // Clamp responses 0-7, we only know about 0-4 for now.
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER_VERDICT,
+ std::min<uint32_t>(response.verdict(), 7));
+ const char* ext = GetFileExt(mFileName);
+ AccumulateCategoricalKeyed(nsCString(ext), VerdictToLabel(std::min<uint32_t>(
+ response.verdict(), 7)));
+ switch (response.verdict()) {
+ case safe_browsing::ClientDownloadResponse::DANGEROUS:
+ aVerdict = nsIApplicationReputationService::VERDICT_DANGEROUS;
+ aReason = Reason::VerdictDangerous;
+ break;
+ case safe_browsing::ClientDownloadResponse::DANGEROUS_HOST:
+ aVerdict = nsIApplicationReputationService::VERDICT_DANGEROUS_HOST;
+ aReason = Reason::VerdictDangerousHost;
+ break;
+ case safe_browsing::ClientDownloadResponse::POTENTIALLY_UNWANTED:
+ aVerdict = nsIApplicationReputationService::VERDICT_POTENTIALLY_UNWANTED;
+ aReason = Reason::VerdictUnwanted;
+ break;
+ case safe_browsing::ClientDownloadResponse::UNCOMMON:
+ aVerdict = nsIApplicationReputationService::VERDICT_UNCOMMON;
+ aReason = Reason::VerdictUncommon;
+ break;
+ case safe_browsing::ClientDownloadResponse::UNKNOWN:
+ aVerdict = nsIApplicationReputationService::VERDICT_SAFE;
+ aReason = Reason::VerdictUnknown;
+ break;
+ case safe_browsing::ClientDownloadResponse::SAFE:
+ aVerdict = nsIApplicationReputationService::VERDICT_SAFE;
+ aReason = Reason::VerdictSafe;
+ break;
+ default:
+ // Treat everything else as safe
+ aVerdict = nsIApplicationReputationService::VERDICT_SAFE;
+ aReason = Reason::VerdictUnrecognized;
+ break;
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(ApplicationReputationService, nsIApplicationReputationService)
+
+ApplicationReputationService*
+ ApplicationReputationService::gApplicationReputationService = nullptr;
+
+already_AddRefed<ApplicationReputationService>
+ApplicationReputationService::GetSingleton() {
+ if (!gApplicationReputationService) {
+ // Note: This is cleared in the new ApplicationReputationService destructor.
+ gApplicationReputationService = new ApplicationReputationService();
+ }
+ return do_AddRef(gApplicationReputationService);
+}
+
+ApplicationReputationService::ApplicationReputationService() {
+ LOG(("Application reputation service started up"));
+}
+
+ApplicationReputationService::~ApplicationReputationService() {
+ LOG(("Application reputation service shutting down"));
+ MOZ_ASSERT(gApplicationReputationService == this);
+ gApplicationReputationService = nullptr;
+}
+
+NS_IMETHODIMP
+ApplicationReputationService::QueryReputation(
+ nsIApplicationReputationQuery* aQuery,
+ nsIApplicationReputationCallback* aCallback) {
+ LOG(("Starting application reputation check [query=%p]", aQuery));
+ NS_ENSURE_ARG_POINTER(aQuery);
+ NS_ENSURE_ARG_POINTER(aCallback);
+
+ nsresult rv = QueryReputationInternal(aQuery, aCallback);
+ if (NS_FAILED(rv)) {
+ Reason reason = rv == NS_ERROR_NOT_AVAILABLE ? Reason::DPDisabled
+ : Reason::InternalError;
+
+ AccumulateCategorical(reason);
+ Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SHOULD_BLOCK, false);
+
+ aCallback->OnComplete(false, rv,
+ nsIApplicationReputationService::VERDICT_SAFE);
+ }
+ return NS_OK;
+}
+
+nsresult ApplicationReputationService::QueryReputationInternal(
+ nsIApplicationReputationQuery* aQuery,
+ nsIApplicationReputationCallback* aCallback) {
+ // If malware checks aren't enabled, don't query application reputation.
+ if (!Preferences::GetBool(PREF_SB_MALWARE_ENABLED, false)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (!Preferences::GetBool(PREF_SB_DOWNLOADS_ENABLED, false)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aQuery->GetSourceURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Bail if the URI hasn't been set.
+ NS_ENSURE_STATE(uri);
+
+ // Create a new pending lookup and start the call chain.
+ RefPtr<PendingLookup> lookup(new PendingLookup(aQuery, aCallback));
+
+ // Add an observer for shutdown
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) {
+ return NS_ERROR_FAILURE;
+ }
+
+ observerService->AddObserver(lookup, "quit-application", true);
+ return lookup->StartLookup();
+}
+
+nsresult ApplicationReputationService::IsBinary(const nsACString& aFileName,
+ bool* aBinary) {
+ *aBinary = ::IsBinary(aFileName);
+ return NS_OK;
+}
+
+nsresult ApplicationReputationService::IsExecutable(const nsACString& aFileName,
+ bool* aExecutable) {
+ *aExecutable =
+ ::IsFileType(aFileName, sExecutableExts, ArrayLength(sExecutableExts));
+ return NS_OK;
+}