From 5bb89780fa60db619478a36ed698030d37b99f94 Mon Sep 17 00:00:00 2001 From: Sebastian Streich Date: Tue, 30 Jun 2020 16:29:22 +0000 Subject: [PATCH] Bug 1614969 - Check download with MixedContentBlocker r=ckerschb Differential Revision: https://phabricator.services.mozilla.com/D73302 --- .../en-US/chrome/security/security.properties | 3 + dom/security/nsContentSecurityUtils.cpp | 75 ++++++++++++++ dom/security/nsContentSecurityUtils.h | 6 ++ .../test/mixedcontentblocker/browser.ini | 6 ++ .../browser_test_mixed_content_download.js | 98 +++++++++++++++++++ .../mixedcontentblocker/download_page.html | 35 +++++++ .../mixedcontentblocker/download_server.sjs | 9 ++ dom/security/test/moz.build | 1 + modules/libpref/init/StaticPrefList.yaml | 6 ++ .../exthandler/nsExternalHelperAppService.cpp | 12 ++- 10 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 dom/security/test/mixedcontentblocker/browser.ini create mode 100644 dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js create mode 100644 dom/security/test/mixedcontentblocker/download_page.html create mode 100644 dom/security/test/mixedcontentblocker/download_server.sjs diff --git a/dom/locales/en-US/chrome/security/security.properties b/dom/locales/en-US/chrome/security/security.properties index 833097cf8af7..d1ad8ca87959 100644 --- a/dom/locales/en-US/chrome/security/security.properties +++ b/dom/locales/en-US/chrome/security/security.properties @@ -45,6 +45,9 @@ InsecurePasswordsPresentOnIframe=Password fields present on an insecure (http:// LoadingMixedActiveContent2=Loading mixed (insecure) active content “%1$S” on a secure page 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”. + # 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. # LOCALIZATION NOTE: Do not translate "allow-top-navigation-by-user-activation", "allow-top-navigation", "sandbox" or "iframe" diff --git a/dom/security/nsContentSecurityUtils.cpp b/dom/security/nsContentSecurityUtils.cpp index 05c8258381cc..84b75c243048 100644 --- a/dom/security/nsContentSecurityUtils.cpp +++ b/dom/security/nsContentSecurityUtils.cpp @@ -22,6 +22,7 @@ #include "mozilla/ExtensionPolicyService.h" #include "mozilla/Logging.h" #include "mozilla/dom/Document.h" +#include "LoadInfo.h" #include "mozilla/StaticPrefs_extensions.h" #include "mozilla/StaticPrefs_dom.h" @@ -1039,3 +1040,77 @@ bool nsContentSecurityUtils::ValidateScriptFilename(const char* aFilename, // builds and return false to prevent execution in non-debug builds. return true; } + +/* static */ +void nsContentSecurityUtils::LogMessageToConsole(nsIHttpChannel* aChannel, + const char* aMsg) { + nsCOMPtr uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return; + } + + uint64_t windowID = 0; + rv = aChannel->GetTopLevelContentWindowId(&windowID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + if (!windowID) { + nsCOMPtr loadInfo = aChannel->LoadInfo(); + loadInfo->GetInnerWindowID(&windowID); + } + + nsAutoString localizedMsg; + nsAutoCString spec; + uri->GetSpec(spec); + AutoTArray params = {NS_ConvertUTF8toUTF16(spec)}; + rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eSECURITY_PROPERTIES, aMsg, params, localizedMsg); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsContentUtils::ReportToConsoleByWindowID( + localizedMsg, nsIScriptError::warningFlag, NS_LITERAL_CSTRING("Security"), + windowID, uri); +} + +/* static */ +bool nsContentSecurityUtils::IsDownloadAllowed( + nsIChannel* aChannel, const nsAutoCString& aMimeTypeGuess) { + MOZ_ASSERT(aChannel, "IsDownloadAllowed without channel?"); + if (!StaticPrefs::dom_block_download_insecure()) { + return true; + } + + nsCOMPtr loadInfo = aChannel->LoadInfo(); + if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { + return true; + } + + nsCOMPtr contentLocation; + aChannel->GetURI(getter_AddRefs(contentLocation)); + + nsCOMPtr loadingPrincipal = loadInfo->GetLoadingPrincipal(); + // Creating a fake Loadinfo that is just used for the MCB check. + nsCOMPtr secCheckLoadInfo = + new LoadInfo(loadingPrincipal, loadInfo->TriggeringPrincipal(), nullptr, + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, + nsIContentPolicy::TYPE_OTHER); + + int16_t decission = nsIContentPolicy::ACCEPT; + nsMixedContentBlocker::ShouldLoad(false, // aHadInsecureImageRedirect + contentLocation, // aContentLocation, + secCheckLoadInfo, // aLoadinfo + aMimeTypeGuess, // aMimeGuess, + &decission // aDecision + ); + if (decission == nsIContentPolicy::ACCEPT) { + return true; + } + nsCOMPtr httpChannel = do_QueryInterface(aChannel); + if (httpChannel) { + LogMessageToConsole(httpChannel, "MixedContentBlockedDownload"); + } + return false; +} \ No newline at end of file diff --git a/dom/security/nsContentSecurityUtils.h b/dom/security/nsContentSecurityUtils.h index e794927bc78c..2b347b9afb8c 100644 --- a/dom/security/nsContentSecurityUtils.h +++ b/dom/security/nsContentSecurityUtils.h @@ -50,6 +50,12 @@ class nsContentSecurityUtils { // If any of the two disallows framing, the channel will be cancelled. static void PerformCSPFrameAncestorAndXFOCheck(nsIChannel* aChannel); + // Helper function to Check if a Download is allowed; + static bool IsDownloadAllowed(nsIChannel* aChannel, + const nsAutoCString& aMimeTypeGuess); + // Logs an Error Message to the Console + static void LogMessageToConsole(nsIHttpChannel* aChannel, const char* aMsg); + #if defined(DEBUG) static void AssertAboutPageHasCSP(mozilla::dom::Document* aDocument); #endif diff --git a/dom/security/test/mixedcontentblocker/browser.ini b/dom/security/test/mixedcontentblocker/browser.ini new file mode 100644 index 000000000000..325cc074c133 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/browser.ini @@ -0,0 +1,6 @@ +[DEFAULT] +support-files = + download_page.html + download_server.sjs + +[browser_test_mixed_content_download.js] diff --git a/dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js b/dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js new file mode 100644 index 000000000000..462de73850d1 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/browser_test_mixed_content_download.js @@ -0,0 +1,98 @@ +let INSECURE_BASE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://example.com/" + ) + "download_page.html"; +let SECURE_BASE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "https://example.com/" + ) + "download_page.html"; + +function shouldPromptDownload() { + // Waits until the download Prompt is shown + 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" + ) { + resolve(); + info("Trying to close window"); + win.close(); + } else { + reject(); + } + }, win); + }, + }); + }); +} + +const CONSOLE_ERROR_MESSAGE = "was blocked because it was insecure."; + +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 runTest(url, link, check, decscription) { + let tab = BrowserTestUtils.addTab(gBrowser, url); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Checking: " + decscription); + + let checkPromise = check(); + // Click the Link to trigger the download + SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => { + content.document.getElementById(contentLink).click(); + }); + + await checkPromise; + + ok(true, decscription); + BrowserTestUtils.removeTab(tab); +} + +add_task(async function() { + await runTest( + INSECURE_BASE_URL, + "insecure", + shouldPromptDownload, + "Insecure -> Insecure should download" + ); + await runTest( + INSECURE_BASE_URL, + "secure", + shouldPromptDownload, + "Insecure -> Secure should download" + ); + await runTest( + SECURE_BASE_URL, + "insecure", + shouldConsoleError, + "Secure -> Insecure should Error" + ); + await runTest( + SECURE_BASE_URL, + "secure", + shouldPromptDownload, + "Secure -> Secure should Download" + ); +}); diff --git a/dom/security/test/mixedcontentblocker/download_page.html b/dom/security/test/mixedcontentblocker/download_page.html new file mode 100644 index 000000000000..a9f7b731fe7d --- /dev/null +++ b/dom/security/test/mixedcontentblocker/download_page.html @@ -0,0 +1,35 @@ + + + + + + Test for the download attribute + + + + hi + + + + diff --git a/dom/security/test/mixedcontentblocker/download_server.sjs b/dom/security/test/mixedcontentblocker/download_server.sjs new file mode 100644 index 000000000000..4560e84fc36d --- /dev/null +++ b/dom/security/test/mixedcontentblocker/download_server.sjs @@ -0,0 +1,9 @@ +// force the Browser to Show a Download Prompt + +function handleRequest(request, response) +{ + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Disposition", "attachment"); + response.setHeader("Content-Type", "application/octet-stream"); + response.write('🙈🙊🐵🙊'); +} diff --git a/dom/security/test/moz.build b/dom/security/test/moz.build index 536c674a18b3..f412486308ae 100644 --- a/dom/security/test/moz.build +++ b/dom/security/test/moz.build @@ -34,4 +34,5 @@ BROWSER_CHROME_MANIFESTS += [ 'csp/browser.ini', 'general/browser.ini', 'https-only/browser.ini', + 'mixedcontentblocker/browser.ini' ] diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index a67cf7ae90dc..1311649d692a 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -1506,6 +1506,12 @@ value: true mirror: always +# Block Insecure downloads from Secure Origins +- name: dom.block_download_insecure + type: bool + value: @IS_NIGHTLY_BUILD@ + mirror: always + # Block multiple window.open() per single event. - name: dom.block_multiple_popups type: bool diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp index 9440f4009fc8..b03c65650285 100644 --- a/uriloader/exthandler/nsExternalHelperAppService.cpp +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -46,6 +46,7 @@ #include "nsIRedirectHistoryEntry.h" #include "nsOSHelperAppService.h" #include "nsOSHelperAppServiceChild.h" +#include "nsContentSecurityUtils.h" // used to access our datastore of user-configured helper applications #include "nsIHandlerService.h" @@ -1557,6 +1558,14 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { nsCOMPtr aChannel = do_QueryInterface(request); nsresult rv; + nsAutoCString MIMEType; + mMimeInfo->GetMIMEType(MIMEType); + + if (!nsContentSecurityUtils::IsDownloadAllowed(aChannel, MIMEType)) { + mCanceled = true; + request->Cancel(NS_ERROR_ABORT); + return NS_OK; + } nsCOMPtr fileChan(do_QueryInterface(request)); mIsFileChannel = fileChan != nullptr; @@ -1577,7 +1586,6 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { if (mBrowsingContext) { mMaybeCloseWindowHelper = new MaybeCloseWindowHelper(mBrowsingContext); mMaybeCloseWindowHelper->SetShouldCloseWindow(mShouldCloseWindow); - nsCOMPtr props(do_QueryInterface(request, &rv)); // Determine whether a new window was opened specifically for this request if (props) { @@ -1666,8 +1674,6 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { bool alwaysAsk = true; mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk); - nsAutoCString MIMEType; - mMimeInfo->GetMIMEType(MIMEType); if (alwaysAsk) { // But we *don't* ask if this mimeInfo didn't come from // our user configuration datastore and the user has said