2014 lines
71 KiB
C++
2014 lines
71 KiB
C++
/* -*- 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/glean/ReputationserviceMetrics.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::BasePrincipal;
|
|
using mozilla::OriginAttributes;
|
|
using mozilla::Preferences;
|
|
using mozilla::TimeStamp;
|
|
using mozilla::intl::LocaleService;
|
|
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",
|
|
".terminal",
|
|
// 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",
|
|
//".library-ms", exec // Windows
|
|
//".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", exec // 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::glean::application_reputation::ReasonLabel;
|
|
|
|
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);
|
|
}
|
|
if (principal->IsSystemPrincipal()) {
|
|
return mPendingLookup->LookupNext();
|
|
}
|
|
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++;
|
|
mozilla::glean::application_reputation::local.AccumulateSingleSample(
|
|
BLOCK_LIST);
|
|
LOG(("Found principal %s on blocklist [this = %p]", mSpec.get(), this));
|
|
return mPendingLookup->OnComplete(
|
|
nsIApplicationReputationService::VERDICT_DANGEROUS,
|
|
Reason::eLocalblocklist, NS_OK);
|
|
}
|
|
|
|
nsAutoCString allowList;
|
|
Preferences::GetCString(PREF_DOWNLOAD_ALLOW_TABLE, allowList);
|
|
if ((mLookupType != LookupType::BlocklistOnly) &&
|
|
FindInReadable(allowList, tables)) {
|
|
mPendingLookup->mAllowlistCount++;
|
|
mozilla::glean::application_reputation::local.AccumulateSingleSample(
|
|
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));
|
|
mozilla::glean::application_reputation::local.AccumulateSingleSample(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, std::size(_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,
|
|
std::size(ApplicationReputationService::kBinaryFileExtensions)) ||
|
|
(!IsFileType(
|
|
aFilename, ApplicationReputationService::kNonBinaryExecutables,
|
|
std::size(ApplicationReputationService::kNonBinaryExecutables)) &&
|
|
IsFileType(aFilename, sExecutableExts, std::size(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::eLocalwhitelist, 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)) {
|
|
mozilla::glean::application_reputation::binary_type
|
|
.EnumGet(mozilla::glean::application_reputation::BinaryTypeLabel::
|
|
eBinaryfile)
|
|
.Add();
|
|
} else if (IsFileType(mFileName, kSafeFileExtensions,
|
|
std::size(kSafeFileExtensions))) {
|
|
mozilla::glean::application_reputation::binary_type
|
|
.EnumGet(mozilla::glean::application_reputation::BinaryTypeLabel::
|
|
eNonbinaryfile)
|
|
.Add();
|
|
} else if (IsFileType(mFileName, kMozNonBinaryExecutables,
|
|
std::size(kMozNonBinaryExecutables))) {
|
|
mozilla::glean::application_reputation::binary_type
|
|
.EnumGet(mozilla::glean::application_reputation::BinaryTypeLabel::
|
|
eMoznonbinaryfile)
|
|
.Add();
|
|
} else {
|
|
mozilla::glean::application_reputation::binary_type
|
|
.EnumGet(mozilla::glean::application_reputation::BinaryTypeLabel::
|
|
eUnknownfile)
|
|
.Add();
|
|
}
|
|
} else {
|
|
mozilla::glean::application_reputation::binary_type
|
|
.EnumGet(mozilla::glean::application_reputation::BinaryTypeLabel::
|
|
eMissingfilename)
|
|
.Add();
|
|
}
|
|
|
|
if (IsFileType(mFileName, kDmgFileExtensions,
|
|
std::size(kDmgFileExtensions))) {
|
|
mozilla::glean::application_reputation::binary_archive
|
|
.EnumGet(mozilla::glean::application_reputation::BinaryArchiveLabel::
|
|
eDmgfile)
|
|
.Add();
|
|
} else if (IsFileType(mFileName, kRarFileExtensions,
|
|
std::size(kRarFileExtensions))) {
|
|
mozilla::glean::application_reputation::binary_archive
|
|
.EnumGet(mozilla::glean::application_reputation::BinaryArchiveLabel::
|
|
eRarfile)
|
|
.Add();
|
|
} else if (IsFileType(mFileName, kZipFileExtensions,
|
|
std::size(kZipFileExtensions))) {
|
|
mozilla::glean::application_reputation::binary_archive
|
|
.EnumGet(mozilla::glean::application_reputation::BinaryArchiveLabel::
|
|
eZipfile)
|
|
.Add();
|
|
} else if (mIsBinaryFile) {
|
|
mozilla::glean::application_reputation::binary_archive
|
|
.EnumGet(mozilla::glean::application_reputation::BinaryArchiveLabel::
|
|
eOtherbinaryfile)
|
|
.Add();
|
|
}
|
|
|
|
// 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::eNonbinaryfile, NS_OK);
|
|
}
|
|
|
|
nsresult rv = SendRemoteQuery();
|
|
if (NS_FAILED(rv)) {
|
|
return OnComplete(nsIApplicationReputationService::VERDICT_SAFE,
|
|
Reason::eInternalerror, 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::eInternalerror, 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::eDangerousprefoff;
|
|
}
|
|
break;
|
|
case nsIApplicationReputationService::VERDICT_UNCOMMON:
|
|
if (!Preferences::GetBool(PREF_BLOCK_UNCOMMON, true)) {
|
|
shouldBlock = false;
|
|
aReason = Reason::eUncommonprefoff;
|
|
}
|
|
break;
|
|
case nsIApplicationReputationService::VERDICT_POTENTIALLY_UNWANTED:
|
|
if (!Preferences::GetBool(PREF_BLOCK_POTENTIALLY_UNWANTED, true)) {
|
|
shouldBlock = false;
|
|
aReason = Reason::eUnwantedprefoff;
|
|
}
|
|
break;
|
|
case nsIApplicationReputationService::VERDICT_DANGEROUS_HOST:
|
|
if (!Preferences::GetBool(PREF_BLOCK_DANGEROUS_HOST, true)) {
|
|
shouldBlock = false;
|
|
aReason = Reason::eDangeroushostprefoff;
|
|
}
|
|
break;
|
|
default:
|
|
shouldBlock = false;
|
|
break;
|
|
}
|
|
|
|
mozilla::glean::application_reputation::reason.EnumGet(aReason).Add();
|
|
mozilla::glean::application_reputation::should_block
|
|
.EnumGet(
|
|
static_cast<mozilla::glean::application_reputation::ShouldBlockLabel>(
|
|
shouldBlock))
|
|
.Add();
|
|
|
|
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,
|
|
std::size(ApplicationReputationService::kNonBinaryExecutables)));
|
|
Reason reason = Reason::eNotset;
|
|
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::eNotset) {
|
|
aReason = Reason::eInternalerror;
|
|
}
|
|
});
|
|
|
|
// 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::eRemotelookupdisabled;
|
|
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::eRemotelookupdisabled;
|
|
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->CopyData(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);
|
|
mozilla::glean::application_reputation::remote_lookup_timeout
|
|
.EnumGet(mozilla::glean::application_reputation::
|
|
RemoteLookupTimeoutLabel::eTrue)
|
|
.Add();
|
|
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) {
|
|
mozilla::glean::application_reputation::remote_lookup_timeout
|
|
.EnumGet(mozilla::glean::application_reputation::
|
|
RemoteLookupTimeoutLabel::eFalse)
|
|
.Add();
|
|
|
|
MOZ_ASSERT(mTelemetryRemoteRequestStartMs > 0);
|
|
int32_t msecs = PR_IntervalToMilliseconds(PR_IntervalNow() -
|
|
mTelemetryRemoteRequestStartMs);
|
|
|
|
MOZ_ASSERT(msecs >= 0);
|
|
mozilla::glean::application_reputation::remote_lookup_response_time
|
|
.AccumulateRawDuration(mozilla::TimeDuration::FromMilliseconds(msecs));
|
|
}
|
|
|
|
uint32_t verdict = nsIApplicationReputationService::VERDICT_SAFE;
|
|
Reason reason = Reason::eNotset;
|
|
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::eNotset) {
|
|
aReason = Reason::eNetworkerror;
|
|
}
|
|
});
|
|
|
|
if (NS_FAILED(aResult)) {
|
|
mozilla::glean::application_reputation::server.AccumulateSingleSample(
|
|
SERVER_RESPONSE_FAILED);
|
|
mozilla::glean::application_reputation::server_2
|
|
.EnumGet(NSErrorToLabel(aResult))
|
|
.Add();
|
|
return aResult;
|
|
}
|
|
|
|
nsresult rv;
|
|
nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv);
|
|
if (NS_FAILED(rv)) {
|
|
mozilla::glean::application_reputation::server.AccumulateSingleSample(
|
|
SERVER_RESPONSE_FAILED);
|
|
mozilla::glean::application_reputation::server_2
|
|
.EnumGet(mozilla::glean::application_reputation::Server2Label::
|
|
eFailgetchannel)
|
|
.Add();
|
|
return rv;
|
|
}
|
|
|
|
uint32_t status = 0;
|
|
rv = channel->GetResponseStatus(&status);
|
|
if (NS_FAILED(rv)) {
|
|
mozilla::glean::application_reputation::server.AccumulateSingleSample(
|
|
SERVER_RESPONSE_FAILED);
|
|
mozilla::glean::application_reputation::server_2
|
|
.EnumGet(mozilla::glean::application_reputation::Server2Label::
|
|
eFailgetresponse)
|
|
.Add();
|
|
return rv;
|
|
}
|
|
|
|
if (status != 200) {
|
|
mozilla::glean::application_reputation::server.AccumulateSingleSample(
|
|
SERVER_RESPONSE_FAILED);
|
|
mozilla::glean::application_reputation::server_2
|
|
.EnumGet(HTTPStatusToLabel(status))
|
|
.Add();
|
|
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()));
|
|
mozilla::glean::application_reputation::server.AccumulateSingleSample(
|
|
SERVER_RESPONSE_INVALID);
|
|
return NS_ERROR_CANNOT_CONVERT_DATA;
|
|
}
|
|
|
|
mozilla::glean::application_reputation::server.AccumulateSingleSample(
|
|
SERVER_RESPONSE_VALID);
|
|
mozilla::glean::application_reputation::server_2
|
|
.EnumGet(
|
|
mozilla::glean::application_reputation::Server2Label::eResponsevalid)
|
|
.Add();
|
|
|
|
// Clamp responses 0-7, we only know about 0-4 for now.
|
|
mozilla::glean::application_reputation::server_verdict.AccumulateSingleSample(
|
|
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::eVerdictdangerous;
|
|
break;
|
|
case safe_browsing::ClientDownloadResponse::DANGEROUS_HOST:
|
|
aVerdict = nsIApplicationReputationService::VERDICT_DANGEROUS_HOST;
|
|
aReason = Reason::eVerdictdangeroushost;
|
|
break;
|
|
case safe_browsing::ClientDownloadResponse::POTENTIALLY_UNWANTED:
|
|
aVerdict = nsIApplicationReputationService::VERDICT_POTENTIALLY_UNWANTED;
|
|
aReason = Reason::eVerdictunwanted;
|
|
break;
|
|
case safe_browsing::ClientDownloadResponse::UNCOMMON:
|
|
aVerdict = nsIApplicationReputationService::VERDICT_UNCOMMON;
|
|
aReason = Reason::eVerdictuncommon;
|
|
break;
|
|
case safe_browsing::ClientDownloadResponse::UNKNOWN:
|
|
aVerdict = nsIApplicationReputationService::VERDICT_SAFE;
|
|
aReason = Reason::eVerdictunknown;
|
|
break;
|
|
case safe_browsing::ClientDownloadResponse::SAFE:
|
|
aVerdict = nsIApplicationReputationService::VERDICT_SAFE;
|
|
aReason = Reason::eVerdictsafe;
|
|
break;
|
|
default:
|
|
// Treat everything else as safe
|
|
aVerdict = nsIApplicationReputationService::VERDICT_SAFE;
|
|
aReason = Reason::eVerdictunrecognized;
|
|
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::eDpdisabled
|
|
: Reason::eInternalerror;
|
|
|
|
mozilla::glean::application_reputation::reason.EnumGet(reason).Add();
|
|
mozilla::glean::application_reputation::should_block
|
|
.EnumGet(
|
|
mozilla::glean::application_reputation::ShouldBlockLabel::eFalse)
|
|
.Add();
|
|
|
|
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, std::size(sExecutableExts));
|
|
return NS_OK;
|
|
}
|