diff options
Diffstat (limited to '')
-rw-r--r-- | dom/security/nsHTTPSOnlyUtils.cpp | 1071 |
1 files changed, 1071 insertions, 0 deletions
diff --git a/dom/security/nsHTTPSOnlyUtils.cpp b/dom/security/nsHTTPSOnlyUtils.cpp new file mode 100644 index 0000000000..2a3880ba70 --- /dev/null +++ b/dom/security/nsHTTPSOnlyUtils.cpp @@ -0,0 +1,1071 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Components.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/net/DNS.h" +#include "nsContentUtils.h" +#include "nsHTTPSOnlyUtils.h" +#include "nsIConsoleService.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIHttpsOnlyModePermission.h" +#include "nsILoadInfo.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIScriptError.h" +#include "nsIURIMutator.h" +#include "nsNetUtil.h" +#include "prnetdb.h" + +/* static */ +bool nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(bool aFromPrivateWindow) { + // if the general pref is set to true, then we always return + if (mozilla::StaticPrefs::dom_security_https_only_mode()) { + return true; + } + + // otherwise we check if executing in private browsing mode and return true + // if the PBM pref for HTTPS-Only is set. + if (aFromPrivateWindow && + mozilla::StaticPrefs::dom_security_https_only_mode_pbm()) { + return true; + } + return false; +} + +/* static */ +bool nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(bool aFromPrivateWindow) { + // HTTPS-Only takes priority over HTTPS-First + if (IsHttpsOnlyModeEnabled(aFromPrivateWindow)) { + return false; + } + + // if the general pref is set to true, then we always return + if (mozilla::StaticPrefs::dom_security_https_first()) { + return true; + } + + // otherwise we check if executing in private browsing mode and return true + // if the PBM pref for HTTPS-First is set. + if (aFromPrivateWindow && + mozilla::StaticPrefs::dom_security_https_first_pbm()) { + return true; + } + return false; +} + +/* static */ +void nsHTTPSOnlyUtils::PotentiallyFireHttpRequestToShortenTimout( + mozilla::net::DocumentLoadListener* aDocumentLoadListener) { + // only send http background request to counter timeouts if the + // pref allows us to do that. + if (!mozilla::StaticPrefs:: + dom_security_https_only_mode_send_http_background_request()) { + return; + } + + nsCOMPtr<nsIChannel> channel = aDocumentLoadListener->GetChannel(); + if (!channel) { + return; + } + + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + + // if neither HTTPS-Only nor HTTPS-First mode is enabled, then there is + // nothing to do here. + if ((!IsHttpsOnlyModeEnabled(isPrivateWin) && + !IsHttpsFirstModeEnabled(isPrivateWin)) && + !(loadInfo->GetWasSchemelessInput() && + mozilla::StaticPrefs::dom_security_https_first_schemeless())) { + return; + } + + // if we are not dealing with a top-level load, then there is nothing to do + // here. + if (loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + return; + } + + // if the load is exempt, then there is nothing to do here. + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::nsILoadInfo::HTTPS_ONLY_EXEMPT) { + return; + } + + // if it's not an http channel, then there is nothing to do here. + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel)); + if (!httpChannel) { + return; + } + + // if it's not a GET method, then there is nothing to do here either. + nsAutoCString method; + mozilla::Unused << httpChannel->GetRequestMethod(method); + if (!method.EqualsLiteral("GET")) { + return; + } + + // if it's already an https channel, then there is nothing to do here. + nsCOMPtr<nsIURI> channelURI; + channel->GetURI(getter_AddRefs(channelURI)); + if (!channelURI->SchemeIs("http")) { + return; + } + + // HTTPS-First only applies to standard ports but HTTPS-Only brute forces + // all http connections to be https and overrules HTTPS-First. In case + // HTTPS-First is enabled, but HTTPS-Only is not enabled, we might return + // early if attempting to send a background request to a non standard port. + if ((IsHttpsFirstModeEnabled(isPrivateWin) || + (loadInfo->GetWasSchemelessInput() && + mozilla::StaticPrefs::dom_security_https_first_schemeless()))) { + int32_t port = 0; + nsresult rv = channelURI->GetPort(&port); + int defaultPortforScheme = NS_GetDefaultPort("http"); + if (NS_SUCCEEDED(rv) && port != defaultPortforScheme && port != -1) { + return; + } + } + + // Check for general exceptions + if (OnionException(channelURI) || LoopbackOrLocalException(channelURI)) { + return; + } + + RefPtr<nsIRunnable> task = + new TestHTTPAnswerRunnable(channelURI, aDocumentLoadListener); + NS_DispatchToMainThread(task.forget()); +} + +/* static */ +bool nsHTTPSOnlyUtils::ShouldUpgradeRequest(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)) { + return false; + } + + // 2. Check for general exceptions + if (OnionException(aURI) || LoopbackOrLocalException(aURI)) { + return false; + } + + // 3. Check if NoUpgrade-flag is set in LoadInfo + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + AutoTArray<nsString, 1> params = { + NS_ConvertUTF8toUTF16(aURI->GetSpecOrDefault())}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyNoUpgradeException", params, + nsIScriptError::infoFlag, aLoadInfo, + aURI); + return false; + } + + // All subresources of an exempt triggering principal are also exempt + ExtContentPolicyType contentType = aLoadInfo->GetExternalContentPolicyType(); + if (contentType != ExtContentPolicy::TYPE_DOCUMENT) { + if (!aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal() && + TestIfPrincipalIsExempt(aLoadInfo->TriggeringPrincipal())) { + return false; + } + } + + // We can not upgrade "Save-As" downloads, since we have no way of detecting + // if the upgrade failed (Bug 1674859). For now we will just allow the + // download, since there will still be a visual warning about the download + // being insecure. + if (contentType == ExtContentPolicyType::TYPE_SAVEAS_DOWNLOAD) { + return false; + } + + // We can upgrade the request - let's log it to the console + // Appending an 's' to the scheme for the logging. (http -> https) + nsAutoCString scheme; + aURI->GetScheme(scheme); + scheme.AppendLiteral("s"); + NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault()); + NS_ConvertUTF8toUTF16 reportScheme(scheme); + + bool isSpeculative = aLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_SPECULATIVE; + AutoTArray<nsString, 2> params = {reportSpec, reportScheme}; + nsHTTPSOnlyUtils::LogLocalizedString( + isSpeculative ? "HTTPSOnlyUpgradeSpeculativeConnection" + : "HTTPSOnlyUpgradeRequest", + params, nsIScriptError::warningFlag, aLoadInfo, aURI); + + // If the status was not determined before, we now indicate that the request + // will get upgraded, but no event-listener has been registered yet. + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) { + httpsOnlyStatus ^= nsILoadInfo::HTTPS_ONLY_UNINITIALIZED; + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED; + aLoadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); + } + return true; +} + +/* static */ +bool nsHTTPSOnlyUtils::ShouldUpgradeWebSocket(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)) { + return false; + } + + // 2. Check for general exceptions + if (OnionException(aURI) || LoopbackOrLocalException(aURI)) { + return false; + } + + // 3. Check if NoUpgrade-flag is set in LoadInfo + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + // Let's log to the console, that we didn't upgrade this request + AutoTArray<nsString, 1> params = { + NS_ConvertUTF8toUTF16(aURI->GetSpecOrDefault())}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyNoUpgradeException", params, + nsIScriptError::infoFlag, aLoadInfo, + aURI); + return false; + } + + // All subresources of an exempt triggering principal are also exempt. + if (!aLoadInfo->TriggeringPrincipal()->IsSystemPrincipal() && + TestIfPrincipalIsExempt(aLoadInfo->TriggeringPrincipal())) { + return false; + } + + // We can upgrade the request - let's log it to the console + // Appending an 's' to the scheme for the logging. (ws -> wss) + nsAutoCString scheme; + aURI->GetScheme(scheme); + scheme.AppendLiteral("s"); + NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault()); + NS_ConvertUTF8toUTF16 reportScheme(scheme); + + AutoTArray<nsString, 2> params = {reportSpec, reportScheme}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyUpgradeRequest", params, + nsIScriptError::warningFlag, aLoadInfo, + aURI); + return true; +} + +/* static */ +bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop( + nsIURI* aURI, nsILoadInfo* aLoadInfo, + const mozilla::EnumSet<UpgradeDowngradeEndlessLoopOptions>& aOptions) { + // 1. Check if the HTTPS-Only/HTTPS-First is even enabled, before doing + // anything else + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + bool enforceForHTTPSOnlyMode = + IsHttpsOnlyModeEnabled(isPrivateWin) && + aOptions.contains( + UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSOnlyMode); + bool enforceForHTTPSFirstMode = + IsHttpsFirstModeEnabled(isPrivateWin) && + aOptions.contains( + UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSFirstMode); + bool enforceForHTTPSRR = + aOptions.contains(UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSRR); + if (!enforceForHTTPSOnlyMode && !enforceForHTTPSFirstMode && + !enforceForHTTPSRR) { + return false; + } + + // 2. Check if the upgrade downgrade pref even wants us to try to break the + // cycle. In the case that HTTPS RR is presented, we ignore this pref. + if (!mozilla::StaticPrefs:: + dom_security_https_only_mode_break_upgrade_downgrade_endless_loop() && + !enforceForHTTPSRR) { + return false; + } + + // 3. If it's not a top-level load, then there is nothing to do here either. + if (aLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + return false; + } + + // 4. If the load is exempt, then it's defintely not related to https-only + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if ((httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) && + !enforceForHTTPSRR) { + return false; + } + + // 5. If the URI to be loaded is not http, then it's defnitely no endless + // loop caused by https-only. + if (!aURI->SchemeIs("http")) { + return false; + } + + nsAutoCString uriHost; + aURI->GetAsciiHost(uriHost); + + auto uriAndPrincipalComparator = [&](nsIPrincipal* aPrincipal) { + nsAutoCString principalHost; + aPrincipal->GetAsciiHost(principalHost); + bool checkPath = mozilla::StaticPrefs:: + dom_security_https_only_check_path_upgrade_downgrade_endless_loop(); + if (!checkPath) { + return uriHost.Equals(principalHost); + } + + nsAutoCString uriPath; + nsresult rv = aURI->GetFilePath(uriPath); + if (NS_FAILED(rv)) { + return false; + } + nsAutoCString principalPath; + aPrincipal->GetFilePath(principalPath); + return uriHost.Equals(principalHost) && uriPath.Equals(principalPath); + }; + + // 6. Check actual redirects. If the Principal that kicked off the + // load/redirect is not https, then it's definitely not a redirect cause by + // https-only. If the scheme of the principal however is https and the + // asciiHost of the URI to be loaded and the asciiHost of the Principal are + // identical, then we are dealing with an upgrade downgrade scenario and we + // have to break the cycle. + if (!aLoadInfo->RedirectChain().IsEmpty()) { + nsCOMPtr<nsIPrincipal> redirectPrincipal; + for (nsIRedirectHistoryEntry* entry : aLoadInfo->RedirectChain()) { + entry->GetPrincipal(getter_AddRefs(redirectPrincipal)); + if (redirectPrincipal && redirectPrincipal->SchemeIs("https") && + uriAndPrincipalComparator(redirectPrincipal)) { + return true; + } + } + } else { + // 6.1 We should only check if this load is triggered by a user gesture + // when the redirect chain is empty, since this information is only useful + // in our case here. When the redirect chain is not empty, this load is + // defnitely triggered by redirection, not a user gesture. + if (aLoadInfo->GetHasValidUserGestureActivation()) { + return false; + } + } + + // 7. Meta redirects and JS based redirects (win.location). If the security + // context that triggered the load is not https, then it's defnitely no + // endless loop caused by https-only. If the scheme is http however and the + // asciiHost of the URI to be loaded matches the asciiHost of the Principal, + // then we are dealing with an upgrade downgrade scenario and we have to break + // the cycle. + nsCOMPtr<nsIPrincipal> triggeringPrincipal = aLoadInfo->TriggeringPrincipal(); + if (!triggeringPrincipal->SchemeIs("https")) { + return false; + } + + return uriAndPrincipalComparator(triggeringPrincipal); +} + +/* static */ +bool nsHTTPSOnlyUtils::ShouldUpgradeHttpsFirstRequest(nsIURI* aURI, + nsILoadInfo* aLoadInfo) { + // 1. Check if HTTPS-First Mode is enabled + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (!IsHttpsFirstModeEnabled(isPrivateWin) && + !(aLoadInfo->GetWasSchemelessInput() && + mozilla::StaticPrefs::dom_security_https_first_schemeless())) { + return false; + } + + // 2. HTTPS-First only upgrades top-level loads (and speculative connections) + ExtContentPolicyType contentType = aLoadInfo->GetExternalContentPolicyType(); + if (contentType != ExtContentPolicy::TYPE_DOCUMENT && + contentType != ExtContentPolicy::TYPE_SPECULATIVE) { + return false; + } + + // 3. Check for general exceptions + if (OnionException(aURI) || LoopbackOrLocalException(aURI)) { + return false; + } + + // 4. 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; + } + + // 5. HTTPS-First Mode only upgrades default ports - do not upgrade the + // request to https if port is specified and not the default port of 80. + MOZ_ASSERT(aURI->SchemeIs("http"), "how come the request is not 'http'?"); + int defaultPortforScheme = NS_GetDefaultPort("http"); + // If no port is specified, then the API returns -1 to indicate the default + // port. + int32_t port = 0; + nsresult rv = aURI->GetPort(&port); + NS_ENSURE_SUCCESS(rv, false); + if (port != defaultPortforScheme && port != -1) { + return false; + } + // 6. Do not upgrade form submissions (for now), revisit within + // Bug 1720500: Revisit upgrading form submissions. + if (aLoadInfo->GetIsFormSubmission()) { + return false; + } + + // https-first needs to account for breaking upgrade-downgrade endless + // loops at this point because this function is called before we + // check the redirect limit in HttpBaseChannel. If we encounter + // a same-origin server side downgrade from e.g https://example.com + // to http://example.com then we simply not annotating the loadinfo + // and returning false from within this function. Please note that + // the handling for https-only mode is different from https-first mode, + // because https-only mode results in an exception page in case + // we encounter and endless upgrade downgrade loop. + bool isUpgradeDowngradeEndlessLoop = IsUpgradeDowngradeEndlessLoop( + aURI, aLoadInfo, + {UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSFirstMode}); + if (isUpgradeDowngradeEndlessLoop) { + 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. + if (aLoadInfo->GetWasSchemelessInput() && + mozilla::StaticPrefs::dom_security_https_first_schemeless()) { + nsAutoCString urlCString; + aURI->GetSpec(urlCString); + NS_ConvertUTF8toUTF16 urlString(urlCString); + + AutoTArray<nsString, 1> params = {urlString}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSFirstSchemeless", params, + nsIScriptError::warningFlag, aLoadInfo, + aURI, true); + } else { + nsAutoCString scheme; + + aURI->GetScheme(scheme); + scheme.AppendLiteral("s"); + NS_ConvertUTF8toUTF16 reportSpec(aURI->GetSpecOrDefault()); + NS_ConvertUTF8toUTF16 reportScheme(scheme); + + bool isSpeculative = contentType == ExtContentPolicy::TYPE_SPECULATIVE; + AutoTArray<nsString, 2> params = {reportSpec, reportScheme}; + nsHTTPSOnlyUtils::LogLocalizedString( + isSpeculative ? "HTTPSOnlyUpgradeSpeculativeConnection" + : "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<nsIURI> +nsHTTPSOnlyUtils::PotentiallyDowngradeHttpsFirstRequest(nsIChannel* aChannel, + nsresult aStatus) { + nsCOMPtr<nsILoadInfo> 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; + } + // Once loading is in progress we set that flag so that timeout counter + // measures do not kick in. + loadInfo->SetHttpsOnlyStatus( + httpsOnlyStatus | nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS); + + nsresult status = aStatus; + // Since 4xx and 5xx errors return NS_OK instead of NS_ERROR_*, we need + // to check each NS_OK for those errors. + // Only downgrade an NS_OK status if it is an 4xx or 5xx error. + if (NS_SUCCEEDED(aStatus)) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + // If no httpChannel exists we have nothing to do here. + if (!httpChannel) { + return nullptr; + } + uint32_t responseStatus = 0; + if (NS_FAILED(httpChannel->GetResponseStatus(&responseStatus))) { + return nullptr; + } + + // In case we found one 4xx or 5xx error we need to log it later on, + // for that reason we flip the nsresult 'status' from 'NS_OK' to the + // corresponding NS_ERROR_*. + // To do so we convert the response status to an nsresult error + // Every NS_OK that is NOT an 4xx or 5xx error code won't get downgraded. + if (responseStatus >= 400 && responseStatus < 600) { + // HttpProxyResponseToErrorCode() maps 400 and 404 on + // the same error as a 500 status which would lead to no downgrade + // later on. For that reason we explicit filter for 400 and 404 status + // codes to log them correctly and to downgrade them if possible. + switch (responseStatus) { + case 400: + status = NS_ERROR_PROXY_BAD_REQUEST; + break; + case 404: + status = NS_ERROR_PROXY_NOT_FOUND; + break; + default: + status = mozilla::net::HttpProxyResponseToErrorCode(responseStatus); + break; + } + } + if (NS_SUCCEEDED(status)) { + return nullptr; + } + } + + // We're only downgrading if it's possible that the error was + // caused by the upgrade. + if (HttpsUpgradeUnrelatedErrorCode(status)) { + return nullptr; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsAutoCString spec; + nsCOMPtr<nsIURI> newURI; + + // Only downgrade if the current scheme is (a) https or (b) view-source:https + if (uri->SchemeIs("https")) { + rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, nullptr); + + rv = NS_NewURI(getter_AddRefs(newURI), spec); + NS_ENSURE_SUCCESS(rv, nullptr); + + rv = NS_MutateURI(newURI).SetScheme("http"_ns).Finalize( + getter_AddRefs(newURI)); + NS_ENSURE_SUCCESS(rv, nullptr); + } else if (uri->SchemeIs("view-source")) { + nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(uri); + if (!nestedURI) { + return nullptr; + } + nsCOMPtr<nsIURI> innerURI; + rv = nestedURI->GetInnerURI(getter_AddRefs(innerURI)); + NS_ENSURE_SUCCESS(rv, nullptr); + if (!innerURI || !innerURI->SchemeIs("https")) { + return nullptr; + } + rv = NS_MutateURI(innerURI).SetScheme("http"_ns).Finalize( + getter_AddRefs(innerURI)); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsAutoCString innerSpec; + rv = innerURI->GetSpec(innerSpec); + NS_ENSURE_SUCCESS(rv, nullptr); + + spec.Append("view-source:"); + spec.Append(innerSpec); + + rv = NS_NewURI(getter_AddRefs(newURI), spec); + NS_ENSURE_SUCCESS(rv, nullptr); + } else { + return nullptr; + } + + // Log downgrade to console + NS_ConvertUTF8toUTF16 reportSpec(uri->GetSpecOrDefault()); + AutoTArray<nsString, 1> params = {reportSpec}; + nsHTTPSOnlyUtils::LogLocalizedString("HTTPSOnlyFailedDowngradeAgain", params, + nsIScriptError::warningFlag, loadInfo, + uri, true); + + return newURI.forget(); +} + +/* static */ +bool nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(nsIChannel* aChannel, + nsresult aError) { + // If there is no failed channel, then there is nothing to do here. + if (!aChannel) { + return false; + } + + // If HTTPS-Only Mode is not enabled, then there is nothing to do here. + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (!IsHttpsOnlyModeEnabled(isPrivateWin)) { + return false; + } + + // If the load is exempt or did not get upgraded, + // then there is nothing to do here. + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT || + httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) { + return false; + } + + // 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 !HttpsUpgradeUnrelatedErrorCode(aError); +} + +/* static */ +bool nsHTTPSOnlyUtils::TestIfPrincipalIsExempt(nsIPrincipal* aPrincipal) { + static nsCOMPtr<nsIPermissionManager> sPermMgr; + if (!sPermMgr) { + sPermMgr = mozilla::components::PermissionManager::Service(); + mozilla::ClearOnShutdown(&sPermMgr); + } + NS_ENSURE_TRUE(sPermMgr, false); + + uint32_t perm; + nsresult rv = sPermMgr->TestExactPermissionFromPrincipal( + aPrincipal, "https-only-load-insecure"_ns, &perm); + NS_ENSURE_SUCCESS(rv, false); + + return perm == nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW || + perm == nsIHttpsOnlyModePermission::LOAD_INSECURE_ALLOW_SESSION; +} + +/* static */ +void nsHTTPSOnlyUtils::TestSitePermissionAndPotentiallyAddExemption( + nsIChannel* aChannel) { + NS_ENSURE_TRUE_VOID(aChannel); + + // If HTTPS-Only or HTTPS-First Mode is not enabled, then there is nothing to + // do here. + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + bool isHttpsOnly = IsHttpsOnlyModeEnabled(isPrivateWin); + bool isHttpsFirst = IsHttpsFirstModeEnabled(isPrivateWin); + bool isSchemelessHttpsFirst = + (loadInfo->GetWasSchemelessInput() && + mozilla::StaticPrefs::dom_security_https_first_schemeless()); + if (!isHttpsOnly && !isHttpsFirst && !isSchemelessHttpsFirst) { + return; + } + + // if it's not a top-level load then there is nothing to here. + ExtContentPolicyType type = loadInfo->GetExternalContentPolicyType(); + if (type != ExtContentPolicy::TYPE_DOCUMENT) { + return; + } + + // it it's not an http channel, then there is nothing to do here. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + if (!httpChannel) { + return; + } + + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + aChannel, getter_AddRefs(principal)); + NS_ENSURE_SUCCESS_VOID(rv); + + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + bool isPrincipalExempt = TestIfPrincipalIsExempt(principal); + if (isPrincipalExempt) { + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT; + } else { + // We explicitly remove the exemption flag, because this + // function is also consulted after redirects. + httpsOnlyStatus &= ~nsILoadInfo::HTTPS_ONLY_EXEMPT; + } + if (httpsOnlyStatus & nsILoadInfo::HTTPS_FIRST_EXEMPT_NEXT_LOAD && + isHttpsFirst) { + httpsOnlyStatus &= ~nsILoadInfo::HTTPS_FIRST_EXEMPT_NEXT_LOAD; + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT; + } + loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); +} + +/* static */ +bool nsHTTPSOnlyUtils::IsSafeToAcceptCORSOrMixedContent( + nsILoadInfo* aLoadInfo) { + // Check if the request is exempt from upgrades + if ((aLoadInfo->GetHttpsOnlyStatus() & nsILoadInfo::HTTPS_ONLY_EXEMPT)) { + return false; + } + // Check if HTTPS-Only Mode is enabled for this request + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + 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 || NS_ERROR_SUPERFLUOS_AUTH == aError; +} + +/* ------ Logging ------ */ + +/* static */ +void nsHTTPSOnlyUtils::LogLocalizedString(const char* aName, + const nsTArray<nsString>& aParams, + uint32_t aFlags, + nsILoadInfo* aLoadInfo, nsIURI* aURI, + bool aUseHttpsFirst) { + nsAutoString logMsg; + nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES, + aName, aParams, logMsg); + LogMessage(logMsg, aFlags, aLoadInfo, aURI, aUseHttpsFirst); +} + +/* static */ +void nsHTTPSOnlyUtils::LogMessage(const nsAString& aMessage, uint32_t aFlags, + 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) { + return; + } + + // Prepending HTTPS-Only to the outgoing console message + nsString message; + message.Append(aUseHttpsFirst ? u"HTTPS-First Mode: "_ns + : u"HTTPS-Only Mode: "_ns); + message.Append(aMessage); + + // Allow for easy distinction in devtools code. + auto category = aUseHttpsFirst ? "HTTPSFirst"_ns : "HTTPSOnly"_ns; + + uint64_t windowId = aLoadInfo->GetInnerWindowID(); + if (!windowId) { + windowId = aLoadInfo->GetTriggeringWindowId(); + } + if (windowId) { + // Send to content console + nsContentUtils::ReportToConsoleByWindowID(message, aFlags, category, + windowId, aURI); + } else { + // Send to browser console + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + nsContentUtils::LogSimpleConsoleError(message, category, isPrivateWin, + true /* from chrome context */, + aFlags); + } +} + +/* ------ Exceptions ------ */ + +/* static */ +bool nsHTTPSOnlyUtils::OnionException(nsIURI* aURI) { + // Onion-host exception can get disabled with a pref + if (mozilla::StaticPrefs::dom_security_https_only_mode_upgrade_onion()) { + return false; + } + nsAutoCString host; + aURI->GetHost(host); + return StringEndsWith(host, ".onion"_ns); +} + +/* static */ +bool nsHTTPSOnlyUtils::LoopbackOrLocalException(nsIURI* aURI) { + nsAutoCString asciiHost; + nsresult rv = aURI->GetAsciiHost(asciiHost); + NS_ENSURE_SUCCESS(rv, false); + + // Let's make a quick check if the host matches these loopback strings + // before we do anything else + if (asciiHost.EqualsLiteral("localhost") || asciiHost.EqualsLiteral("::1")) { + return true; + } + + mozilla::net::NetAddr addr; + if (NS_FAILED(addr.InitFromString(asciiHost))) { + return false; + } + // Loopback IPs are always exempt + if (addr.IsLoopbackAddr()) { + return true; + } + + // Local IP exception can get disabled with a pref + bool upgradeLocal = + mozilla::StaticPrefs::dom_security_https_only_mode_upgrade_local(); + return (!upgradeLocal && addr.IsIPAddrLocal()); +} + +/* static */ +bool nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI, + nsIURI* aOtherURI, + nsILoadInfo* aLoadInfo) { + // 1. Check if one of parameters is null then webpage can't be loaded yet + // and no further inspections are needed + if (!aHTTPSSchemeURI || !aOtherURI || !aLoadInfo) { + return false; + } + + // 2. If the URI to be loaded is not http, then same origin will be detected + // already + if (!mozilla::net::SchemeIsHTTP(aOtherURI)) { + return false; + } + + // 3. Check if the HTTPS-Only Mode is even enabled, before we do anything else + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (!IsHttpsOnlyModeEnabled(isPrivateWin) && + !IsHttpsFirstModeEnabled(isPrivateWin)) { + return false; + } + + // 4. If the load is exempt, then it's defintely not related to https-only + uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + return false; + } + + // 5. Create a new target URI with 'https' instead of 'http' and compare it + // to the current URI + int32_t port = 0; + nsresult rv = aOtherURI->GetPort(&port); + NS_ENSURE_SUCCESS(rv, false); + // a port of -1 indicates the default port, hence we upgrade from port 80 to + // port 443 + // otherwise we keep the port. + if (port == -1) { + port = NS_GetDefaultPort("https"); + } + nsCOMPtr<nsIURI> newHTTPSchemeURI; + rv = NS_MutateURI(aOtherURI) + .SetScheme("https"_ns) + .SetPort(port) + .Finalize(newHTTPSchemeURI); + NS_ENSURE_SUCCESS(rv, false); + + bool uriEquals = false; + if (NS_FAILED( + aHTTPSSchemeURI->EqualsExceptRef(newHTTPSchemeURI, &uriEquals))) { + return false; + } + + return uriEquals; +} +///////////////////////////////////////////////////////////////////// +// Implementation of TestHTTPAnswerRunnable + +NS_IMPL_ISUPPORTS_INHERITED(TestHTTPAnswerRunnable, mozilla::Runnable, + nsIStreamListener, nsIInterfaceRequestor, + nsITimerCallback) + +TestHTTPAnswerRunnable::TestHTTPAnswerRunnable( + nsIURI* aURI, mozilla::net::DocumentLoadListener* aDocumentLoadListener) + : mozilla::Runnable("TestHTTPAnswerRunnable"), + mURI(aURI), + mDocumentLoadListener(aDocumentLoadListener) {} + +/* static */ +bool TestHTTPAnswerRunnable::IsBackgroundRequestRedirected( + nsIHttpChannel* aChannel) { + // If there is no background request (aChannel), then there is nothing + // to do here. + if (!aChannel) { + return false; + } + // If the request was not redirected, then there is nothing to do here. + nsCOMPtr<nsILoadInfo> loadinfo = aChannel->LoadInfo(); + if (loadinfo->RedirectChain().IsEmpty()) { + return false; + } + + // If the final URI is not targeting an https scheme, then we definitely not + // dealing with a 'same-origin' redirect. + nsCOMPtr<nsIURI> finalURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI)); + NS_ENSURE_SUCCESS(rv, false); + if (!finalURI->SchemeIs("https")) { + return false; + } + + // If the background request was not http, then there is nothing to do here. + nsCOMPtr<nsIPrincipal> firstURIPrincipal; + loadinfo->RedirectChain()[0]->GetPrincipal(getter_AddRefs(firstURIPrincipal)); + if (!firstURIPrincipal || !firstURIPrincipal->SchemeIs("http")) { + return false; + } + + // By now we have verified that the inital background request was http and + // that the redirected scheme is https. We want to find the following case + // where the background channel redirects to the https version of the + // top-level request. + // --> background channel: http://example.com + // |--> redirects to: https://example.com + // Now we have to check that the hosts are 'same-origin'. + nsAutoCString redirectHost; + nsAutoCString finalHost; + firstURIPrincipal->GetAsciiHost(redirectHost); + finalURI->GetAsciiHost(finalHost); + return finalHost.Equals(redirectHost); +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::OnStartRequest(nsIRequest* aRequest) { + // If the request status is not OK, it means it encountered some + // kind of error in which case we do not want to do anything. + nsresult requestStatus; + aRequest->GetStatus(&requestStatus); + if (requestStatus != NS_OK) { + return NS_OK; + } + + // Check if the original top-level channel which https-only is trying + // to upgrade is already in progress or if the channel is an auth channel. + // If it is in progress or Auth is in progress, then all good, if not + // then let's cancel that channel so we can dispaly the exception page. + nsCOMPtr<nsIChannel> docChannel = mDocumentLoadListener->GetChannel(); + nsCOMPtr<nsIHttpChannel> httpsOnlyChannel = do_QueryInterface(docChannel); + if (httpsOnlyChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = httpsOnlyChannel->LoadInfo(); + uint32_t topLevelLoadInProgress = + loadInfo->GetHttpsOnlyStatus() & + nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS; + + nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal = + do_QueryInterface(httpsOnlyChannel); + bool isAuthChannel = false; + mozilla::Unused << httpChannelInternal->GetIsAuthChannel(&isAuthChannel); + // some server configurations need a long time to respond to an https + // connection, but also redirect any http connection to the https version of + // it. If the top-level load has not started yet, but the http background + // request redirects to https, then do not show the error page, but keep + // waiting for the https response of the upgraded top-level request. + if (!topLevelLoadInProgress) { + nsCOMPtr<nsIHttpChannel> backgroundHttpChannel = + do_QueryInterface(aRequest); + topLevelLoadInProgress = + IsBackgroundRequestRedirected(backgroundHttpChannel); + } + if (!topLevelLoadInProgress && !isAuthChannel) { + // Only really cancel the original top-level channel if it's + // status is still NS_OK, otherwise it might have already + // encountered some other error and was cancelled. + nsresult httpsOnlyChannelStatus; + httpsOnlyChannel->GetStatus(&httpsOnlyChannelStatus); + if (httpsOnlyChannelStatus == NS_OK) { + httpsOnlyChannel->Cancel(NS_ERROR_NET_TIMEOUT_EXTERNAL); + } + } + } + + // Cancel this http request because it has reached the end of it's + // lifetime at this point. + aRequest->Cancel(NS_ERROR_ABORT); + return NS_ERROR_ABORT; +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + // TestHTTPAnswerRunnable only cares about ::OnStartRequest which + // will also cancel the request, so we should in fact never even + // get here. + MOZ_ASSERT(false, "how come we get to ::OnDataAvailable"); + return NS_OK; +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + // TestHTTPAnswerRunnable only cares about ::OnStartRequest + return NS_OK; +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::GetInterface(const nsIID& aIID, void** aResult) { + return QueryInterface(aIID, aResult); +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::Run() { + // Wait N milliseconds to give the original https request a heads start + // before firing up this http request in the background. By default the + // timer is set to 3 seconds. If the https request has not received + // any signal from the server during that time, than it's almost + // certain the upgraded request will result in time out. + uint32_t background_timer_ms = mozilla::StaticPrefs:: + dom_security_https_only_fire_http_request_background_timer_ms(); + + return NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, + background_timer_ms, nsITimer::TYPE_ONE_SHOT); +} + +NS_IMETHODIMP +TestHTTPAnswerRunnable::Notify(nsITimer* aTimer) { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + // If the original channel has already started loading at this point + // then there is no need to do the dance. + nsCOMPtr<nsIChannel> origChannel = mDocumentLoadListener->GetChannel(); + nsCOMPtr<nsILoadInfo> origLoadInfo = origChannel->LoadInfo(); + uint32_t origHttpsOnlyStatus = origLoadInfo->GetHttpsOnlyStatus(); + uint32_t topLevelLoadInProgress = + origHttpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_TOP_LEVEL_LOAD_IN_PROGRESS; + uint32_t downloadInProgress = + origHttpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS; + if (topLevelLoadInProgress || downloadInProgress) { + return NS_OK; + } + + mozilla::OriginAttributes attrs = origLoadInfo->GetOriginAttributes(); + RefPtr<nsIPrincipal> nullPrincipal = mozilla::NullPrincipal::Create(attrs); + + uint32_t loadFlags = + nsIRequest::LOAD_ANONYMOUS | nsIRequest::INHIBIT_CACHING | + nsIRequest::INHIBIT_PERSISTENT_CACHING | nsIRequest::LOAD_BYPASS_CACHE | + nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + + // No need to connect to the URI including the path because we only care about + // the round trip time if a server responds to an http request. + nsCOMPtr<nsIURI> backgroundChannelURI; + nsAutoCString prePathStr; + nsresult rv = mURI->GetPrePath(prePathStr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = NS_NewURI(getter_AddRefs(backgroundChannelURI), prePathStr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // we are using TYPE_OTHER because TYPE_DOCUMENT might have side effects + nsCOMPtr<nsIChannel> testHTTPChannel; + rv = NS_NewChannel(getter_AddRefs(testHTTPChannel), backgroundChannelURI, + nullPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER, nullptr, nullptr, nullptr, + nullptr, loadFlags); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We have exempt that load from HTTPS-Only to avoid getting upgraded + // to https as well. Additonally let's not log that request to the console + // because it might confuse end users. + nsCOMPtr<nsILoadInfo> loadInfo = testHTTPChannel->LoadInfo(); + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_EXEMPT | + nsILoadInfo::HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE | + nsILoadInfo::HTTPS_ONLY_BYPASS_ORB; + loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); + + testHTTPChannel->SetNotificationCallbacks(this); + testHTTPChannel->AsyncOpen(this); + return NS_OK; +} |