diff --git a/dom/security/SecFetch.cpp b/dom/security/SecFetch.cpp new file mode 100644 index 0000000000..67a9fce29c --- /dev/null +++ b/dom/security/SecFetch.cpp @@ -0,0 +1,305 @@ +/* -*- 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 "SecFetch.h" +#include "nsIHttpChannel.h" +#include "mozIThirdPartyUtil.h" +#include "nsMixedContentBlocker.h" +#include "nsNetUtil.h" + +// Helper function which maps an internal content policy type +// to the corresponding destination for the context of SecFetch. +nsCString MapInternalContentPolicyTypeToDest(nsContentPolicyType aType) { + switch (aType) { + case nsIContentPolicy::TYPE_OTHER: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS: + case nsIContentPolicy::TYPE_SCRIPT: + return NS_LITERAL_CSTRING("script"); + case nsIContentPolicy::TYPE_INTERNAL_WORKER: + return NS_LITERAL_CSTRING("worker"); + case nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER: + return NS_LITERAL_CSTRING("sharedworker"); + case nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER: + return NS_LITERAL_CSTRING("serviceworker"); + case nsIContentPolicy::TYPE_IMAGESET: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON: + case nsIContentPolicy::TYPE_IMAGE: + return NS_LITERAL_CSTRING("image"); + case nsIContentPolicy::TYPE_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD: + return NS_LITERAL_CSTRING("style"); + case nsIContentPolicy::TYPE_OBJECT: + case nsIContentPolicy::TYPE_INTERNAL_OBJECT: + return NS_LITERAL_CSTRING("object"); + case nsIContentPolicy::TYPE_INTERNAL_EMBED: + return NS_LITERAL_CSTRING("embed"); + case nsIContentPolicy::TYPE_DOCUMENT: + return NS_LITERAL_CSTRING("document"); + case nsIContentPolicy::TYPE_SUBDOCUMENT: + case nsIContentPolicy::TYPE_INTERNAL_IFRAME: + return NS_LITERAL_CSTRING("iframe"); + case nsIContentPolicy::TYPE_INTERNAL_FRAME: + return NS_LITERAL_CSTRING("frame"); + case nsIContentPolicy::TYPE_REFRESH: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_XBL: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_PING: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_XMLHTTPREQUEST: + case nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_OBJECT_SUBREQUEST: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_DTD: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_FONT: + return NS_LITERAL_CSTRING("font"); + case nsIContentPolicy::TYPE_MEDIA: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_INTERNAL_AUDIO: + return NS_LITERAL_CSTRING("audio"); + case nsIContentPolicy::TYPE_INTERNAL_VIDEO: + return NS_LITERAL_CSTRING("video"); + case nsIContentPolicy::TYPE_INTERNAL_TRACK: + return NS_LITERAL_CSTRING("track"); + case nsIContentPolicy::TYPE_WEBSOCKET: + return NS_LITERAL_CSTRING("websocket"); + case nsIContentPolicy::TYPE_CSP_REPORT: + return NS_LITERAL_CSTRING("report"); + case nsIContentPolicy::TYPE_XSLT: + return NS_LITERAL_CSTRING("xslt"); + case nsIContentPolicy::TYPE_BEACON: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_FETCH: + return NS_LITERAL_CSTRING("empty"); + case nsIContentPolicy::TYPE_WEB_MANIFEST: + return NS_LITERAL_CSTRING("manifest"); + case nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD: + return NS_LITERAL_CSTRING("empty"); + default: + MOZ_CRASH("Unhandled nsContentPolicyType value"); + break; + } + + return NS_LITERAL_CSTRING("empty"); +} + +// Helper function to determine whether a request (including involved +// redirects) is same-origin in the context of SecFetch. +bool IsSameOrigin(nsIHttpChannel* aHTTPChannel) { + nsCOMPtr channelURI; + NS_GetFinalChannelURI(aHTTPChannel, getter_AddRefs(channelURI)); + + nsCOMPtr loadInfo = aHTTPChannel->GetLoadInfo(); + bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + + // if the initial request is not same-origin, we can return here + // because we already know it's not a same-origin request + nsAutoCString triggeringOrigin, loadingOrigin, redirectOrigin; + + nsresult rv = loadInfo->TriggeringPrincipal()->GetOrigin(triggeringOrigin); + if NS_FAILED(rv) { + // Assume same origin + return true; + } + rv = loadInfo->LoadingPrincipal()->GetOrigin(loadingOrigin); + if NS_FAILED(rv) { + // Assume same origin + return true; + } + + if (triggeringOrigin != loadingOrigin) { + return false; + } + + // let's further check all the hoops in the redirectChain to + // ensure all involved redirects are same-origin + for (nsIPrincipal* principal : loadInfo->RedirectChain()) { + if (principal) { + rv = principal->GetOrigin(redirectOrigin); + if NS_FAILED(rv) + continue; + if (loadingOrigin != redirectOrigin) { + return false; + } + } + } + + // must be a same-origin request + return true; +} + +// Helper function to determine whether a request (including involved +// redirects) is same-site in the context of SecFetch. +bool IsSameSite(nsIChannel* aHTTPChannel) { + nsCOMPtr thirdPartyUtil = + do_GetService(THIRDPARTYUTIL_CONTRACTID); + if (!thirdPartyUtil) { + return false; + } + + nsAutoCString hostDomain; + nsCOMPtr loadInfo = aHTTPChannel->GetLoadInfo(); + nsresult rv = loadInfo->TriggeringPrincipal()->GetBaseDomain(hostDomain); + Unused << NS_WARN_IF(NS_FAILED(rv)); + + nsAutoCString channelDomain; + nsCOMPtr channelURI; + NS_GetFinalChannelURI(aHTTPChannel, getter_AddRefs(channelURI)); + rv = thirdPartyUtil->GetBaseDomain(channelURI, channelDomain); + Unused << NS_WARN_IF(NS_FAILED(rv)); + + // if the initial request is not same-site, or not https, we can + // return here because we already know it's not a same-site request + bool usingHttps = false; + rv = channelURI->SchemeIs("https", &usingHttps); + if (!hostDomain.Equals(channelDomain) || !usingHttps) { + return false; + } + + // let's further check all the hoops in the redirectChain to + // ensure all involved redirects are same-site and https + for (nsIPrincipal* principal : loadInfo->RedirectChain()) { + if (principal) { + principal->GetBaseDomain(hostDomain); + nsCOMPtr redirectURI; + principal->GetURI(getter_AddRefs(redirectURI)); + rv = redirectURI->SchemeIs("https", &usingHttps); + if (NS_FAILED(rv) || !hostDomain.Equals(channelDomain) || !usingHttps) { + return false; + } + } + } + + // must be a same-site request + return true; +} + +// Helper function to determine whether a request was triggered +// by the end user in the context of SecFetch. +bool IsUserTriggeredForSecFetchSite(nsIHttpChannel* aHTTPChannel) { + nsCOMPtr loadInfo = aHTTPChannel->GetLoadInfo(); + nsContentPolicyType contentType = loadInfo->InternalContentPolicyType(); + + // only requests wich result in type "document" are subject to + // user initiated actions in the context of SecFetch. + if (contentType != nsIContentPolicy::TYPE_DOCUMENT && + contentType != nsIContentPolicy::TYPE_SUBDOCUMENT && + contentType != nsIContentPolicy::TYPE_INTERNAL_IFRAME) { + return false; + } + + return true; +} + +void SecFetch::AddSecFetchDest(nsIHttpChannel* aHTTPChannel) { + nsCOMPtr loadInfo = aHTTPChannel->GetLoadInfo(); + nsContentPolicyType contentType = loadInfo->InternalContentPolicyType(); + nsCString dest = MapInternalContentPolicyTypeToDest(contentType); + + nsresult rv = aHTTPChannel->SetRequestHeader( + NS_LITERAL_CSTRING("Sec-Fetch-Dest"), dest, false); + Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void SecFetch::AddSecFetchMode(nsIHttpChannel* aHTTPChannel) { + nsAutoCString mode("no-cors"); + + nsCOMPtr loadInfo = aHTTPChannel->GetLoadInfo(); + uint32_t securityMode = loadInfo->GetSecurityMode(); + nsContentPolicyType externalType = loadInfo->GetExternalContentPolicyType(); + + if (securityMode == nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_INHERITS || + securityMode == nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED) { + mode = NS_LITERAL_CSTRING("same-origin"); + } else if (securityMode == nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS) { + mode = NS_LITERAL_CSTRING("cors"); + } else { + // If it's not one of the security modes above, then we ensure it's + // at least one of the others defined in nsILoadInfo + MOZ_ASSERT( + securityMode == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS || + securityMode == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + "unhandled security mode"); + } + + if (externalType == nsIContentPolicy::TYPE_DOCUMENT || + externalType == nsIContentPolicy::TYPE_SUBDOCUMENT || + externalType == nsIContentPolicy::TYPE_REFRESH || + externalType == nsIContentPolicy::TYPE_OBJECT) { + mode = NS_LITERAL_CSTRING("navigate"); + } else if (externalType == nsIContentPolicy::TYPE_WEBSOCKET) { + mode = NS_LITERAL_CSTRING("websocket"); + } + + nsresult rv = aHTTPChannel->SetRequestHeader( + NS_LITERAL_CSTRING("Sec-Fetch-Mode"), mode, false); + Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void SecFetch::AddSecFetchSite(nsIHttpChannel* aHTTPChannel) { + nsAutoCString site("same-origin"); + + bool isSameOrigin = IsSameOrigin(aHTTPChannel); + if (!isSameOrigin) { + bool isSameSite = IsSameSite(aHTTPChannel); + if (isSameSite) { + site = NS_LITERAL_CSTRING("same-site"); + } else { + site = NS_LITERAL_CSTRING("cross-site"); + } + } + + if (IsUserTriggeredForSecFetchSite(aHTTPChannel)) { + site = NS_LITERAL_CSTRING("none"); + } + + nsresult rv = aHTTPChannel->SetRequestHeader( + NS_LITERAL_CSTRING("Sec-Fetch-Site"), site, false); + Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void SecFetch::AddSecFetchUser(nsIHttpChannel* aHTTPChannel) { + // TODO: Bug 1621987: Implement Sec-Fetch-User + + // nsAutoCString user("?1"); + // nsresult rv = aHTTPChannel->SetRequestHeader( + // NS_LITERAL_CSTRING("Sec-Fetch-User"), user, false); + // Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void SecFetch::AddSecFetchHeader(nsIHttpChannel* aHTTPChannel) { + // if sec-fetch-* is prefed off, then there is nothing to do + if (Preferences::GetBool("network.http.secfetch.enabled",false)) { + return; + } + + nsCOMPtr uri; + nsresult rv = aHTTPChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // if we are not dealing with a potentially trustworthy URL, then + // there is nothing to do here + if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) { + return; + } + + AddSecFetchDest(aHTTPChannel); + AddSecFetchMode(aHTTPChannel); + AddSecFetchSite(aHTTPChannel); + AddSecFetchUser(aHTTPChannel); +} diff --git a/dom/security/SecFetch.h b/dom/security/SecFetch.h new file mode 100644 index 0000000000..9ebec60de5 --- /dev/null +++ b/dom/security/SecFetch.h @@ -0,0 +1,29 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_SecFetch_h +#define mozilla_dom_SecFetch_h + +class nsIHttpChannel; + +namespace mozilla { +namespace dom { + +class SecFetch final { + public: + static void AddSecFetchHeader(nsIHttpChannel* aHTTPChannel); + + private: + static void AddSecFetchDest(nsIHttpChannel* aHTTPChannel); + static void AddSecFetchMode(nsIHttpChannel* aHTTPChannel); + static void AddSecFetchSite(nsIHttpChannel* aHTTPChannel); + static void AddSecFetchUser(nsIHttpChannel* aHTTPChannel); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_SecFetch_h diff --git a/dom/security/moz.build b/dom/security/moz.build index 224b7a4a9a..e57c0f0cc9 100644 --- a/dom/security/moz.build +++ b/dom/security/moz.build @@ -13,6 +13,7 @@ EXPORTS.mozilla.dom += [ 'nsCSPService.h', 'nsCSPUtils.h', 'nsMixedContentBlocker.h', + 'SecFetch.h', 'SRICheck.h', 'SRILogHelper.h', 'SRIMetadata.h', @@ -32,6 +33,7 @@ UNIFIED_SOURCES += [ 'nsCSPService.cpp', 'nsCSPUtils.cpp', 'nsMixedContentBlocker.cpp', + 'SecFetch.cpp', 'SRICheck.cpp', 'SRIMetadata.cpp', ] diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 9d11726d47..a58e378cc6 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -1616,6 +1616,8 @@ pref("network.http.altsvc.enabled", true); pref("network.http.altsvc.oe", false); // Send upgrade-insecure-requests HTTP header? pref("network.http.upgrade-insecure-requests", false); +// Send Sec-Fetch-* headers? +pref("network.http.secfetch.enabled", false); pref("network.http.diagnostics", false); diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp index ea95740870..d00cf377cc 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -99,6 +99,7 @@ #include "CacheControlParser.h" #include "nsMixedContentBlocker.h" #include "CacheStorageService.h" +#include "mozilla/dom/SecFetch.h" namespace mozilla { namespace net { @@ -380,6 +381,8 @@ nsHttpChannel::Connect() NS_LITERAL_CSTRING("1"), false); NS_ENSURE_SUCCESS(rv, rv); } + + mozilla::dom::SecFetch::AddSecFetchHeader(this); bool isHttps = false; rv = mURI->SchemeIs("https", &isHttps);