diff options
Diffstat (limited to 'toolkit/components/downloads/ApplicationReputation.cpp')
-rw-r--r-- | toolkit/components/downloads/ApplicationReputation.cpp | 1629 |
1 files changed, 1629 insertions, 0 deletions
diff --git a/toolkit/components/downloads/ApplicationReputation.cpp b/toolkit/components/downloads/ApplicationReputation.cpp new file mode 100644 index 0000000000..7bd219dbf2 --- /dev/null +++ b/toolkit/components/downloads/ApplicationReputation.cpp @@ -0,0 +1,1629 @@ +/* -*- 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 "nsICryptoHash.h" +#include "nsIHttpChannel.h" +#include "nsIIOService.h" +#include "nsIPrefService.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 "nsIX509Cert.h" +#include "nsIX509CertDB.h" +#include "nsIX509CertList.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/LoadContext.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" + +#include "nsAutoPtr.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsNetCID.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsXPCOMStrings.h" + +#include "nsIContentPolicy.h" +#include "nsILoadInfo.h" +#include "nsContentUtils.h" + +using mozilla::ArrayLength; +using mozilla::BasePrincipal; +using mozilla::DocShellOriginAttributes; +using mozilla::PrincipalOriginAttributes; +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; + +// 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_GENERAL_LOCALE "general.useragent.locale" +#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) + +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 nsIObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITIMERCALLBACK + 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, + }; + + // 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, the referrer and possibly any redirects. + nsTArray<nsCString> mAnylistSpecs; + + // 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; + + // Returns true if the file is likely to be binary. + bool IsBinaryFile(); + + // Returns the type of download binary for the file. + ClientDownloadRequest::DownloadType GetDownloadType(const nsAString& aFilename); + + // Clean up and call the callback. PendingLookup must not be used after this + // function is called. + nsresult OnComplete(bool shouldBlock, nsresult rv, + uint32_t verdict = nsIApplicationReputationService::VERDICT_SAFE); + + // Wrapper function for nsIStreamListener.onStopRequest to make it easy to + // guarantee calling the callback + nsresult OnStopRequestInternal(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aResult, + bool* aShouldBlock, + uint32_t* aVerdict); + + // 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(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 + // 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(); +}; + +// 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, bool aAllowlistOnly); + +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; + bool mAllowlistOnly; + RefPtr<PendingLookup> mPendingLookup; + nsresult LookupSpecInternal(const nsACString& aSpec); +}; + +NS_IMPL_ISUPPORTS(PendingDBLookup, + nsIUrlClassifierCallback) + +PendingDBLookup::PendingDBLookup(PendingLookup* aPendingLookup) : + mAllowlistOnly(false), + 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, + bool aAllowlistOnly) +{ + LOG(("Checking principal %s [this=%p]", aSpec.Data(), this)); + mSpec = aSpec; + mAllowlistOnly = aAllowlistOnly; + 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); + + PrincipalOriginAttributes attrs; + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateCodebasePrincipal(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 = + do_GetService(NS_URLCLASSIFIERDBSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString tables; + nsAutoCString allowlist; + Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE, &allowlist); + if (!allowlist.IsEmpty()) { + tables.Append(allowlist); + } + nsAutoCString blocklist; + Preferences::GetCString(PREF_DOWNLOAD_BLOCK_TABLE, &blocklist); + if (!mAllowlistOnly && !blocklist.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 (!mAllowlistOnly && 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(true, NS_OK, + nsIApplicationReputationService::VERDICT_DANGEROUS); + } + + nsAutoCString allowList; + Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE, &allowList); + if (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 + } else { + 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) + +PendingLookup::PendingLookup(nsIApplicationReputationQuery* aQuery, + nsIApplicationReputationCallback* aCallback) : + 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 char16_t* kBinaryFileExtensions[] = { + // Extracted from the "File Type Policies" Chrome extension + //u".001", + //u".7z", + //u".ace", + //u".action", // Mac script + //u".ad", // Windows + u".ade", // MS Access + u".adp", // MS Access + u".apk", // Android package + u".app", // Executable application + u".application", // MS ClickOnce + u".appref-ms", // MS ClickOnce + //u".arc", + //u".arj", + u".as", // Mac archive + u".asp", // Windows Server script + u".asx", // Windows Media Player + //u".b64", + //u".balz", + u".bas", // Basic script + u".bash", // Linux shell + u".bat", // Windows shell + //u".bhx", + //u".bin", + u".bz", // Linux archive (bzip) + u".bz2", // Linux archive (bzip2) + u".bzip2", // Linux archive (bzip2) + u".cab", // Windows archive + u".cdr", // Mac disk image + u".cfg", // Windows + u".chi", // Windows Help + u".chm", // Windows Help + u".class", // Java + u".cmd", // Windows executable + u".com", // Windows executable + u".command", // Mac script + u".cpgz", // Mac archive + //u".cpio", + u".cpl", // Windows executable + u".crt", // Windows signed certificate + u".crx", // Chrome extensions + u".csh", // Linux shell + u".dart", // Mac disk image + u".dc42", // Apple DiskCopy Image + u".deb", // Linux package + u".dex", // Android + u".diskcopy42", // Apple DiskCopy Image + u".dll", // Windows executable + u".dmg", // Mac disk image + u".dmgpart", // Mac disk image + //u".docb", // MS Office + //u".docm", // MS Word + //u".docx", // MS Word + //u".dotm", // MS Word + //u".dott", // MS Office + u".drv", // Windows driver + u".dvdr", // Mac Disk image + u".efi", // Firmware + u".eml", // MS Outlook + u".exe", // Windows executable + //u".fat", + u".fon", // Windows font + u".fxp", // MS FoxPro + u".gadget", // Windows + u".grp", // Windows + u".gz", // Linux archive (gzip) + u".gzip", // Linux archive (gzip) + u".hfs", // Mac disk image + u".hlp", // Windows Help + u".hqx", // Mac archive + u".hta", // HTML trusted application + u".htt", // MS HTML template + u".img", // Mac disk image + u".imgpart", // Mac disk image + u".inf", // Windows installer + u".ini", // Generic config file + u".ins", // IIS config + //u".inx", // InstallShield + u".iso", // CD image + u".isp", // IIS config + //u".isu", // InstallShield + u".jar", // Java + u".jnlp", // Java + //u".job", // Windows + u".js", // JavaScript script + u".jse", // JScript + u".ksh", // Linux shell + //u".lha", + u".lnk", // Windows + u".local", // Windows + //u".lpaq1", + //u".lpaq5", + //u".lpaq8", + //u".lzh", + //u".lzma", + u".mad", // MS Access + u".maf", // MS Access + u".mag", // MS Access + u".mam", // MS Access + u".manifest", // Windows + u".maq", // MS Access + u".mar", // MS Access + u".mas", // MS Access + u".mat", // MS Access + u".mau", // Media attachment + u".mav", // MS Access + u".maw", // MS Access + u".mda", // MS Access + u".mdb", // MS Access + u".mde", // MS Access + u".mdt", // MS Access + u".mdw", // MS Access + u".mdz", // MS Access + u".mht", // MS HTML + u".mhtml", // MS HTML + u".mim", // MS Mail + u".mmc", // MS Office + u".mof", // Windows + u".mpkg", // Mac installer + u".msc", // Windows executable + u".msg", // MS Outlook + u".msh", // Windows shell + u".msh1", // Windows shell + u".msh1xml", // Windows shell + u".msh2", // Windows shell + u".msh2xml", // Windows shell + u".mshxml", // Windows + u".msi", // Windows installer + u".msp", // Windows installer + u".mst", // Windows installer + u".ndif", // Mac disk image + //u".ntfs", // 7z + u".ocx", // ActiveX + u".ops", // MS Office + //u".out", // Linux binary + //u".paf", // PortableApps package + //u".paq8f", + //u".paq8jd", + //u".paq8l", + //u".paq8o", + u".partial", // Downloads + u".pax", // Mac archive + u".pcd", // Microsoft Visual Test + u".pdf", // Adobe Acrobat + //u".pea", + u".pet", // Linux package + u".pif", // Windows + u".pkg", // Mac installer + u".pl", // Perl script + u".plg", // MS Visual Studio + //u".potx", // MS PowerPoint + //u".ppam", // MS PowerPoint + //u".ppsx", // MS PowerPoint + //u".pptm", // MS PowerPoint + //u".pptx", // MS PowerPoint + u".prf", // MS Outlook + u".prg", // Windows + u".ps1", // Windows shell + u".ps1xml", // Windows shell + u".ps2", // Windows shell + u".ps2xml", // Windows shell + u".psc1", // Windows shell + u".psc2", // Windows shell + u".pst", // MS Outlook + u".pup", // Linux package + u".py", // Python script + u".pyc", // Python binary + u".pyw", // Python GUI + //u".quad", + //u".r00", + //u".r01", + //u".r02", + //u".r03", + //u".r04", + //u".r05", + //u".r06", + //u".r07", + //u".r08", + //u".r09", + //u".r10", + //u".r11", + //u".r12", + //u".r13", + //u".r14", + //u".r15", + //u".r16", + //u".r17", + //u".r18", + //u".r19", + //u".r20", + //u".r21", + //u".r22", + //u".r23", + //u".r24", + //u".r25", + //u".r26", + //u".r27", + //u".r28", + //u".r29", + //u".rar", + u".rb", // Ruby script + u".reg", // Windows Registry + u".rels", // MS Office + //u".rgs", // Windows Registry + u".rpm", // Linux package + //u".rtf", // MS Office + //u".run", // Linux shell + u".scf", // Windows shell + u".scr", // Windows + u".sct", // Windows shell + u".search-ms", // Windows + u".sh", // Linux shell + u".shar", // Linux shell + u".shb", // Windows + u".shs", // Windows shell + //u".sldm", // MS PowerPoint + //u".sldx", // MS PowerPoint + u".slp", // Linux package + u".smi", // Mac disk image + u".sparsebundle", // Mac disk image + u".sparseimage", // Mac disk image + u".spl", // Adobe Flash + //u".squashfs", + u".svg", + u".swf", // Adobe Flash + u".swm", // Windows Imaging + u".sys", // Windows + u".tar", // Linux archive + u".taz", // Linux archive (bzip2) + u".tbz", // Linux archive (bzip2) + u".tbz2", // Linux archive (bzip2) + u".tcsh", // Linux shell + u".tgz", // Linux archive (gzip) + //u".toast", // Roxio disk image + //u".torrent", // Bittorrent + u".tpz", // Linux archive (gzip) + u".txz", // Linux archive (xz) + u".tz", // Linux archive (gzip) + //u".u3p", // U3 Smart Apps + u".udf", // MS Excel + u".udif", // Mac disk image + u".url", // Windows + //u".uu", + //u".uue", + u".vb", // Visual Basic script + u".vbe", // Visual Basic script + u".vbs", // Visual Basic script + //u".vbscript", // Visual Basic script + u".vhd", // Windows virtual hard drive + u".vhdx", // Windows virtual hard drive + u".vmdk", // VMware virtual disk + u".vsd", // MS Visio + u".vsmacros", // MS Visual Studio + u".vss", // MS Visio + u".vst", // MS Visio + u".vsw", // MS Visio + u".website", // Windows + u".wim", // Windows Imaging + //u".workflow", // Mac Automator + //u".wrc", // FreeArc archive + u".ws", // Windows script + u".wsc", // Windows script + u".wsf", // Windows script + u".wsh", // Windows script + u".xar", // MS Excel + u".xbap", // XAML Browser Application + u".xip", // Mac archive + //u".xlsm", // MS Excel + //u".xlsx", // MS Excel + //u".xltm", // MS Excel + //u".xltx", // MS Excel + u".xml", + u".xnk", // MS Exchange + u".xrm-ms", // Windows + u".xsl", // XML Stylesheet + //u".xxe", + u".xz", // Linux archive (xz) + u".z", // InstallShield +#ifdef XP_WIN // disable on Mac/Linux, see 1167493 + u".zip", // Generic archive +#endif + u".zipx", // WinZip + //u".zpaq", +}; + +bool +PendingLookup::IsBinaryFile() +{ + nsString fileName; + nsresult rv = mQuery->GetSuggestedFileName(fileName); + if (NS_FAILED(rv)) { + LOG(("No suggested filename [this = %p]", this)); + return false; + } + LOG(("Suggested filename: %s [this = %p]", + NS_ConvertUTF16toUTF8(fileName).get(), this)); + + for (size_t i = 0; i < ArrayLength(kBinaryFileExtensions); ++i) { + if (StringEndsWith(fileName, nsDependentString(kBinaryFileExtensions[i]))) { + return true; + } + } + + return false; +} + +ClientDownloadRequest::DownloadType +PendingLookup::GetDownloadType(const nsAString& aFilename) { + MOZ_ASSERT(IsBinaryFile()); + + // From https://cs.chromium.org/chromium/src/chrome/common/safe_browsing/download_protection_util.cc?l=17 + if (StringEndsWith(aFilename, NS_LITERAL_STRING(".zip"))) { + return ClientDownloadRequest::ZIPPED_EXECUTABLE; + } else if (StringEndsWith(aFilename, NS_LITERAL_STRING(".apk"))) { + return ClientDownloadRequest::ANDROID_APK; + } else if (StringEndsWith(aFilename, NS_LITERAL_STRING(".app")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".cdr")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".dart")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".dc42")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".diskcopy42")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".dmg")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".dmgpart")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".dvdr")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".img")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".imgpart")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".iso")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".mpkg")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".ndif")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".pkg")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".smi")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".sparsebundle")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".sparseimage")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".toast")) || + StringEndsWith(aFilename, NS_LITERAL_STRING(".udif"))) { + 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 (mBlocklistCount > 0) { + return OnComplete(true, NS_OK, + nsIApplicationReputationService::VERDICT_DANGEROUS); + } + int index = mAnylistSpecs.Length() - 1; + nsCString spec; + if (index >= 0) { + // Check the source URI, referrer and redirect chain. + spec = mAnylistSpecs[index]; + mAnylistSpecs.RemoveElementAt(index); + RefPtr<PendingDBLookup> lookup(new PendingDBLookup(this)); + return lookup->LookupSpec(spec, false); + } + // If any of mAnylistSpecs matched the blocklist, go ahead and block. + if (mBlocklistCount > 0) { + return OnComplete(true, NS_OK, + nsIApplicationReputationService::VERDICT_DANGEROUS); + } + // If any of mAnylistSpecs matched the allowlist, go ahead and pass. + if (mAllowlistCount > 0) { + return OnComplete(false, NS_OK); + } + // Only binary signatures remain. + index = mAllowlistSpecs.Length() - 1; + if (index >= 0) { + spec = mAllowlistSpecs[index]; + LOG(("PendingLookup::LookupNext: checking %s on allowlist", spec.get())); + mAllowlistSpecs.RemoveElementAt(index); + RefPtr<PendingDBLookup> lookup(new PendingDBLookup(this)); + return lookup->LookupSpec(spec, true); + } + // There are no more URIs to check against local list. If the file is + // not eligible for remote lookup, bail. + if (!IsBinaryFile()) { + LOG(("Not eligible for remote lookups [this=%x]", this)); + return OnComplete(false, NS_OK); + } + nsresult rv = SendRemoteQuery(); + if (NS_FAILED(rv)) { + return OnComplete(false, 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; + rv = certDB->ConstructX509( + const_cast<char *>(aChain.element(0).certificate().data()), + aChain.element(0).certificate().size(), getter_AddRefs(signer)); + NS_ENSURE_SUCCESS(rv, rv); + + for (int i = 1; i < aChain.element_size(); ++i) { + // Get the issuer. + nsCOMPtr<nsIX509Cert> issuer; + rv = certDB->ConstructX509( + const_cast<char *>(aChain.element(i).certificate().data()), + aChain.element(i).certificate().size(), 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<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. The most recent redirect is + // the last element. + nsCString spec; + rv = GetStrippedSpec(uri, 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() +{ + mStartTime = TimeStamp::Now(); + nsresult rv = DoLookupInternal(); + if (NS_FAILED(rv)) { + return OnComplete(false, NS_OK); + } + return rv; +} + +nsresult +PendingLookup::GetSpecHash(nsACString& aSpec, nsACString& hexEncodedHash) +{ + nsresult rv; + + nsCOMPtr<nsICryptoHash> cryptoHash = + do_CreateInstance("@mozilla.org/security/hash;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = cryptoHash->Init(nsICryptoHash::SHA256); + 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<const 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; + + } else 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.Append("://"); + 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<nsIURI> referrer = nullptr; + rv = mQuery->GetReferrerURI(getter_AddRefs(referrer)); + if (referrer) { + nsCString referrerSpec; + rv = GetStrippedSpec(referrer, referrerSpec); + NS_ENSURE_SUCCESS(rv, rv); + mAnylistSpecs.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)); + } + + // 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); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = GenerateWhitelistStrings(); + NS_ENSURE_SUCCESS(rv, rv); + + // Start the call chain. + return LookupNext(); +} + +nsresult +PendingLookup::OnComplete(bool shouldBlock, nsresult rv, uint32_t verdict) +{ + MOZ_ASSERT(!shouldBlock || + verdict != nsIApplicationReputationService::VERDICT_SAFE); + + if (NS_FAILED(rv)) { + nsAutoCString errorName; + mozilla::GetErrorName(rv, errorName); + LOG(("Failed sending remote query for application reputation " + "[rv = %s, this = %p]", errorName.get(), this)); + } + + if (mTimeoutTimer) { + mTimeoutTimer->Cancel(); + mTimeoutTimer = nullptr; + } + + Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SHOULD_BLOCK, + shouldBlock); + double t = (TimeStamp::Now() - mStartTime).ToMilliseconds(); + LOG(("Application Reputation verdict is %lu, obtained in %f ms [this = %p]", + verdict, 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, rv, verdict); + return res; +} + +nsresult +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; + nsresult rv = aSigArray->Enumerate(getter_AddRefs(chains)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMoreChains = false; + rv = chains->HasMoreElements(&hasMoreChains); + NS_ENSURE_SUCCESS(rv, rv); + + while (hasMoreChains) { + nsCOMPtr<nsISupports> chainSupports; + rv = chains->GetNext(getter_AddRefs(chainSupports)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIX509CertList> certList = do_QueryInterface(chainSupports, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + safe_browsing::ClientDownloadRequest_CertificateChain* certChain = + 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) { + nsCOMPtr<nsISupports> certSupports; + rv = chainElt->GetNext(getter_AddRefs(certSupports)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIX509Cert> cert = do_QueryInterface(certSupports, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint8_t* data = nullptr; + uint32_t len = 0; + rv = cert->GetRawDER(&len, &data); + NS_ENSURE_SUCCESS(rv, rv); + + // Add this certificate to the protobuf to send remotely. + certChain->add_element()->set_certificate(data, len); + free(data); + + rv = chainElt->HasMoreElements(&hasMoreCerts); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = chains->HasMoreElements(&hasMoreChains); + NS_ENSURE_SUCCESS(rv, rv); + } + if (mRequest.signature().certificate_chain_size() > 0) { + mRequest.mutable_signature()->set_trusted(true); + } + return NS_OK; +} + +nsresult +PendingLookup::SendRemoteQuery() +{ + nsresult rv = SendRemoteQueryInternal(); + if (NS_FAILED(rv)) { + return OnComplete(false, rv); + } + // SendRemoteQueryInternal has fired off the query and we call OnComplete in + // the nsIStreamListener.onStopRequest. + return rv; +} + +nsresult +PendingLookup::SendRemoteQueryInternal() +{ + // 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)); + return NS_ERROR_NOT_AVAILABLE; + } + // If the remote lookup URL is empty or absent, bail. + nsCString serviceUrl; + NS_ENSURE_SUCCESS(Preferences::GetCString(PREF_SB_APP_REP_URL, &serviceUrl), + NS_ERROR_NOT_AVAILABLE); + if (serviceUrl.IsEmpty()) { + LOG(("Remote lookup URL is empty [this = %p]", this)); + return NS_ERROR_NOT_AVAILABLE; + } + + // If the blocklist or allowlist is empty (so we couldn't do local lookups), + // bail + { + nsAutoCString table; + NS_ENSURE_SUCCESS(Preferences::GetCString(PREF_DOWNLOAD_BLOCK_TABLE, + &table), + NS_ERROR_NOT_AVAILABLE); + if (table.IsEmpty()) { + LOG(("Blocklist is empty [this = %p]", this)); + return NS_ERROR_NOT_AVAILABLE; + } + } +#ifdef XP_WIN + // The allowlist is only needed to do signature verification on Windows + { + nsAutoCString table; + NS_ENSURE_SUCCESS(Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE, + &table), + NS_ERROR_NOT_AVAILABLE); + if (table.IsEmpty()) { + LOG(("Allowlist is empty [this = %p]", this)); + return NS_ERROR_NOT_AVAILABLE; + } + } +#endif + + 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; + NS_ENSURE_SUCCESS(Preferences::GetCString(PREF_GENERAL_LOCALE, &locale), + NS_ERROR_NOT_AVAILABLE); + mRequest.set_locale(locale.get()); + nsCString sha256Hash; + rv = mQuery->GetSha256Hash(sha256Hash); + NS_ENSURE_SUCCESS(rv, rv); + mRequest.mutable_digests()->set_sha256(sha256Hash.Data()); + nsString fileName; + rv = mQuery->GetSuggestedFileName(fileName); + NS_ENSURE_SUCCESS(rv, rv); + mRequest.set_file_basename(NS_ConvertUTF16toUTF8(fileName).get()); + mRequest.set_download_type(GetDownloadType(fileName)); + + 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; + } + LOG(("Serialized protocol buffer [this = %p]: (length=%d) %s", this, + serialized.length(), serialized.c_str())); + + // 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->NewChannel2(serviceUrl, + nullptr, + nullptr, + nullptr, // aLoadingNode + nsContentUtils::GetSystemPrincipal(), + nullptr, // aTriggeringPrincipal + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER, + getter_AddRefs(mChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> loadInfo = mChannel->GetLoadInfo(); + if (loadInfo) { + loadInfo->SetOriginAttributes( + mozilla::NeckoOriginAttributes(NECKO_SAFEBROWSING_APP_ID, false)); + } + + 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, + NS_LITERAL_CSTRING("application/octet-stream"), serialized.size(), + NS_LITERAL_CSTRING("POST"), false); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the Safebrowsing cookie jar, so that the regular Google cookie is not + // sent with this request. See bug 897516. + DocShellOriginAttributes attrs; + attrs.mAppId = NECKO_SAFEBROWSING_APP_ID; + nsCOMPtr<nsIInterfaceRequestor> loadContext = new mozilla::LoadContext(attrs); + rv = mChannel->SetNotificationCallbacks(loadContext); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t timeoutMs = Preferences::GetUint(PREF_SB_DOWNLOADS_REMOTE_TIMEOUT, 10000); + mTimeoutTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + mTimeoutTimer->InitWithCallback(this, timeoutMs, nsITimer::TYPE_ONE_SHOT); + + rv = mChannel->AsyncOpen2(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); + mTimeoutTimer->Cancel(); + 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, + nsISupports *aContext, + nsIInputStream *aStream, + uint64_t offset, + uint32_t count) { + uint32_t read; + return aStream->ReadSegments(AppendSegmentToString, &mResponse, count, &read); +} + +NS_IMETHODIMP +PendingLookup::OnStartRequest(nsIRequest *aRequest, + nsISupports *aContext) { + return NS_OK; +} + +NS_IMETHODIMP +PendingLookup::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aResult) { + NS_ENSURE_STATE(mCallback); + + bool shouldBlock = false; + uint32_t verdict = nsIApplicationReputationService::VERDICT_SAFE; + Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT, + false); + + nsresult rv = OnStopRequestInternal(aRequest, aContext, aResult, + &shouldBlock, &verdict); + OnComplete(shouldBlock, rv, verdict); + return rv; +} + +nsresult +PendingLookup::OnStopRequestInternal(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aResult, + bool* aShouldBlock, + uint32_t* aVerdict) { + if (NS_FAILED(aResult)) { + Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER, + SERVER_RESPONSE_FAILED); + return aResult; + } + + *aShouldBlock = false; + *aVerdict = nsIApplicationReputationService::VERDICT_SAFE; + nsresult rv; + nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv); + if (NS_FAILED(rv)) { + Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER, + SERVER_RESPONSE_FAILED); + return rv; + } + + uint32_t status = 0; + rv = channel->GetResponseStatus(&status); + if (NS_FAILED(rv)) { + Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER, + SERVER_RESPONSE_FAILED); + return rv; + } + + if (status != 200) { + Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_SERVER, + SERVER_RESPONSE_FAILED); + 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); + // 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)); + switch(response.verdict()) { + case safe_browsing::ClientDownloadResponse::DANGEROUS: + *aShouldBlock = Preferences::GetBool(PREF_BLOCK_DANGEROUS, true); + *aVerdict = nsIApplicationReputationService::VERDICT_DANGEROUS; + break; + case safe_browsing::ClientDownloadResponse::DANGEROUS_HOST: + *aShouldBlock = Preferences::GetBool(PREF_BLOCK_DANGEROUS_HOST, true); + *aVerdict = nsIApplicationReputationService::VERDICT_DANGEROUS_HOST; + break; + case safe_browsing::ClientDownloadResponse::POTENTIALLY_UNWANTED: + *aShouldBlock = Preferences::GetBool(PREF_BLOCK_POTENTIALLY_UNWANTED, false); + *aVerdict = nsIApplicationReputationService::VERDICT_POTENTIALLY_UNWANTED; + break; + case safe_browsing::ClientDownloadResponse::UNCOMMON: + *aShouldBlock = Preferences::GetBool(PREF_BLOCK_UNCOMMON, false); + *aVerdict = nsIApplicationReputationService::VERDICT_UNCOMMON; + break; + default: + // Treat everything else as safe + break; + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(ApplicationReputationService, + nsIApplicationReputationService) + +ApplicationReputationService* + ApplicationReputationService::gApplicationReputationService = nullptr; + +ApplicationReputationService* +ApplicationReputationService::GetSingleton() +{ + if (gApplicationReputationService) { + NS_ADDREF(gApplicationReputationService); + return gApplicationReputationService; + } + + // We're not initialized yet. + gApplicationReputationService = new ApplicationReputationService(); + if (gApplicationReputationService) { + NS_ADDREF(gApplicationReputationService); + } + + return 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); + + Accumulate(mozilla::Telemetry::APPLICATION_REPUTATION_COUNT, true); + nsresult rv = QueryReputationInternal(aQuery, aCallback); + if (NS_FAILED(rv)) { + 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) { + nsresult rv; + // 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; + 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)); + NS_ENSURE_STATE(lookup); + + // Add an observer for shutdown + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) { + return NS_ERROR_FAILURE; + } + + observerService->AddObserver(lookup, "quit-application", false); + return lookup->StartLookup(); +} |