Bug 1965504 part 5 - integrate Content Analysis into Downloads a=diannaS
The actual call to content analysis is in DownloadCore.sys.mjs's _checkReputationAndMove() method. Original Revision: https://phabricator.services.mozilla.com/D251881 Differential Revision: https://phabricator.services.mozilla.com/D258207
This commit is contained in:
committed by
dsmith@mozilla.com
parent
143dc7420f
commit
099ed944b9
@@ -143,6 +143,7 @@ export var DownloadsCommon = {
|
||||
DOWNLOAD_BLOCKED_PARENTAL: 6,
|
||||
DOWNLOAD_DIRTY: 8,
|
||||
DOWNLOAD_BLOCKED_POLICY: 9,
|
||||
DOWNLOAD_BLOCKED_CONTENT_ANALYSIS: 10,
|
||||
|
||||
// The following are the possible values of the "attention" property.
|
||||
ATTENTION_NONE: "",
|
||||
@@ -295,6 +296,18 @@ export var DownloadsCommon = {
|
||||
if (download.error.becauseBlockedByReputationCheck) {
|
||||
return DownloadsCommon.DOWNLOAD_DIRTY;
|
||||
}
|
||||
if (download.error.becauseBlockedByContentAnalysis) {
|
||||
// BLOCK_VERDICT_MALWARE indicates that the download was
|
||||
// blocked by the content analysis service, so return
|
||||
// DOWNLOAD_BLOCKED_CONTENT_ANALYSIS to indicate this.
|
||||
// Otherwise, the content analysis service returned
|
||||
// WARN, so the user has a chance to unblock the download,
|
||||
// which corresponds with DOWNLOAD_DIRTY.
|
||||
return download.error.reputationCheckVerdict ===
|
||||
lazy.Downloads.Error.BLOCK_VERDICT_MALWARE
|
||||
? DownloadsCommon.DOWNLOAD_BLOCKED_CONTENT_ANALYSIS
|
||||
: DownloadsCommon.DOWNLOAD_DIRTY;
|
||||
}
|
||||
return DownloadsCommon.DOWNLOAD_FAILED;
|
||||
}
|
||||
if (download.canceled) {
|
||||
@@ -311,7 +324,7 @@ export var DownloadsCommon = {
|
||||
*/
|
||||
async deleteDownload(download) {
|
||||
// Check hasBlockedData to avoid double counting if you click the X button
|
||||
// in the Libarary view and then delete the download from the history.
|
||||
// in the Library view and then delete the download from the history.
|
||||
if (
|
||||
download.error?.becauseBlockedByReputationCheck &&
|
||||
download.hasBlockedData
|
||||
@@ -331,6 +344,9 @@ export var DownloadsCommon = {
|
||||
}
|
||||
let list = await lazy.Downloads.getList(lazy.Downloads.ALL);
|
||||
await list.remove(download);
|
||||
if (download.error?.becauseBlockedByContentAnalysis) {
|
||||
await download.respondToContentAnalysisWarnWithBlock();
|
||||
}
|
||||
await download.finalize(true);
|
||||
},
|
||||
|
||||
@@ -359,6 +375,9 @@ export var DownloadsCommon = {
|
||||
await list.remove(download);
|
||||
}
|
||||
await download.manuallyRemoveData();
|
||||
if (download.error?.becauseBlockedByContentAnalysis) {
|
||||
await download.respondToContentAnalysisWarnWithBlock();
|
||||
}
|
||||
if (clearHistory < 2) {
|
||||
lazy.DownloadHistory.updateMetaData(download).catch(console.error);
|
||||
}
|
||||
@@ -629,6 +648,8 @@ export var DownloadsCommon = {
|
||||
* the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown
|
||||
* reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is
|
||||
* assumed.
|
||||
* becauseBlockedByReputationCheck:
|
||||
* Whether the the download was blocked by a reputation check.
|
||||
* window:
|
||||
* The window with which this action is associated.
|
||||
* dialogType:
|
||||
@@ -645,7 +666,12 @@ export var DownloadsCommon = {
|
||||
* - "confirmBlock" to delete the blocked data permanently.
|
||||
* - "cancel" to do nothing and cancel the operation.
|
||||
*/
|
||||
async confirmUnblockDownload({ verdict, window, dialogType }) {
|
||||
async confirmUnblockDownload({
|
||||
verdict,
|
||||
becauseBlockedByReputationCheck,
|
||||
window,
|
||||
dialogType,
|
||||
}) {
|
||||
let s = DownloadsCommon.strings;
|
||||
|
||||
// All the dialogs have an action button and a cancel button, while only
|
||||
@@ -685,12 +711,18 @@ export var DownloadsCommon = {
|
||||
}
|
||||
|
||||
let message;
|
||||
let tip = s.unblockTip2;
|
||||
switch (verdict) {
|
||||
case lazy.Downloads.Error.BLOCK_VERDICT_UNCOMMON:
|
||||
message = s.unblockTypeUncommon2;
|
||||
break;
|
||||
case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
|
||||
if (becauseBlockedByReputationCheck) {
|
||||
message = s.unblockTypePotentiallyUnwanted2;
|
||||
} else {
|
||||
message = s.unblockTypeContentAnalysisWarn;
|
||||
tip = s.unblockContentAnalysisTip;
|
||||
}
|
||||
break;
|
||||
case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
|
||||
message = s.unblockInsecure2;
|
||||
@@ -700,7 +732,7 @@ export var DownloadsCommon = {
|
||||
message = s.unblockTypeMalware;
|
||||
break;
|
||||
}
|
||||
message += "\n\n" + s.unblockTip2;
|
||||
message += "\n\n" + tip;
|
||||
|
||||
Services.ww.registerNotification(function onOpen(subj, topic) {
|
||||
if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
|
||||
@@ -860,7 +892,10 @@ DownloadsDataCtor.prototype = {
|
||||
download,
|
||||
DownloadsCommon.stateOfDownload(download)
|
||||
);
|
||||
if (download.error?.becauseBlockedByReputationCheck) {
|
||||
if (
|
||||
download.error?.becauseBlockedByReputationCheck ||
|
||||
download.error?.becauseBlockedByContentAnalysis
|
||||
) {
|
||||
this._notifyDownloadEvent("error");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -770,7 +770,10 @@ DownloadsViewUI.DownloadElementShell.prototype = {
|
||||
lazy.DownloadsCommon.strings.stateBlockedParentalControls
|
||||
);
|
||||
this.hideButton();
|
||||
} else if (this.download.error.becauseBlockedByReputationCheck) {
|
||||
} else if (
|
||||
this.download.error.becauseBlockedByReputationCheck ||
|
||||
this.download.error.becauseBlockedByContentAnalysis
|
||||
) {
|
||||
verdict = this.download.error.reputationCheckVerdict;
|
||||
let hover = "";
|
||||
if (!this.download.hasBlockedData) {
|
||||
@@ -882,7 +885,8 @@ DownloadsViewUI.DownloadElementShell.prototype = {
|
||||
let s = lazy.DownloadsCommon.strings;
|
||||
if (
|
||||
!this.download.error ||
|
||||
!this.download.error.becauseBlockedByReputationCheck
|
||||
(!this.download.error.becauseBlockedByReputationCheck &&
|
||||
!this.download.error.becauseBlockedByContentAnalysis)
|
||||
) {
|
||||
return [null, null];
|
||||
}
|
||||
@@ -895,14 +899,37 @@ DownloadsViewUI.DownloadElementShell.prototype = {
|
||||
[s.unblockInsecure2, s.unblockTip2],
|
||||
];
|
||||
case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
|
||||
if (this.download.error.becauseBlockedByReputationCheck) {
|
||||
return [
|
||||
s.blockedPotentiallyUnwanted,
|
||||
[s.unblockTypePotentiallyUnwanted2, s.unblockTip2],
|
||||
];
|
||||
}
|
||||
if (!this.download.error.becauseBlockedByContentAnalysis) {
|
||||
// We expect one of becauseBlockedByReputationCheck or
|
||||
// becauseBlockedByContentAnalysis to be true; if not,
|
||||
// fall through to the error case.
|
||||
break;
|
||||
}
|
||||
return [
|
||||
s.warnedByContentAnalysis,
|
||||
[s.unblockTypeContentAnalysisWarn, s.unblockContentAnalysisWarnTip],
|
||||
];
|
||||
case lazy.Downloads.Error.BLOCK_VERDICT_MALWARE:
|
||||
if (this.download.error.becauseBlockedByReputationCheck) {
|
||||
return [s.blockedMalware, [s.unblockTypeMalware, s.unblockTip2]];
|
||||
|
||||
case lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM:
|
||||
}
|
||||
if (!this.download.error.becauseBlockedByContentAnalysis) {
|
||||
// We expect one of becauseBlockedByReputationCheck or
|
||||
// becauseBlockedByContentAnalysis to be true; if not,
|
||||
// fall through to the error case.
|
||||
break;
|
||||
}
|
||||
return [
|
||||
s.blockedByContentAnalysis,
|
||||
[s.unblockContentAnalysis1, s.unblockContentAnalysis2],
|
||||
];
|
||||
case lazy.Downloads.Error.BLOCK_VERDICT_DOWNLOAD_SPAM: {
|
||||
let title = {
|
||||
id: "downloads-files-not-downloaded",
|
||||
args: {
|
||||
@@ -915,6 +942,7 @@ DownloadsViewUI.DownloadElementShell.prototype = {
|
||||
};
|
||||
return [{ l10n: title }, [{ l10n: details }, null]];
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
"Unexpected reputationCheckVerdict: " +
|
||||
this.download.error.reputationCheckVerdict
|
||||
@@ -944,6 +972,8 @@ DownloadsViewUI.DownloadElementShell.prototype = {
|
||||
confirmUnblock(window, dialogType) {
|
||||
lazy.DownloadsCommon.confirmUnblockDownload({
|
||||
verdict: this.download.error.reputationCheckVerdict,
|
||||
becauseBlockedByReputationCheck:
|
||||
this.download.error.becauseBlockedByReputationCheck,
|
||||
window,
|
||||
dialogType,
|
||||
})
|
||||
@@ -990,6 +1020,7 @@ DownloadsViewUI.DownloadElementShell.prototype = {
|
||||
case lazy.DownloadsCommon.DOWNLOAD_FINISHED:
|
||||
return "downloadsCmd_open";
|
||||
case lazy.DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL:
|
||||
case lazy.DownloadsCommon.DOWNLOAD_BLOCKED_CONTENT_ANALYSIS:
|
||||
return "downloadsCmd_openReferrer";
|
||||
case lazy.DownloadsCommon.DOWNLOAD_DIRTY:
|
||||
return "downloadsCmd_showBlockedInfo";
|
||||
|
||||
@@ -41,9 +41,10 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.download-state:not([state="6"],/* Blocked (parental) */
|
||||
[state="8"],/* Blocked (dirty) */
|
||||
[state="9"] /* Blocked (policy) */)
|
||||
.download-state:not([state="6"], /* Blocked (parental) */
|
||||
[state="8"], /* Blocked (dirty) */
|
||||
[state="9"], /* Blocked (policy) */
|
||||
[state="10"] /* Blocked (content analysis) */)
|
||||
.downloadBlockedBadge,
|
||||
|
||||
.download-state:not([state="-1"],/* Starting (initial) */
|
||||
|
||||
@@ -1710,6 +1710,14 @@ var DownloadsBlockedSubview = {
|
||||
let e = this.elements;
|
||||
let s = DownloadsCommon.strings;
|
||||
|
||||
e.deleteButton.hidden =
|
||||
download.error?.becauseBlockedByContentAnalysis &&
|
||||
download.error?.reputationCheckVerdict === "Malware";
|
||||
|
||||
e.unblockButton.hidden =
|
||||
download.error?.becauseBlockedByContentAnalysis &&
|
||||
download.error?.reputationCheckVerdict === "Malware";
|
||||
|
||||
title.l10n
|
||||
? document.l10n.setAttributes(e.title, title.l10n.id, title.l10n.args)
|
||||
: (e.title.textContent = title);
|
||||
|
||||
@@ -34,6 +34,8 @@ blockedMalware=This file contains a virus or malware.
|
||||
blockedPotentiallyUnwanted=This file may harm your computer.
|
||||
blockedPotentiallyInsecure=File not downloaded: Potential security risk.
|
||||
blockedUncommon2=This file is not commonly downloaded.
|
||||
blockedByContentAnalysis=This file was blocked by your organization.
|
||||
warnedByContentAnalysis=This download may be unsafe and requires confirmation.
|
||||
|
||||
# LOCALIZATION NOTE (fileMovedOrMissing):
|
||||
# Displayed when a complete download which is not at the original folder.
|
||||
@@ -46,8 +48,10 @@ fileDeleted=File deleted
|
||||
|
||||
# LOCALIZATION NOTE (unblockHeaderUnblock, unblockHeaderOpen,
|
||||
# unblockTypeMalware, unblockTypePotentiallyUnwanted2,
|
||||
# unblockTypeUncommon2, unblockTip2, unblockButtonOpen,
|
||||
# unblockButtonUnblock, unblockButtonConfirmBlock, unblockInsecure2):
|
||||
# unblockTypeContentAnalysisWarn, unblockTypeUncommon2,
|
||||
# unblockTip2, unblockContentAnalysisWarnTip, unblockButtonOpen,
|
||||
# unblockButtonUnblock, unblockButtonConfirmBlock, unblockInsecure2,
|
||||
# unblockContentAnalysis1, unblockContentAnalysis2, unblock):
|
||||
# These strings are displayed in the dialog shown when the user asks a blocked
|
||||
# download to be unblocked. The severity of the threat is expressed in
|
||||
# descending order by the unblockType strings, it is higher for files detected
|
||||
@@ -56,12 +60,17 @@ unblockHeaderUnblock=Are you sure you want to allow this download?
|
||||
unblockHeaderOpen=Are you sure you want to open this file?
|
||||
unblockTypeMalware=This file contains a virus or other malware that will harm your computer.
|
||||
unblockTypePotentiallyUnwanted2=This file is disguised as a helpful download, but it can make unexpected changes to your programs and settings.
|
||||
unblockTypeContentAnalysisWarn=Your organization uses data-loss prevention software that has flagged this content as unsafe.
|
||||
unblockTypeUncommon2=This file is not commonly downloaded and may not be safe to open. It may contain a virus or make unexpected changes to your programs and settings.
|
||||
unblockInsecure2=The download is offered over HTTP even though the current document was delivered over a secure HTTPS connection. If you proceed, the download may be corrupted or tampered with during the download process.
|
||||
unblockTip2=You can search for an alternate download source or try again later.
|
||||
unblockContentAnalysisWarnTip=You can choose to download it anyway.
|
||||
unblockButtonOpen=Open
|
||||
unblockButtonUnblock=Allow download
|
||||
unblockButtonConfirmBlock=Remove file
|
||||
unblockContentAnalysis1=Your organization uses data-loss prevention software that has blocked this download.
|
||||
unblockContentAnalysis2=This decision can only be overridden by your organization.
|
||||
unblockContentAnalysisTip=You may continue your download or cancel it.
|
||||
|
||||
# LOCALIZATION NOTE (sizeWithUnits):
|
||||
# %1$S is replaced with the size number, and %2$S with the measurement unit.
|
||||
|
||||
@@ -370,10 +370,13 @@ Download.prototype = {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.error && this.error.becauseBlockedByReputationCheck) {
|
||||
if (
|
||||
this.error?.becauseBlockedByReputationCheck ||
|
||||
this.error?.becauseBlockedByContentAnalysis
|
||||
) {
|
||||
return Promise.reject(
|
||||
new DownloadError({
|
||||
message: "Cannot start after being blocked by a reputation check.",
|
||||
message: "Cannot start after being blocked by a safety check.",
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -708,6 +711,10 @@ Download.prototype = {
|
||||
return this._promiseUnblock;
|
||||
}
|
||||
|
||||
if (this.error?.becauseBlockedByContentAnalysis) {
|
||||
this.respondToContentAnalysisWarnWithAllow();
|
||||
}
|
||||
|
||||
if (!this.hasBlockedData) {
|
||||
return Promise.reject(
|
||||
new Error("unblock may only be called on Downloads with blocked data.")
|
||||
@@ -716,7 +723,9 @@ Download.prototype = {
|
||||
|
||||
this._promiseUnblock = (async () => {
|
||||
try {
|
||||
if (this.target.partFilePath) {
|
||||
await IOUtils.move(this.target.partFilePath, this.target.path);
|
||||
}
|
||||
await this.target.refresh();
|
||||
} catch (ex) {
|
||||
await this.refresh();
|
||||
@@ -733,6 +742,44 @@ Download.prototype = {
|
||||
return this._promiseUnblock;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates that the download should be allowed. Will do nothing
|
||||
* if content analysis was not used.
|
||||
*/
|
||||
respondToContentAnalysisWarnWithAllow() {
|
||||
if (this.error?.contentAnalysisWarnRequestToken) {
|
||||
lazy.DownloadIntegration.getContentAnalysisService().respondToWarnDialog(
|
||||
this.error.contentAnalysisWarnRequestToken,
|
||||
true
|
||||
);
|
||||
this.error.contentAnalysisWarnRequestToken = undefined;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates that the download should be blocked. Will do nothing
|
||||
* if content analysis was not used.
|
||||
*/
|
||||
async respondToContentAnalysisWarnWithBlock() {
|
||||
if (this.error?.contentAnalysisWarnRequestToken) {
|
||||
lazy.DownloadIntegration.getContentAnalysisService().respondToWarnDialog(
|
||||
this.error.contentAnalysisWarnRequestToken,
|
||||
false
|
||||
);
|
||||
this.error.contentAnalysisWarnRequestToken = undefined;
|
||||
if (!this.target.partFilePath) {
|
||||
// Callers will be finalizing the download after this.
|
||||
// But if the download happened in place, we need to
|
||||
// remove the final target file.
|
||||
try {
|
||||
await this.saver.removeData(true);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirms that a blocked download should be cleaned up.
|
||||
*
|
||||
@@ -773,6 +820,9 @@ Download.prototype = {
|
||||
}
|
||||
|
||||
this._promiseConfirmBlock = (async () => {
|
||||
if (this.error?.becauseBlockedByContentAnalysis) {
|
||||
await this.respondToContentAnalysisWarnWithBlock();
|
||||
}
|
||||
// This call never throws exceptions. If the removal fails, the blocked
|
||||
// data remains stored on disk in the ".part" file.
|
||||
await this.saver.removeData();
|
||||
@@ -1854,7 +1904,8 @@ export var DownloadError = function (aProperties) {
|
||||
} else if (
|
||||
aProperties.becauseBlocked ||
|
||||
aProperties.becauseBlockedByParentalControls ||
|
||||
aProperties.becauseBlockedByReputationCheck
|
||||
aProperties.becauseBlockedByReputationCheck ||
|
||||
aProperties.becauseBlockedByContentAnalysis
|
||||
) {
|
||||
this.message = "Download blocked.";
|
||||
} else {
|
||||
@@ -1882,6 +1933,12 @@ export var DownloadError = function (aProperties) {
|
||||
this.becauseBlocked = true;
|
||||
this.becauseBlockedByReputationCheck = true;
|
||||
this.reputationCheckVerdict = aProperties.reputationCheckVerdict || "";
|
||||
} else if (aProperties.becauseBlockedByContentAnalysis) {
|
||||
this.becauseBlocked = true;
|
||||
this.becauseBlockedByContentAnalysis = true;
|
||||
this.contentAnalysisWarnRequestToken =
|
||||
aProperties.contentAnalysisWarnRequestToken;
|
||||
this.reputationCheckVerdict = aProperties.reputationCheckVerdict;
|
||||
} else if (aProperties.becauseBlocked) {
|
||||
this.becauseBlocked = true;
|
||||
}
|
||||
@@ -1939,6 +1996,11 @@ DownloadError.prototype = {
|
||||
*/
|
||||
becauseBlockedByReputationCheck: false,
|
||||
|
||||
/**
|
||||
* Indicates the download was blocked by a local content analysis tool.
|
||||
*/
|
||||
becauseBlockedByContentAnalysis: false,
|
||||
|
||||
/**
|
||||
* If becauseBlockedByReputationCheck is true, indicates the detailed reason
|
||||
* why the download was blocked, according to the "BLOCK_VERDICT_" constants.
|
||||
@@ -2000,6 +2062,7 @@ DownloadError.fromSerializable = function (aSerializable) {
|
||||
property != "becauseBlocked" &&
|
||||
property != "becauseBlockedByParentalControls" &&
|
||||
property != "becauseBlockedByReputationCheck" &&
|
||||
property != "becauseBlockedByContentAnalysis" &&
|
||||
property != "reputationCheckVerdict"
|
||||
);
|
||||
|
||||
@@ -2573,6 +2636,8 @@ DownloadCopySaver.prototype = {
|
||||
* @rejects DownloadError if the download should be blocked.
|
||||
*/
|
||||
async _checkReputationAndMove(aSetPropertiesFn) {
|
||||
const REPUTATION_CHECK = 0;
|
||||
const CONTENT_ANALYSIS_CHECK = 1;
|
||||
/**
|
||||
* Maps nsIApplicationReputationService verdicts with the DownloadError ones.
|
||||
*/
|
||||
@@ -2587,25 +2652,108 @@ DownloadCopySaver.prototype = {
|
||||
DownloadError.BLOCK_VERDICT_MALWARE,
|
||||
};
|
||||
|
||||
let checkContentAnalysis = download => {
|
||||
// Start an asynchronous content analysis check.
|
||||
return lazy.DownloadIntegration.shouldBlockForContentAnalysis(
|
||||
download
|
||||
).then(result => {
|
||||
result.check = CONTENT_ANALYSIS_CHECK;
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
let checkReputation = download => {
|
||||
// Start an asynchronous reputation check.
|
||||
return lazy.DownloadIntegration.shouldBlockForReputationCheck(
|
||||
download
|
||||
).then(result => {
|
||||
result.check = REPUTATION_CHECK;
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
let hasMostRestrictiveResult = ([result1, result2]) => {
|
||||
// Verdicts are sorted from least-to-most restrictive. However, a result that
|
||||
// shouldBlock is always more restrictive than one that does not. Since
|
||||
// reputation allows shouldBlock to be overridden by prefs but content
|
||||
// analysis does not, we need to be careful of that.
|
||||
if (result1.shouldBlock && !result2.shouldBlock) {
|
||||
return result1;
|
||||
}
|
||||
if (result2.shouldBlock) {
|
||||
return result2;
|
||||
}
|
||||
// Verdicts are in a pre-defined order (see nsIApplicationReputationService),
|
||||
// so find the most restrictive one.
|
||||
const verdictToRestrictiveness = {
|
||||
[Ci.nsIApplicationReputationService.VERDICT_SAFE]: 0,
|
||||
[Ci.nsIApplicationReputationService.VERDICT_POTENTIALLY_UNWANTED]: 1,
|
||||
[Ci.nsIApplicationReputationService.VERDICT_UNCOMMON]: 2,
|
||||
[Ci.nsIApplicationReputationService.VERDICT_DANGEROUS_HOST]: 3,
|
||||
[Ci.nsIApplicationReputationService.VERDICT_DANGEROUS]: 4,
|
||||
};
|
||||
return verdictToRestrictiveness[result1.verdict] >
|
||||
verdictToRestrictiveness[result2.verdict]
|
||||
? result1
|
||||
: result2;
|
||||
};
|
||||
|
||||
let download = this.download;
|
||||
let targetPath = this.download.target.path;
|
||||
let partFilePath = this.download.target.partFilePath;
|
||||
|
||||
let { shouldBlock, verdict } =
|
||||
await lazy.DownloadIntegration.shouldBlockForReputationCheck(download);
|
||||
let downloadErrorVerdict = kVerdictMap[verdict] || "";
|
||||
if (shouldBlock) {
|
||||
let reputationPromise = checkReputation(download);
|
||||
let caPromise = checkContentAnalysis(download);
|
||||
|
||||
let permissionResult = await Promise.any([
|
||||
reputationPromise,
|
||||
caPromise,
|
||||
]).then(async result => {
|
||||
// If the first result is the most restrictive one, we can return it
|
||||
// immediately.
|
||||
if (
|
||||
result.shouldBlock &&
|
||||
result.verdict == Ci.nsIApplicationReputationService.VERDICT_DANGEROUS
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
// Otherwise wait for both results and compare them.
|
||||
return await Promise.all([reputationPromise, caPromise]).then(
|
||||
hasMostRestrictiveResult
|
||||
);
|
||||
});
|
||||
|
||||
let downloadErrorVerdict = kVerdictMap[permissionResult.verdict] || "";
|
||||
permissionResult.verdict = downloadErrorVerdict;
|
||||
if (permissionResult.shouldBlock) {
|
||||
if (permissionResult.check === REPUTATION_CHECK) {
|
||||
Glean.downloads.userActionOnBlockedDownload[
|
||||
downloadErrorVerdict
|
||||
].accumulateSingleSample(0);
|
||||
}
|
||||
|
||||
let newProperties = { progress: 100, hasPartialData: false };
|
||||
|
||||
// We will remove the potentially dangerous file if instructed by
|
||||
// DownloadIntegration. We will always remove the file when the
|
||||
// download did not use a partial file path, meaning it
|
||||
// currently has its final filename.
|
||||
if (!lazy.DownloadIntegration.shouldKeepBlockedData() || !partFilePath) {
|
||||
// currently has its final filename, or if it was blocked by
|
||||
// content analysis.
|
||||
let neverRemoveData = false;
|
||||
let alwaysRemoveData = false;
|
||||
if (permissionResult.check === CONTENT_ANALYSIS_CHECK) {
|
||||
if (downloadErrorVerdict === DownloadError.BLOCK_VERDICT_MALWARE) {
|
||||
alwaysRemoveData = true;
|
||||
} else {
|
||||
neverRemoveData = true;
|
||||
}
|
||||
}
|
||||
let removeData =
|
||||
!neverRemoveData &&
|
||||
(alwaysRemoveData ||
|
||||
!lazy.DownloadIntegration.shouldKeepBlockedData() ||
|
||||
!partFilePath);
|
||||
if (removeData) {
|
||||
await this.removeData(!partFilePath);
|
||||
} else {
|
||||
newProperties.hasBlockedData = true;
|
||||
@@ -2613,10 +2761,19 @@ DownloadCopySaver.prototype = {
|
||||
|
||||
aSetPropertiesFn(newProperties);
|
||||
|
||||
if (permissionResult.check == REPUTATION_CHECK) {
|
||||
throw new DownloadError({
|
||||
becauseBlockedByReputationCheck: true,
|
||||
reputationCheckVerdict: downloadErrorVerdict,
|
||||
});
|
||||
} else {
|
||||
throw new DownloadError({
|
||||
becauseBlockedByContentAnalysis: true,
|
||||
reputationCheckVerdict: downloadErrorVerdict,
|
||||
contentAnalysisWarnRequestToken:
|
||||
permissionResult.contentAnalysisWarnRequestToken,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (partFilePath) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import { DownloadList } from "resource://gre/modules/DownloadList.sys.mjs";
|
||||
import { DownloadError } from "resource://gre/modules/DownloadCore.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
@@ -32,6 +33,7 @@ const METADATA_STATE_CANCELED = 3;
|
||||
const METADATA_STATE_PAUSED = 4;
|
||||
const METADATA_STATE_BLOCKED_PARENTAL = 6;
|
||||
const METADATA_STATE_DIRTY = 8;
|
||||
const METADATA_STATE_BLOCKED_CONTENT_ANALYSIS = 9;
|
||||
|
||||
/**
|
||||
* Provides methods to retrieve downloads from previous sessions and store
|
||||
@@ -125,6 +127,12 @@ export let DownloadHistory = {
|
||||
state = METADATA_STATE_BLOCKED_PARENTAL;
|
||||
} else if (download.error.becauseBlockedByReputationCheck) {
|
||||
state = METADATA_STATE_DIRTY;
|
||||
} else if (download.error.becauseBlockedByContentAnalysis) {
|
||||
state =
|
||||
download.error.reputationCheckVerdict ===
|
||||
DownloadError.BLOCK_VERDICT_MALWARE
|
||||
? METADATA_STATE_BLOCKED_CONTENT_ANALYSIS
|
||||
: METADATA_STATE_DIRTY;
|
||||
} else {
|
||||
state = METADATA_STATE_FAILED;
|
||||
}
|
||||
@@ -433,6 +441,11 @@ class HistoryDownload {
|
||||
this.error = { message: "History download failed." };
|
||||
} else if (metaData.state == METADATA_STATE_BLOCKED_PARENTAL) {
|
||||
this.error = { becauseBlockedByParentalControls: true };
|
||||
} else if (metaData.state == METADATA_STATE_BLOCKED_CONTENT_ANALYSIS) {
|
||||
this.error = {
|
||||
becauseBlockedByContentAnalysis: true,
|
||||
reputationCheckVerdict: metaData.reputationCheckVerdict || "",
|
||||
};
|
||||
} else if (metaData.state == METADATA_STATE_DIRTY) {
|
||||
this.error = {
|
||||
becauseBlockedByReputationCheck: true,
|
||||
@@ -503,6 +516,17 @@ class HistoryDownload {
|
||||
this.deleted = true;
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method mimicks the "respondToContentAnalysisWarnWithBlock"
|
||||
* method of session downloads.
|
||||
*/
|
||||
async respondToContentAnalysisWarnWithBlock() {
|
||||
// A history download cannot be pending a content
|
||||
// analysis response (since it doesn't persist after Firefox
|
||||
// is closed), so just do nothing.
|
||||
console.warn("attempted to block via Content Analysis a history download");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,6 +96,7 @@ const kSaveDelayMs = 1500;
|
||||
*/
|
||||
const kObserverTopics = [
|
||||
"quit-application-requested",
|
||||
"quit-application-granted",
|
||||
"offline-requested",
|
||||
"last-pb-context-exiting",
|
||||
"last-pb-context-exited",
|
||||
@@ -423,6 +424,151 @@ export var DownloadIntegration = {
|
||||
});
|
||||
},
|
||||
|
||||
getContentAnalysisService() {
|
||||
// Do not use a lazy service getter for this, because tests set up different mocks,
|
||||
// so if multiple tests run that call into this we can end up calling into an old mock.
|
||||
return Cc["@mozilla.org/contentanalysis;1"].getService(
|
||||
Ci.nsIContentAnalysis
|
||||
);
|
||||
},
|
||||
|
||||
async shouldBlockForContentAnalysis(download) {
|
||||
const contentAnalysis = this.getContentAnalysisService();
|
||||
|
||||
if (!contentAnalysis.isActive) {
|
||||
return {
|
||||
verdict: Ci.nsIApplicationReputationService.VERDICT_SAFE,
|
||||
shouldBlock: false,
|
||||
};
|
||||
}
|
||||
|
||||
// For PDF files loaded in pdf.js the originalUrl is the original URL
|
||||
// where the PDF was loaded from, and the url is the URL of the pdf.js
|
||||
// resource.
|
||||
let downloadUrl = download.source.originalUrl ?? download.source.url;
|
||||
let resources = [
|
||||
{
|
||||
url: downloadUrl,
|
||||
type: Ci.nsIClientDownloadResource.DOWNLOAD_URL,
|
||||
},
|
||||
];
|
||||
|
||||
let redirects = download.saver.getRedirects();
|
||||
if (redirects) {
|
||||
for (let redirect of redirects) {
|
||||
resources.push({
|
||||
url: redirect.referrerURI,
|
||||
type: Ci.nsIClientDownloadResource.DOWNLOAD_REDIRECT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// source.referrerInfo is a string or nsIReferrerInfo that
|
||||
// represents the download referrer. May be null.
|
||||
if (download.source.referrerInfo) {
|
||||
const url =
|
||||
download.source.referrerInfo instanceof Ci.nsIReferrerInfo
|
||||
? download.source.referrerInfo.originalReferrer?.spec
|
||||
: download.source.referrerInfo;
|
||||
if (url) {
|
||||
resources.push({
|
||||
url,
|
||||
type: Ci.nsIClientDownloadResource.TAB_URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let url = lazy.NetUtil.newURI(downloadUrl);
|
||||
let fileNameForDisplay = download.target.path;
|
||||
try {
|
||||
// Try to get a prettier name
|
||||
let file = new lazy.FileUtils.File(download.target.path);
|
||||
fileNameForDisplay = file.displayName;
|
||||
} catch (ex) {
|
||||
// oh well
|
||||
}
|
||||
const requestToken = Services.uuid.generateUUID().toString();
|
||||
let warnResponseObserver = undefined;
|
||||
// Set up a separate promise to wait specifically for a WARN
|
||||
// response (if it comes) while we also wait for a final response.
|
||||
// This is necessary because if the agent sends a WARN response,
|
||||
// it doesn't count as a real response, and the Content Analysis code
|
||||
// won't respond to the callback until respondToWarnDialog() is called.
|
||||
const warnResultPromise = new Promise(resolve => {
|
||||
warnResponseObserver = function (subject, topic, _data) {
|
||||
if (topic == "dlp-response") {
|
||||
/** @type nsIContentAnalysisResponse */
|
||||
let response = subject;
|
||||
if (
|
||||
response.requestToken === requestToken &&
|
||||
response.action === Ci.nsIContentAnalysisResponse.eWarn
|
||||
) {
|
||||
resolve({
|
||||
isContentAnalysisWarn: true,
|
||||
verdict:
|
||||
Ci.nsIApplicationReputationService.VERDICT_POTENTIALLY_UNWANTED,
|
||||
contentAnalysisWarnRequestToken: requestToken,
|
||||
shouldBlock: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
Services.obs.addObserver(warnResponseObserver, "dlp-response");
|
||||
});
|
||||
let finalResultPromise = contentAnalysis
|
||||
.analyzeContentRequests(
|
||||
[
|
||||
{
|
||||
analysisType: Ci.nsIContentAnalysisRequest.eFileDownloaded,
|
||||
operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eDownload,
|
||||
fileNameForDisplay,
|
||||
// "Save As" downloads do not have a browsing context
|
||||
reason:
|
||||
download.source.browsingContextId === 0
|
||||
? Ci.nsIContentAnalysisRequest.eSaveAsDownload
|
||||
: Ci.nsIContentAnalysisRequest.eNormalDownload,
|
||||
resources,
|
||||
requestToken,
|
||||
url,
|
||||
filePath: download.target.path,
|
||||
// When doing a download analysis, the Content Analysis code won't
|
||||
// display dialogs in the window, but the code still wants a
|
||||
// content window and will get the topChromeWindow to show
|
||||
// a notification.
|
||||
windowGlobalParent: BrowsingContext.get(
|
||||
download.source.browsingContextId
|
||||
)?.topWindowContext,
|
||||
sha256Digest: download.saver.getSha256Hash(),
|
||||
},
|
||||
],
|
||||
/* autoAcknowledge*/ true
|
||||
)
|
||||
.then(response => {
|
||||
return {
|
||||
verdict: response.shouldAllowContent
|
||||
? Ci.nsIApplicationReputationService.VERDICT_SAFE
|
||||
: Ci.nsIApplicationReputationService.VERDICT_DANGEROUS,
|
||||
shouldBlock: !response.shouldAllowContent,
|
||||
contentAnalysisWarnRequestToken: undefined,
|
||||
};
|
||||
});
|
||||
try {
|
||||
let finalOrWarnResult = await Promise.race([
|
||||
finalResultPromise,
|
||||
warnResultPromise,
|
||||
]);
|
||||
return finalOrWarnResult;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
verdict: Ci.nsIApplicationReputationService.VERDICT_DANGEROUS,
|
||||
shouldBlock: true,
|
||||
};
|
||||
} finally {
|
||||
Services.obs.removeObserver(warnResponseObserver, "dlp-response");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether downloaded files should be marked as coming from
|
||||
* Internet Zone.
|
||||
@@ -961,6 +1107,15 @@ var DownloadObserver = {
|
||||
*/
|
||||
_privateInProgressDownloads: new Set(),
|
||||
|
||||
/**
|
||||
* Set of downloads that have finished but have gotten a content analysis
|
||||
* WARN response. These downloads need to be canceled when quitting, because
|
||||
* the next time we start Firefox the content analysis agent may not have
|
||||
* the same context as the one that was running when it analyzed the
|
||||
* download.
|
||||
*/
|
||||
_contentAnalysisWarnInProgressDownloads: new Set(),
|
||||
|
||||
/**
|
||||
* Set that contains the downloads that have been canceled when going offline
|
||||
* or to sleep. These are started again when returning online or waking. This
|
||||
@@ -991,6 +1146,11 @@ var DownloadObserver = {
|
||||
},
|
||||
onDownloadChanged: aDownload => {
|
||||
if (aDownload.stopped) {
|
||||
if (aDownload.error?.contentAnalysisWarnRequestToken) {
|
||||
this._contentAnalysisWarnInProgressDownloads.add(aDownload);
|
||||
} else {
|
||||
this._contentAnalysisWarnInProgressDownloads.delete(aDownload);
|
||||
}
|
||||
downloadsSet.delete(aDownload);
|
||||
} else {
|
||||
downloadsSet.add(aDownload);
|
||||
@@ -998,6 +1158,7 @@ var DownloadObserver = {
|
||||
},
|
||||
onDownloadRemoved: aDownload => {
|
||||
downloadsSet.delete(aDownload);
|
||||
this._contentAnalysisWarnInProgressDownloads.delete(aDownload);
|
||||
// The download must also be removed from the canceled when offline set.
|
||||
this._canceledOfflineDownloads.delete(aDownload);
|
||||
},
|
||||
@@ -1065,12 +1226,41 @@ var DownloadObserver = {
|
||||
observe: function DO_observe(aSubject, aTopic, aData) {
|
||||
let downloadsCount;
|
||||
switch (aTopic) {
|
||||
case "quit-application-requested":
|
||||
case "quit-application-requested": {
|
||||
downloadsCount =
|
||||
this._publicInProgressDownloads.size +
|
||||
this._privateInProgressDownloads.size;
|
||||
this._privateInProgressDownloads.size +
|
||||
this._contentAnalysisWarnInProgressDownloads.size;
|
||||
this._confirmCancelDownloads(aSubject, downloadsCount, "ON_QUIT");
|
||||
break;
|
||||
}
|
||||
case "quit-application-granted": {
|
||||
let blockPromises = [];
|
||||
for (let download of this._contentAnalysisWarnInProgressDownloads) {
|
||||
blockPromises.push(
|
||||
(async () => {
|
||||
await download.respondToContentAnalysisWarnWithBlock();
|
||||
await download.finalize(true);
|
||||
})()
|
||||
);
|
||||
}
|
||||
if (blockPromises.length) {
|
||||
// Wait for all the downloads to be blocked (and the files deleted)
|
||||
// before proceeding with the quit.
|
||||
let promiseDone = false;
|
||||
Promise.all(blockPromises).finally(() => {
|
||||
promiseDone = true;
|
||||
});
|
||||
Services.tm.spinEventLoopUntil(
|
||||
"DownloadIntegration.sys.mjs:DI_observe_quit-application-granted",
|
||||
() => {
|
||||
return promiseDone;
|
||||
}
|
||||
);
|
||||
}
|
||||
this._contentAnalysisWarnInProgressDownloads.clear();
|
||||
break;
|
||||
}
|
||||
case "offline-requested":
|
||||
downloadsCount =
|
||||
this._publicInProgressDownloads.size +
|
||||
|
||||
@@ -20,6 +20,8 @@ interface nsIReferrerInfo;
|
||||
interface nsIApplicationReputationService : nsISupports {
|
||||
/**
|
||||
* Indicates the reason for the application reputation block.
|
||||
* These values should not be modified as they match the values in
|
||||
* ClientDownloadResponse.Verdict in csd.proto.
|
||||
*/
|
||||
const unsigned long VERDICT_SAFE = 0;
|
||||
const unsigned long VERDICT_DANGEROUS = 1;
|
||||
|
||||
Reference in New Issue
Block a user