diff --git a/build/pgo/server-locations.txt b/build/pgo/server-locations.txt index 564a320f8e9d..678c5f5f1ee8 100644 --- a/build/pgo/server-locations.txt +++ b/build/pgo/server-locations.txt @@ -305,6 +305,10 @@ https://127.0.0.3:433 privileged,cer https://badcertdomain.example.com:82 privileged,cert=badCertDomain https://mismatch.badcertdomain.example.com:443 privileged,cert=badCertDomain +# Hosts for HTTPS-First upgrades/downgrades +http://httpsfirst.com:80 privileged +https://httpsfirst.com:443 privileged,nocert + # Hosts for sha1 console warning tests https://sha1ee.example.com:443 privileged,cert=sha1_end_entity https://sha256ee.example.com:443 privileged,cert=sha256_end_entity diff --git a/docshell/base/nsDocShellLoadState.cpp b/docshell/base/nsDocShellLoadState.cpp index 605561586a50..7bf73ae4bd45 100644 --- a/docshell/base/nsDocShellLoadState.cpp +++ b/docshell/base/nsDocShellLoadState.cpp @@ -49,6 +49,7 @@ nsDocShellLoadState::nsDocShellLoadState( mInheritPrincipal = aLoadState.InheritPrincipal(); mPrincipalIsExplicit = aLoadState.PrincipalIsExplicit(); mForceAllowDataURI = aLoadState.ForceAllowDataURI(); + mIsExemptFromHTTPSOnlyMode = aLoadState.IsExemptFromHTTPSOnlyMode(); mOriginalFrameSrc = aLoadState.OriginalFrameSrc(); mIsFormSubmission = aLoadState.IsFormSubmission(); mLoadType = aLoadState.LoadType(); @@ -103,6 +104,7 @@ nsDocShellLoadState::nsDocShellLoadState(const nsDocShellLoadState& aOther) mPrincipalToInherit(aOther.mPrincipalToInherit), mPartitionedPrincipalToInherit(aOther.mPartitionedPrincipalToInherit), mForceAllowDataURI(aOther.mForceAllowDataURI), + mIsExemptFromHTTPSOnlyMode(aOther.mIsExemptFromHTTPSOnlyMode), mOriginalFrameSrc(aOther.mOriginalFrameSrc), mIsFormSubmission(aOther.mIsFormSubmission), mLoadType(aOther.mLoadType), @@ -144,6 +146,7 @@ nsDocShellLoadState::nsDocShellLoadState(nsIURI* aURI, uint64_t aLoadIdentifier) mPrincipalIsExplicit(false), mNotifiedBeforeUnloadListeners(false), mForceAllowDataURI(false), + mIsExemptFromHTTPSOnlyMode(false), mOriginalFrameSrc(false), mIsFormSubmission(false), mLoadType(LOAD_NORMAL), @@ -486,6 +489,15 @@ void nsDocShellLoadState::SetForceAllowDataURI(bool aForceAllowDataURI) { mForceAllowDataURI = aForceAllowDataURI; } +bool nsDocShellLoadState::IsExemptFromHTTPSOnlyMode() const { + return mIsExemptFromHTTPSOnlyMode; +} + +void nsDocShellLoadState::SetIsExemptFromHTTPSOnlyMode( + bool aIsExemptFromHTTPSOnlyMode) { + mIsExemptFromHTTPSOnlyMode = aIsExemptFromHTTPSOnlyMode; +} + bool nsDocShellLoadState::OriginalFrameSrc() const { return mOriginalFrameSrc; } void nsDocShellLoadState::SetOriginalFrameSrc(bool aOriginalFrameSrc) { @@ -933,6 +945,7 @@ DocShellLoadStateInit nsDocShellLoadState::Serialize() { loadState.InheritPrincipal() = mInheritPrincipal; loadState.PrincipalIsExplicit() = mPrincipalIsExplicit; loadState.ForceAllowDataURI() = mForceAllowDataURI; + loadState.IsExemptFromHTTPSOnlyMode() = mIsExemptFromHTTPSOnlyMode; loadState.OriginalFrameSrc() = mOriginalFrameSrc; loadState.IsFormSubmission() = mIsFormSubmission; loadState.LoadType() = mLoadType; diff --git a/docshell/base/nsDocShellLoadState.h b/docshell/base/nsDocShellLoadState.h index 087af8096f29..71411fd4cd32 100644 --- a/docshell/base/nsDocShellLoadState.h +++ b/docshell/base/nsDocShellLoadState.h @@ -131,6 +131,10 @@ class nsDocShellLoadState final { void SetForceAllowDataURI(bool aForceAllowDataURI); + bool IsExemptFromHTTPSOnlyMode() const; + + void SetIsExemptFromHTTPSOnlyMode(bool aIsExemptFromHTTPSOnlyMode); + bool OriginalFrameSrc() const; void SetOriginalFrameSrc(bool aOriginalFrameSrc); @@ -404,6 +408,10 @@ class nsDocShellLoadState final { // to a data URI will be allowed. bool mForceAllowDataURI; + // If this attribute is true, then the top-level navigaion + // will be exempt from HTTPS-Only-Mode upgrades. + bool mIsExemptFromHTTPSOnlyMode; + // If this attribute is true, this load corresponds to a frame // element loading its original src (or srcdoc) attribute. bool mOriginalFrameSrc; diff --git a/dom/ipc/DOMTypes.ipdlh b/dom/ipc/DOMTypes.ipdlh index aa51babf4e4c..819f5e0fd1fc 100644 --- a/dom/ipc/DOMTypes.ipdlh +++ b/dom/ipc/DOMTypes.ipdlh @@ -250,6 +250,7 @@ struct DocShellLoadStateInit nsIPrincipal PrincipalToInherit; nsIPrincipal PartitionedPrincipalToInherit; bool ForceAllowDataURI; + bool IsExemptFromHTTPSOnlyMode; bool OriginalFrameSrc; bool IsFormSubmission; uint32_t LoadType; diff --git a/dom/locales/en-US/chrome/security/security.properties b/dom/locales/en-US/chrome/security/security.properties index a2da66ca8ac1..d0044ce16d3a 100644 --- a/dom/locales/en-US/chrome/security/security.properties +++ b/dom/locales/en-US/chrome/security/security.properties @@ -138,6 +138,8 @@ HTTPSOnlyUpgradeRequest = Upgrading insecure request “%1$S” to use “%2$S HTTPSOnlyNoUpgradeException = Not upgrading insecure request “%1$S” because it is exempt. # LOCALIZATION NOTE: %1$S is the URL of the failed request; %2$S is an error-code. HTTPSOnlyFailedRequest = Upgrading insecure request “%1$S” failed. (%2$S) +# LOCALIZATION NOTE: %S is the URL of the failed request; +HTTPSOnlyFailedDowngradeAgain = Upgrading insecure request “%S” failed. Downgrading to “http” again. # LOCALIZATION NOTE: %S is the URL of the blocked request; IframeSandboxBlockedDownload = Download of “%S” was blocked because the triggering iframe has the sandbox flag set. diff --git a/dom/security/nsHTTPSOnlyUtils.cpp b/dom/security/nsHTTPSOnlyUtils.cpp index a4c62515d0c7..3a8e75f16698 100644 --- a/dom/security/nsHTTPSOnlyUtils.cpp +++ b/dom/security/nsHTTPSOnlyUtils.cpp @@ -44,6 +44,11 @@ bool nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(bool aFromPrivateWindow) { return false; } +/* static */ +bool nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled() { + return mozilla::StaticPrefs::dom_security_https_only_mode_https_first(); +} + /* static */ void nsHTTPSOnlyUtils::PotentiallyFireHttpRequestToShortenTimout( mozilla::net::DocumentLoadListener* aDocumentLoadListener) { @@ -62,8 +67,9 @@ void nsHTTPSOnlyUtils::PotentiallyFireHttpRequestToShortenTimout( nsCOMPtr loadInfo = channel->LoadInfo(); bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; - // if https-only mode is not even enabled, then there is nothing to do here. - if (!IsHttpsOnlyModeEnabled(isPrivateWin)) { + // if neither HTTPS-Only nor HTTPS-First mode is enabled, then there is + // nothing to do here. + if (!IsHttpsOnlyModeEnabled(isPrivateWin) && !IsHttpsFirstModeEnabled()) { return; } @@ -213,7 +219,7 @@ bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(nsIURI* aURI, nsILoadInfo* aLoadInfo) { // 1. Check if the HTTPS-Only Mode is even enabled, before we do anything else bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; - if (!IsHttpsOnlyModeEnabled(isPrivateWin)) { + if (!IsHttpsOnlyModeEnabled(isPrivateWin) && !IsHttpsFirstModeEnabled()) { return false; } @@ -286,6 +292,93 @@ bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(nsIURI* aURI, return uriHost.Equals(triggeringHost); } +/* static */ +bool nsHTTPSOnlyUtils::ShouldUpgradeHttpsFirstRequest(nsIURI* aURI, + nsILoadInfo* aLoadInfo) { + // 1. Check if HTTPS-First Mode is enabled + if (!IsHttpsFirstModeEnabled()) { + return false; + } + + // 2. HTTPS-First only upgrades top-level loads + if (aLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + return false; + } + + // 3. Don't upgrade if upgraded previously or exempt from upgrades + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST || + httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + return false; + } + + // We can upgrade the request - let's log to the console and set the status + // so we know that we upgraded the request. + MOZ_ASSERT(aURI->SchemeIs("http"), "how come the request is not 'http'?"); + nsAutoCString scheme; + aURI->GetScheme(scheme); + scheme.AppendLiteral("s"); + NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault()); + NS_ConvertUTF8toUTF16 reportScheme(scheme); + + AutoTArray params = {reportSpec, reportScheme}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyUpgradeRequest", params, + nsIScriptError::warningFlag, aLoadInfo, + aURI, true); + + // Set flag so we know that we upgraded the request + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST; + aLoadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); + return true; +} + +/* static */ +already_AddRefed +nsHTTPSOnlyUtils::PotentiallyDowngradeHttpsFirstRequest(nsIChannel* aChannel, + nsresult aError) { + nsCOMPtr loadInfo = aChannel->LoadInfo(); + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + // Only downgrade if we this request was upgraded using HTTPS-First Mode + if (!(httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST)) { + return nullptr; + } + + // No matter if we're downgrading or not, the request failed so we need to + // inform the background request. + loadInfo->SetHttpsOnlyStatus( + httpsOnlyStatus | nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS); + + // We're only downgrading if it's possible that the error was + // caused by the upgrade. + if (HttpsUpgradeUnrelatedErrorCode(aError)) { + return nullptr; + } + + nsCOMPtr uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, nullptr); + + // Only downgrade if the current scheme is HTTPS + if (!uri->SchemeIs("https")) { + return nullptr; + } + + // Change the scheme to http + nsCOMPtr newURI; + mozilla::Unused << NS_MutateURI(uri).SetScheme("http"_ns).Finalize( + getter_AddRefs(newURI)); + + // Log downgrade to console + NS_ConvertUTF8toUTF16 reportSpec(uri->GetSpecOrDefault()); + AutoTArray params = {reportSpec}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyFailedDowngradeAgain", params, + nsIScriptError::warningFlag, loadInfo, + uri, true); + + return newURI.forget(); +} + /* static */ bool nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(nsIChannel* aChannel, nsresult aError) { @@ -311,14 +404,7 @@ bool nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(nsIChannel* aChannel, // If it's one of those errors, then most likely it's not a HTTPS-Only error // (This list of errors is largely drawn from nsDocShell::DisplayLoadError()) - return !(NS_ERROR_UNKNOWN_PROTOCOL == aError || - NS_ERROR_FILE_NOT_FOUND == aError || - NS_ERROR_FILE_ACCESS_DENIED == aError || - NS_ERROR_UNKNOWN_HOST == aError || NS_ERROR_PHISHING_URI == aError || - NS_ERROR_MALWARE_URI == aError || NS_ERROR_UNWANTED_URI == aError || - NS_ERROR_HARMFUL_URI == aError || - NS_ERROR_CONTENT_CRASHED == aError || - NS_ERROR_FRAME_CRASHED == aError); + return !HttpsUpgradeUnrelatedErrorCode(aError); } /* static */ @@ -391,23 +477,35 @@ bool nsHTTPSOnlyUtils::IsSafeToAcceptCORSOrMixedContent( return nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(isPrivateWin); } +/* static */ +bool nsHTTPSOnlyUtils::HttpsUpgradeUnrelatedErrorCode(nsresult aError) { + return NS_ERROR_UNKNOWN_PROTOCOL == aError || + NS_ERROR_FILE_NOT_FOUND == aError || + NS_ERROR_FILE_ACCESS_DENIED == aError || + NS_ERROR_UNKNOWN_HOST == aError || NS_ERROR_PHISHING_URI == aError || + NS_ERROR_MALWARE_URI == aError || NS_ERROR_UNWANTED_URI == aError || + NS_ERROR_HARMFUL_URI == aError || NS_ERROR_CONTENT_CRASHED == aError || + NS_ERROR_FRAME_CRASHED == aError; +} + /* ------ Logging ------ */ /* static */ void nsHTTPSOnlyUtils::LogLocalizedString(const char* aName, const nsTArray& aParams, uint32_t aFlags, - nsILoadInfo* aLoadInfo, - nsIURI* aURI) { + nsILoadInfo* aLoadInfo, nsIURI* aURI, + bool aUseHttpsFirst) { nsAutoString logMsg; nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES, aName, aParams, logMsg); - LogMessage(logMsg, aFlags, aLoadInfo, aURI); + LogMessage(logMsg, aFlags, aLoadInfo, aURI, aUseHttpsFirst); } /* static */ void nsHTTPSOnlyUtils::LogMessage(const nsAString& aMessage, uint32_t aFlags, - nsILoadInfo* aLoadInfo, nsIURI* aURI) { + nsILoadInfo* aLoadInfo, nsIURI* aURI, + bool aUseHttpsFirst) { // do not log to the console if the loadinfo says we should not! uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE) { @@ -416,11 +514,12 @@ void nsHTTPSOnlyUtils::LogMessage(const nsAString& aMessage, uint32_t aFlags, // Prepending HTTPS-Only to the outgoing console message nsString message; - message.AppendLiteral(u"HTTPS-Only Mode: "); + message.Append(aUseHttpsFirst ? u"HTTPS-First Mode: "_ns + : u"HTTPS-Only Mode: "_ns); message.Append(aMessage); // Allow for easy distinction in devtools code. - nsCString category("HTTPSOnly"); + nsCString category(aUseHttpsFirst ? "HTTPSFirst" : "HTTPSOnly"); uint32_t innerWindowId = aLoadInfo->GetInnerWindowID(); if (innerWindowId > 0) { diff --git a/dom/security/nsHTTPSOnlyUtils.h b/dom/security/nsHTTPSOnlyUtils.h index f1166ea0f1e7..60e2820cb1d2 100644 --- a/dom/security/nsHTTPSOnlyUtils.h +++ b/dom/security/nsHTTPSOnlyUtils.h @@ -14,12 +14,18 @@ class nsHTTPSOnlyUtils { public: /** - * Returns if HTTPSOnly-Mode preference is enabled + * Returns if HTTPS-Only Mode preference is enabled * @param aFromPrivateWindow true if executing in private browsing mode * @return true if HTTPS-Only Mode is enabled */ static bool IsHttpsOnlyModeEnabled(bool aFromPrivateWindow); + /** + * Returns if HTTPS-First Mode preference is enabled + * @return true if HTTPS-First Mode is enabled + */ + static bool IsHttpsFirstModeEnabled(); + /** * Potentially fires an http request for a top-level load (provided by * aDocumentLoadListener) in the background to avoid long timeouts in case @@ -64,8 +70,29 @@ class nsHTTPSOnlyUtils { nsILoadInfo* aLoadInfo); /** - * Checks if the error code is on a block-list of codes that are probably not - * related to a HTTPS-Only Mode upgrade. + * Determines if a request should get upgraded because of the HTTPS-First + * mode. If true, the httpsOnlyStatus in LoadInfo gets updated and a message + * is logged in the console. + * @param aURI nsIURI of request + * @param aLoadInfo nsILoadInfo of request + * @return true if request should get upgraded + */ + static bool ShouldUpgradeHttpsFirstRequest(nsIURI* aURI, + nsILoadInfo* aLoadInfo); + + /** + * Determines if the request was previously upgraded with HTTPS-First, creates + * a downgraded URI and logs to console. + * @param aError Error code + * @param aChannel Failed channel + * @return URI with http-scheme or nullptr + */ + static already_AddRefed PotentiallyDowngradeHttpsFirstRequest( + nsIChannel* aChannel, nsresult aError); + + /** + * Checks if the error code is on a block-list of codes that are probably + * not related to a HTTPS-Only Mode upgrade. * @param aChannel The failed Channel. * @param aError Error Code from Request * @return false if error is not related to upgrade @@ -74,16 +101,18 @@ class nsHTTPSOnlyUtils { /** * Logs localized message to either content console or browser console - * @param aName Localization key - * @param aParams Localization parameters - * @param aFlags Logging Flag (see nsIScriptError) - * @param aLoadInfo The loadinfo of the request. - * @param [aURI] Optional: URI to log + * @param aName Localization key + * @param aParams Localization parameters + * @param aFlags Logging Flag (see nsIScriptError) + * @param aLoadInfo The loadinfo of the request. + * @param [aURI] Optional: URI to log + * @param [aUseHttpsFirst] Optional: Log using HTTPS-First (vs. HTTPS-Only) */ static void LogLocalizedString(const char* aName, const nsTArray& aParams, uint32_t aFlags, nsILoadInfo* aLoadInfo, - nsIURI* aURI = nullptr); + nsIURI* aURI = nullptr, + bool aUseHttpsFirst = false); /** * Tests if the HTTPS-Only upgrade exception is set for a given principal. @@ -123,15 +152,24 @@ class nsHTTPSOnlyUtils { nsILoadInfo* aLoadInfo); private: + /** + * Checks if it can be ruled out that the error has something + * to do with an HTTPS upgrade. + * @param aError error code + * @return true if error is unrelated to the upgrade + */ + static bool HttpsUpgradeUnrelatedErrorCode(nsresult aError); /** * Logs localized message to either content console or browser console - * @param aMessage Message to log - * @param aFlags Logging Flag (see nsIScriptError) - * @param aLoadInfo The loadinfo of the request. - * @param [aURI] Optional: URI to log + * @param aMessage Message to log + * @param aFlags Logging Flag (see nsIScriptError) + * @param aLoadInfo The loadinfo of the request. + * @param [aURI] Optional: URI to log + * @param [aUseHttpsFirst] Optional: Log using HTTPS-First (vs. HTTPS-Only) */ static void LogMessage(const nsAString& aMessage, uint32_t aFlags, - nsILoadInfo* aLoadInfo, nsIURI* aURI = nullptr); + nsILoadInfo* aLoadInfo, nsIURI* aURI = nullptr, + bool aUseHttpsFirst = false); /** * Checks whether the URI ends with .onion diff --git a/dom/security/test/https-first/.eslintrc.js b/dom/security/test/https-first/.eslintrc.js new file mode 100644 index 000000000000..1779fd7f1cf8 --- /dev/null +++ b/dom/security/test/https-first/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/browser-test"], +}; diff --git a/dom/security/test/https-first/browser.ini b/dom/security/test/https-first/browser.ini new file mode 100644 index 000000000000..47678f26fd4f --- /dev/null +++ b/dom/security/test/https-first/browser.ini @@ -0,0 +1,4 @@ +[browser_httpsfirst.js] +support-files = + file_httpsfirst_timeout_server.sjs +[browser_httpsfirst_console_logging.js] diff --git a/dom/security/test/https-first/browser_httpsfirst.js b/dom/security/test/https-first/browser_httpsfirst.js new file mode 100644 index 000000000000..143e65944435 --- /dev/null +++ b/dom/security/test/https-first/browser_httpsfirst.js @@ -0,0 +1,63 @@ +"use strict"; + +const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const TIMEOUT_PAGE_URI_HTTP = + TEST_PATH_HTTP + "file_httpsfirst_timeout_server.sjs"; + +async function runPrefTest(aURI, aDesc, aAssertURLStartsWith) { + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true); + BrowserTestUtils.loadURI(browser, aURI); + await loaded; + + await ContentTask.spawn(browser, { aDesc, aAssertURLStartsWith }, function({ + aDesc, + aAssertURLStartsWith, + }) { + ok( + content.document.location.href.startsWith(aAssertURLStartsWith), + aDesc + ); + }); + }); +} + +add_task(async function() { + await runPrefTest( + "http://example.com", + "HTTPS-First disabled; Should not upgrade", + "http://" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode_https_first", true]], + }); + + await runPrefTest( + "http://example.com", + "Should upgrade upgradeable website", + "https://" + ); + + await runPrefTest( + "http://httpsfirst.com", + "Should downgrade after error.", + "http://" + ); + + await runPrefTest( + "http://domain.does.not.exist", + "Should not downgrade on dnsNotFound error.", + "https://" + ); + + await runPrefTest( + TIMEOUT_PAGE_URI_HTTP, + "Should downgrade after timeout.", + "http://" + ); +}); diff --git a/dom/security/test/https-first/browser_httpsfirst_console_logging.js b/dom/security/test/https-first/browser_httpsfirst_console_logging.js new file mode 100644 index 000000000000..2e3841a5096d --- /dev/null +++ b/dom/security/test/https-first/browser_httpsfirst_console_logging.js @@ -0,0 +1,71 @@ +// Bug 1658924 - HTTPS-First Mode - Tests for console logging +// https://bugzilla.mozilla.org/show_bug.cgi?id=1658924 +// This test makes sure that the various console messages from the HTTPS-First +// mode get logged to the console. +"use strict"; + +// Test Cases +// description: Description of what the subtests expects. +// expectLogLevel: Expected log-level of a message. +// expectIncludes: Expected substrings the message should contain. +let tests = [ + { + description: "Top-Level upgrade should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: ["Upgrading insecure request", "to use", "httpsfirst.com"], + }, + { + description: "Top-Level upgrade failure should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "Upgrading insecure request", + "failed", + "httpsfirst.com", + "Downgrading to", + ], + }, +]; + +add_task(async function() { + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + + // Enable HTTPS-First Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode_https_first", true]], + }); + Services.console.registerListener(on_new_message); + // 1. Upgrade page to https:// + await BrowserTestUtils.loadURI( + gBrowser.selectedBrowser, + "http://httpsfirst.com" + ); + + await BrowserTestUtils.waitForCondition(() => tests.length === 0); + + // Clean up + Services.console.unregisterListener(on_new_message); +}); + +function on_new_message(msgObj) { + const message = msgObj.message; + const logLevel = msgObj.logLevel; + + if (message.includes("HTTPS-First Mode:")) { + for (let i = 0; i < tests.length; i++) { + const testCase = tests[i]; + // Check if log-level matches + if (logLevel !== testCase.expectLogLevel) { + continue; + } + // Check if all substrings are included + if (testCase.expectIncludes.some(str => !message.includes(str))) { + continue; + } + ok(true, testCase.description); + tests.splice(i, 1); + break; + } + } +} diff --git a/dom/security/test/https-first/file_httpsfirst_timeout_server.sjs b/dom/security/test/https-first/file_httpsfirst_timeout_server.sjs new file mode 100644 index 000000000000..bc2c4b9de286 --- /dev/null +++ b/dom/security/test/https-first/file_httpsfirst_timeout_server.sjs @@ -0,0 +1,15 @@ + +function handleRequest(request, response) +{ + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.scheme === "https") { + // Simulating a timeout by processing the https request + // async and *never* return anything! + response.processAsync(); + return; + } + // we should never get here; just in case, return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/moz.build b/dom/security/test/moz.build index dcfc6b951a5b..85b2cc8fa58e 100644 --- a/dom/security/test/moz.build +++ b/dom/security/test/moz.build @@ -34,6 +34,7 @@ BROWSER_CHROME_MANIFESTS += [ "cors/browser.ini", "csp/browser.ini", "general/browser.ini", + "https-first/browser.ini", "https-only/browser.ini", "mixedcontentblocker/browser.ini", ] diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index b5e2b5a871f2..fb05e1aa85bc 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -2931,6 +2931,13 @@ value: false mirror: always +# If true, top-level request will get upgraded to HTTPS and +# downgraded again if the request failed. +- name: dom.security.https_only_mode_https_first + type: RelaxedAtomicBool + value: false + mirror: always + - name: dom.security.unexpected_system_load_telemetry_enabled type: bool value: true diff --git a/netwerk/base/nsILoadInfo.idl b/netwerk/base/nsILoadInfo.idl index 8eb63f06588e..1358e2981615 100644 --- a/netwerk/base/nsILoadInfo.idl +++ b/netwerk/base/nsILoadInfo.idl @@ -470,6 +470,12 @@ interface nsILoadInfo : nsISupports */ const unsigned long HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE = (1 << 5); + /** + * This flag indicates that the request should not be logged to the + * console. + */ + const unsigned long HTTPS_ONLY_UPGRADED_HTTPS_FIRST = (1 << 6); + /** * Upgrade state of HTTPS-Only Mode. The flag HTTPS_ONLY_EXEMPT can get * set on requests that should be excempt from an upgrade. diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp index 165ce6578917..37f4bbabe0db 100644 --- a/netwerk/base/nsNetUtil.cpp +++ b/netwerk/base/nsNetUtil.cpp @@ -2856,7 +2856,8 @@ nsresult NS_ShouldSecureUpgrade( !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(aURI)) { if (aLoadInfo) { // Check if the request can get upgraded with the HTTPS-Only mode - if (nsHTTPSOnlyUtils::ShouldUpgradeRequest(aURI, aLoadInfo)) { + if (nsHTTPSOnlyUtils::ShouldUpgradeRequest(aURI, aLoadInfo) || + nsHTTPSOnlyUtils::ShouldUpgradeHttpsFirstRequest(aURI, aLoadInfo)) { aShouldUpgrade = true; return NS_OK; } diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp index 69ec91bd2d33..63c1539d66cb 100644 --- a/netwerk/ipc/DocumentLoadListener.cpp +++ b/netwerk/ipc/DocumentLoadListener.cpp @@ -140,6 +140,12 @@ static auto CreateDocumentLoadInfo(CanonicalBrowsingContext* aBrowsingContext, attrs, securityFlags, sandboxFlags); } + if (aLoadState->IsExemptFromHTTPSOnlyMode()) { + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT; + loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); + } + loadInfo->SetTriggeringSandboxFlags(aLoadState->TriggeringSandboxFlags()); loadInfo->SetHasValidUserGestureActivation( aLoadState->HasValidUserGestureActivation()); @@ -2159,6 +2165,17 @@ bool DocumentLoadListener::MaybeHandleLoadErrorWithURIFixup(nsresult aStatus) { mLoadStateInternalLoadFlags & nsDocShell::INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, bc->UsePrivateBrowsing(), true, getter_AddRefs(newPostData)); + + // If the request failed, the above attempt to fix it failed but it + // was upgraded using HTTPS-First, then let's check if we can downgrade + // the scheme to HTTP again. + bool isHTTPSFirstFixup = false; + if (NS_FAILED(aStatus) && !newURI) { + newURI = nsHTTPSOnlyUtils::PotentiallyDowngradeHttpsFirstRequest(mChannel, + aStatus); + isHTTPSFirstFixup = true; + } + if (!newURI) { return false; } @@ -2179,6 +2196,12 @@ bool DocumentLoadListener::MaybeHandleLoadErrorWithURIFixup(nsresult aStatus) { loadState->SetPostDataStream(newPostData); + if (isHTTPSFirstFixup) { + // We have to exempt the load from HTTPS-First to prevent a + // upgrade-downgrade loop. + loadState->SetIsExemptFromHTTPSOnlyMode(true); + } + bc->LoadURI(loadState, false); return true; }