Bug 1877195: Expand mixed-content download protection to all http downloads, r=freddyb,Gijs,anti-tracking-reviewers,pbz

Differential Revision: https://phabricator.services.mozilla.com/D200267
This commit is contained in:
Christoph Kerschbaumer
2024-03-04 10:03:18 +00:00
parent c88bb69212
commit 967443a323
28 changed files with 386 additions and 57 deletions

View File

@@ -14,7 +14,7 @@ function triggerSave(aWindow, aCallback) {
let testBrowser = aWindow.gBrowser.selectedBrowser;
// This page sets a cookie if and only if a cookie does not exist yet
let testURI =
"http://mochi.test:8888/browser/browser/base/content/test/general/bug792517-2.html";
"https://example.com/browser/browser/base/content/test/general/bug792517-2.html";
BrowserTestUtils.startLoadingURIString(testBrowser, testURI);
BrowserTestUtils.browserLoaded(testBrowser, false, testURI).then(() => {
waitForFocus(function () {
@@ -132,7 +132,7 @@ function test() {
info("onExamineResponse with " + channel.URI.spec);
if (
channel.URI.spec !=
"http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.sjs"
"https://example.com/browser/browser/base/content/test/general/bug792517.sjs"
) {
info("returning");
return;
@@ -158,7 +158,7 @@ function test() {
info("onModifyRequest with " + channel.URI.spec);
if (
channel.URI.spec !=
"http://mochi.test:8888/browser/browser/base/content/test/general/bug792517.sjs"
"https://example.com/browser/browser/base/content/test/general/bug792517.sjs"
) {
return;
}

View File

@@ -36,7 +36,7 @@ function triggerSave(aWindow, aCallback) {
var fileName;
let testBrowser = aWindow.gBrowser.selectedBrowser;
let testURI =
"http://mochi.test:8888/browser/browser/base/content/test/general/navigating_window_with_download.html";
"https://example.com/browser/browser/base/content/test/general/navigating_window_with_download.html";
// Only observe the UTC dialog if it's enabled by pref
if (Services.prefs.getBoolPref(ALWAYS_ASK_PREF)) {

View File

@@ -14,7 +14,7 @@ add_task(async function () {
let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
BrowserTestUtils.startLoadingURIString(
gBrowser,
"http://mochi.test:8888/browser/browser/base/content/test/general/web_video.html"
"https://example.com/browser/browser/base/content/test/general/web_video.html"
);
await loadPromise;

View File

@@ -2,6 +2,6 @@
<html>
<head><title>This window will navigate while you're downloading something</title></head>
<body>
<iframe src="http://mochi.test:8888/browser/browser/base/content/test/general/unknownContentType_file.pif"></iframe>
<iframe src="https://example.com/browser/browser/base/content/test/general/unknownContentType_file.pif"></iframe>
</body>
</html>

View File

@@ -696,7 +696,7 @@ export var DownloadsCommon = {
message = s.unblockTypePotentiallyUnwanted2;
break;
case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
message = s.unblockInsecure2;
message = s.unblockInsecure3;
break;
default:
// Assume Downloads.Error.BLOCK_VERDICT_MALWARE

View File

@@ -888,7 +888,7 @@ DownloadsViewUI.DownloadElementShell.prototype = {
case lazy.Downloads.Error.BLOCK_VERDICT_INSECURE:
return [
s.blockedPotentiallyInsecure,
[s.unblockInsecure2, s.unblockTip2],
[s.unblockInsecure3, s.unblockTip2],
];
case lazy.Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
return [

View File

@@ -5,7 +5,7 @@
const TEST_ROOT = getRootDirectory(gTestPath).replace(
"chrome://mochitests/content",
"http://example.com"
"https://example.com"
);
var MockFilePicker = SpecialPowers.MockFilePicker;

View File

@@ -38,6 +38,7 @@ add_setup(async function () {
set: [
["privacy.firstparty.isolate", true],
["dom.security.https_first", false],
["dom.block_download_insecure", false],
],
});

View File

@@ -47,7 +47,7 @@ fileDeleted=File deleted
# LOCALIZATION NOTE (unblockHeaderUnblock, unblockHeaderOpen,
# unblockTypeMalware, unblockTypePotentiallyUnwanted2,
# unblockTypeUncommon2, unblockTip2, unblockButtonOpen,
# unblockButtonUnblock, unblockButtonConfirmBlock, unblockInsecure2):
# unblockButtonUnblock, unblockButtonConfirmBlock, unblockInsecure3):
# 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
@@ -57,7 +57,7 @@ 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.
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.
unblockInsecure3=You are trying to download this file on a connection thats not secure. If you continue, the file might be changed, used to steal your info or harm your device.
unblockTip2=You can search for an alternate download source or try again later.
unblockButtonOpen=Open
unblockButtonUnblock=Allow download

View File

@@ -44,7 +44,7 @@ LoadingMixedActiveContent2=Loading mixed (insecure) active content “%1$S” on
LoadingMixedDisplayContent2=Loading mixed (insecure) display content “%1$S” on a secure page
LoadingMixedDisplayObjectSubrequestDeprecation=Loading mixed (insecure) content “%1$S” within a plugin on a secure page is discouraged and will be blocked soon.
# LOCALIZATION NOTE: "%S" is the URI of the insecure mixed content download
MixedContentBlockedDownload = Blocked downloading insecure content “%S”.
BlockedInsecureDownload = We blocked a download thats not secure: “%S”.
# LOCALIZATION NOTE: Do not translate "allow-scripts", "allow-same-origin", "sandbox" or "iframe"
BothAllowScriptsAndSameOriginPresent=An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can remove its sandboxing.

View File

@@ -1670,37 +1670,25 @@ long nsContentSecurityUtils::ClassifyDownload(
nsCOMPtr<nsIURI> contentLocation;
aChannel->GetURI(getter_AddRefs(contentLocation));
nsCOMPtr<nsIPrincipal> loadingPrincipal = loadInfo->GetLoadingPrincipal();
if (!loadingPrincipal) {
loadingPrincipal = loadInfo->TriggeringPrincipal();
}
// Creating a fake Loadinfo that is just used for the MCB check.
nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new mozilla::net::LoadInfo(
loadingPrincipal, loadInfo->TriggeringPrincipal(), nullptr,
nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
nsIContentPolicy::TYPE_FETCH);
// Disable HTTPS-Only checks for that loadinfo. This is required because
// otherwise nsMixedContentBlocker::ShouldLoad would assume that the request
// is safe, because HTTPS-Only is handling it.
secCheckLoadInfo->SetHttpsOnlyStatus(nsILoadInfo::HTTPS_ONLY_EXEMPT);
if (StaticPrefs::dom_block_download_insecure()) {
// If we are not dealing with a potentially trustworthy origin, or a URI
// that is safe to be loaded like e.g. data:, then we block the load.
bool isInsecureDownload =
!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(
contentLocation) &&
!nsMixedContentBlocker::URISafeToBeLoadedInSecureContext(
contentLocation);
int16_t decission = nsIContentPolicy::ACCEPT;
nsMixedContentBlocker::ShouldLoad(false, // aHadInsecureImageRedirect
contentLocation, // aContentLocation,
secCheckLoadInfo, // aLoadinfo
false, // aReportError
&decission // aDecision
);
Telemetry::Accumulate(mozilla::Telemetry::MIXED_CONTENT_DOWNLOADS,
decission != nsIContentPolicy::ACCEPT);
Telemetry::Accumulate(mozilla::Telemetry::INSECURE_DOWNLOADS,
isInsecureDownload);
if (StaticPrefs::dom_block_download_insecure() &&
decission != nsIContentPolicy::ACCEPT) {
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
if (httpChannel) {
LogMessageToConsole(httpChannel, "MixedContentBlockedDownload");
if (isInsecureDownload) {
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
if (httpChannel) {
LogMessageToConsole(httpChannel, "BlockedInsecureDownload");
}
return nsITransfer::DOWNLOAD_POTENTIALLY_UNSAFE;
}
return nsITransfer::DOWNLOAD_POTENTIALLY_UNSAFE;
}
if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) {

View File

@@ -48,6 +48,16 @@ support-files = [
"file_gpc_server.sjs",
]
["browser_test_http_download.js"]
skip-if = [
"win11_2009", # Bug 1784764
"os == 'linux' && !debug",
]
support-files = [
"http_download_page.html",
"http_download_server.sjs"
]
["browser_test_referrer_loadInOtherProcess.js"]
["browser_test_report_blocking.js"]

View File

@@ -0,0 +1,275 @@
/* Any copyright is dedicated to the Public Domain.
* https://creativecommons.org/publicdomain/zero/1.0/ */
ChromeUtils.defineESModuleGetters(this, {
Downloads: "resource://gre/modules/Downloads.sys.mjs",
DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
});
const HandlerService = Cc[
"@mozilla.org/uriloader/handler-service;1"
].getService(Ci.nsIHandlerService);
const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
// Using insecure HTTP URL for a test cases around HTTP downloads
let INSECURE_BASE_URL =
getRootDirectory(gTestPath).replace(
"chrome://mochitests/content/",
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
"http://example.com/"
) + "http_download_page.html";
function promiseFocus() {
return new Promise(resolve => {
waitForFocus(resolve);
});
}
async function task_openPanel() {
await promiseFocus();
let promise = BrowserTestUtils.waitForPopupEvent(
DownloadsPanel.panel,
"shown"
);
DownloadsPanel.showPanel();
await promise;
}
const downloadMonitoringView = {
_listeners: [],
onDownloadAdded(download) {
for (let listener of this._listeners) {
listener(download);
}
this._listeners = [];
},
waitForDownload(listener) {
this._listeners.push(listener);
},
};
/**
* Waits until a download is triggered.
* Unless the always_ask_before_handling_new_types pref is true, the download
* will simply be saved, so resolve when the view is notified of the new
* download. Otherwise, it waits until a prompt is shown, selects the choosen
* <action>, then accepts the dialog
* @param [action] Which action to select, either:
* "handleInternally", "save" or "open".
* @returns {Promise} Resolved once done.
*/
function shouldTriggerDownload(action = "save") {
if (
Services.prefs.getBoolPref(
"browser.download.always_ask_before_handling_new_types"
)
) {
return new Promise((resolve, reject) => {
Services.wm.addListener({
onOpenWindow(xulWin) {
Services.wm.removeListener(this);
let win = xulWin.docShell.domWindow;
waitForFocus(() => {
if (
win.location ==
"chrome://mozapps/content/downloads/unknownContentType.xhtml"
) {
let dialog = win.document.getElementById("unknownContentType");
let button = dialog.getButton("accept");
let actionRadio = win.document.getElementById(action);
actionRadio.click();
button.disabled = false;
dialog.acceptDialog();
resolve();
} else {
reject();
}
}, win);
},
});
});
}
return new Promise(res => {
downloadMonitoringView.waitForDownload(res);
});
}
const CONSOLE_ERROR_MESSAGE = "We blocked a download thats not secure";
function shouldConsoleError() {
// Waits until CONSOLE_ERROR_MESSAGE was logged
return new Promise((resolve, reject) => {
function listener(msgObj) {
let text = msgObj.message;
if (text.includes(CONSOLE_ERROR_MESSAGE)) {
Services.console.unregisterListener(listener);
resolve();
}
}
Services.console.registerListener(listener);
});
}
async function resetDownloads() {
// Removes all downloads from the download List
const types = new Set();
let publicList = await Downloads.getList(Downloads.PUBLIC);
let downloads = await publicList.getAll();
for (let download of downloads) {
if (download.contentType) {
types.add(download.contentType);
}
publicList.remove(download);
await download.finalize(true);
}
if (types.size) {
// reset handlers for the contentTypes of any files previously downloaded
for (let type of types) {
const mimeInfo = MIMEService.getFromTypeAndExtension(type, "");
info("resetting handler for type: " + type);
HandlerService.remove(mimeInfo);
}
}
}
function shouldNotifyDownloadUI() {
return new Promise(res => {
downloadMonitoringView.waitForDownload(async aDownload => {
let { error } = aDownload;
if (
error.becauseBlockedByReputationCheck &&
error.reputationCheckVerdict == Downloads.Error.BLOCK_VERDICT_INSECURE
) {
// It's an insecure Download, now Check that it has been cleaned up properly
if ((await IOUtils.stat(aDownload.target.path)).size != 0) {
throw new Error(`Download target is not empty!`);
}
if ((await IOUtils.stat(aDownload.target.path)).size != 0) {
throw new Error(`Download partFile was not cleaned up properly`);
}
// Assert that the Referrer is presnt
if (!aDownload.source.referrerInfo) {
throw new Error("The Blocked download is missing the ReferrerInfo");
}
res(aDownload);
} else {
ok(false, "No error for download that was expected to error!");
}
});
});
}
async function runTest(url, link, checkFunction, description) {
await SpecialPowers.pushPrefEnv({
set: [["dom.block_download_insecure", true]],
});
await resetDownloads();
let tab = BrowserTestUtils.addTab(gBrowser, url);
gBrowser.selectedTab = tab;
let browser = gBrowser.getBrowserForTab(tab);
await BrowserTestUtils.browserLoaded(browser);
info("Checking: " + description);
let checkPromise = checkFunction();
// Click the Link to trigger the download
SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => {
content.document.getElementById(contentLink).click();
});
await checkPromise;
ok(true, description);
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
}
add_setup(async () => {
let list = await Downloads.getList(Downloads.ALL);
list.addView(downloadMonitoringView);
registerCleanupFunction(() => list.removeView(downloadMonitoringView));
});
// Test Blocking
add_task(async function test_blocking() {
for (let prefVal of [true, false]) {
await SpecialPowers.pushPrefEnv({
set: [["browser.download.always_ask_before_handling_new_types", prefVal]],
});
await runTest(
INSECURE_BASE_URL,
"http-link",
() =>
Promise.all([
shouldTriggerDownload(),
shouldNotifyDownloadUI(),
shouldConsoleError(),
]),
"Insecure (HTTP) toplevel -> Insecure (HTTP) download should Error"
);
await SpecialPowers.popPrefEnv();
}
});
// Test Manual Unblocking
add_task(async function test_manual_unblocking() {
for (let prefVal of [true, false]) {
await SpecialPowers.pushPrefEnv({
set: [["browser.download.always_ask_before_handling_new_types", prefVal]],
});
await runTest(
INSECURE_BASE_URL,
"http-link",
async () => {
let [, download] = await Promise.all([
shouldTriggerDownload(),
shouldNotifyDownloadUI(),
]);
await download.unblock();
Assert.equal(
download.error,
null,
"There should be no error after unblocking"
);
},
"A blocked download should succeed to download after a manual unblock"
);
await SpecialPowers.popPrefEnv();
}
});
// Test Unblock Download Visible
add_task(async function test_unblock_download_visible() {
for (let prefVal of [true, false]) {
await SpecialPowers.pushPrefEnv({
set: [["browser.download.always_ask_before_handling_new_types", prefVal]],
});
await promiseFocus();
await runTest(
INSECURE_BASE_URL,
"http-link",
async () => {
let panelHasOpened = BrowserTestUtils.waitForPopupEvent(
DownloadsPanel.panel,
"shown"
);
info("awaiting that the download is triggered and added to the list");
await Promise.all([shouldTriggerDownload(), shouldNotifyDownloadUI()]);
info("awaiting that the Download list shows itself");
await panelHasOpened;
DownloadsPanel.hidePanel();
ok(true, "The Download Panel should have opened on blocked download");
},
"A blocked download should open the download panel"
);
await SpecialPowers.popPrefEnv();
}
});

View File

@@ -0,0 +1,23 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for the download attribute</title>
</head>
<body>
hi
<script>
const host = window.location.host;
const path = location.pathname.replace("http_download_page.html","http_download_server.sjs");
const insecureLink = document.createElement("a");
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
insecureLink.href=`http://${host}/${path}`;
insecureLink.download="true";
insecureLink.id="http-link";
insecureLink.textContent="Not secure Link";
document.body.append(insecureLink);
</script>
</body>
</html>

View File

@@ -0,0 +1,20 @@
// force the Browser to Show a Download Prompt
function handleRequest(request, response) {
let type = "image/png";
let filename = "hello.png";
request.queryString.split("&").forEach(val => {
var [key, value] = val.split("=");
if (key == "type") {
type = value;
}
if (key == "name") {
filename = value;
}
});
response.setHeader("Cache-Control", "no-cache", false);
response.setHeader("Content-Disposition", `attachment; filename=${filename}`);
response.setHeader("Content-Type", type);
response.write("🙈🙊🐵🙊");
}

View File

@@ -155,7 +155,11 @@ async function setHttpsFirstAndOnlyPrefs(httpsFirst, httpsOnly) {
add_task(async function testBaseline() {
// Run with HTTPS-First and HTTPS-Only disabled
await setHttpsFirstAndOnlyPrefs(false, false);
await runTest("#insecure-link", HTTP_LINK, undefined);
await runTest(
"#insecure-link",
HTTP_LINK,
"We blocked a download thats not secure: “http://example.org/”."
);
await runTest("#secure-link", HTTPS_LINK, undefined);
});
@@ -169,7 +173,7 @@ add_task(async function testHttpsFirst() {
await runTest(
"#insecure-link",
HTTP_LINK,
"Blocked downloading insecure content “http://example.org/”."
"We blocked a download thats not secure: “http://example.org/”."
);
await runTest("#secure-link", HTTPS_LINK, undefined);
});
@@ -181,7 +185,7 @@ add_task(async function testHttpsOnly() {
await runTest(
"#insecure-link",
HTTP_LINK,
"Blocked downloading insecure content “http://example.org/”."
"We blocked a download thats not secure: “http://example.org/”."
);
await runTest("#secure-link", HTTPS_LINK, undefined);
});

View File

@@ -101,7 +101,7 @@ function shouldTriggerDownload(action = "save") {
});
}
const CONSOLE_ERROR_MESSAGE = "Blocked downloading insecure content";
const CONSOLE_ERROR_MESSAGE = "We blocked a download thats not secure";
function shouldConsoleError() {
// Waits until CONSOLE_ERROR_MESSAGE was logged

View File

@@ -3,7 +3,7 @@
var gTestRoot = getRootDirectory(gTestPath).replace(
"chrome://mochitests/content/",
"http://mochi.test:8888/"
"https://example.com/"
);
function getFile(aFilename) {

View File

@@ -7,6 +7,7 @@ skip-if = [
prefs = [
"security.mixed_content.upgrade_display_content=false",
"dom.security.https_first=false",
"dom.block_download_insecure=false",
]
support-files = [
"alloworigin.sjs",

View File

@@ -1,4 +1,4 @@
[iframe_sandbox_navigation_download_allow_downloads.sub.tentative.html]
[iframe_sandbox_navigation_download_allow_downloads.sub.tentative.https.html]
expected:
if (os == "linux") and not fission: [OK, TIMEOUT]
if (os == "android") and fission: [TIMEOUT, OK]

View File

@@ -102,6 +102,7 @@ add_task(async function testContextMenuSaveImage() {
set: [
["privacy.partition.network_state", networkIsolation],
["privacy.dynamic_firstparty.use_site", partitionPerSite],
["dom.block_download_insecure", false],
],
});
@@ -197,6 +198,7 @@ add_task(async function testContextMenuSaveVideo() {
set: [
["privacy.partition.network_state", networkIsolation],
["privacy.dynamic_firstparty.use_site", partitionPerSite],
["dom.block_download_insecure", false],
],
});

View File

@@ -1,6 +1,6 @@
<html>
<body>
<img src="http://example.net/browser/toolkit/components/antitracking/test/browser/raptor.jpg" id="image1">
<video src="http://example.net/browser/toolkit/components/antitracking/test/browser/file_video.ogv" id="video1"> </video>
<img src="https://example.net/browser/toolkit/components/antitracking/test/browser/raptor.jpg" id="image1">
<video src="https://example.net/browser/toolkit/components/antitracking/test/browser/file_video.ogv" id="video1"> </video>
</body>
</html>

View File

@@ -11,6 +11,8 @@
// Execution of common tests
Services.prefs.setBoolPref("dom.block_download_insecure", false);
// This is used in common_test_Download.js
// eslint-disable-next-line no-unused-vars
var gUseLegacySaver = true;

View File

@@ -4,8 +4,8 @@
"use strict";
const URL_PATH = "browser/toolkit/components/extensions/test/browser/data";
const TEST_URL = `http://example.com/${URL_PATH}/test_downloads_referrer.html`;
const DOWNLOAD_URL = `http://example.com/${URL_PATH}/test-download.txt`;
const TEST_URL = `https://example.com/${URL_PATH}/test_downloads_referrer.html`;
const DOWNLOAD_URL = `https://example.com/${URL_PATH}/test-download.txt`;
async function triggerSaveAs({ selector }) {
const contextMenu = window.document.getElementById("contentAreaContextMenu");

View File

@@ -5,7 +5,7 @@
const TESTROOT = getRootDirectory(gTestPath).replace(
"chrome://mochitests/content/",
"http://mochi.test:8888/"
"https://example.com/"
);
// Get a ref to the pdf we want to open.

View File

@@ -12347,14 +12347,14 @@
"n_values": 10,
"description": "How often would blocked mixed content be allowed if HSTS upgrades were allowed? 0=display/no-HSTS, 1=display/HSTS, 2=active/no-HSTS, 3=active/HSTS"
},
"MIXED_CONTENT_DOWNLOADS": {
"INSECURE_DOWNLOADS": {
"record_in_processes": ["main", "content"],
"products": ["firefox"],
"alert_emails": ["seceng-telemetry@mozilla.com", "sstreich@mozilla.com"],
"bug_numbers": [1646768],
"expires_in_version": "90",
"alert_emails": ["seceng-telemetry@mozilla.com", "ckerschb@mozilla.com"],
"bug_numbers": [1877195],
"expires_in_version": "130",
"kind": "boolean",
"description": "Accumulates how many downloads are mixed-content (True = The download is MixedContent, False= is not MixedContent)"
"description": "Accumulates how many downloads are insecure (True = The download is insecure, False= The download is secure)"
},
"MIXED_CONTENT_IMAGES": {
"record_in_processes": ["main", "content"],

View File

@@ -24,7 +24,10 @@ server.registerFile(`/${encodeURIComponent(TEST_FILE)}`, file);
*/
add_task(async function test_idn_blocklisted_char_not_escaped() {
await SpecialPowers.pushPrefEnv({
set: [["browser.download.always_ask_before_handling_new_types", false]],
set: [
["browser.download.always_ask_before_handling_new_types", false],
["dom.block_download_insecure", false],
],
});
info("Testing with " + TEST_URL);