diff options
Diffstat (limited to 'dom/security/SecFetch.cpp')
-rw-r--r-- | dom/security/SecFetch.cpp | 411 |
1 files changed, 411 insertions, 0 deletions
diff --git a/dom/security/SecFetch.cpp b/dom/security/SecFetch.cpp new file mode 100644 index 0000000000..b25cd71016 --- /dev/null +++ b/dom/security/SecFetch.cpp @@ -0,0 +1,411 @@ +/* -*- 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 "nsContentUtils.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIReferrerInfo.h" +#include "mozIThirdPartyUtil.h" +#include "nsMixedContentBlocker.h" +#include "nsNetUtil.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/StaticPrefs_dom.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 "empty"_ns; + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_MODULE: + case nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS: + case nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT: + case nsIContentPolicy::TYPE_SCRIPT: + return "script"_ns; + case nsIContentPolicy::TYPE_INTERNAL_WORKER: + case nsIContentPolicy::TYPE_INTERNAL_WORKER_STATIC_MODULE: + return "worker"_ns; + case nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER: + return "sharedworker"_ns; + case nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER: + return "serviceworker"_ns; + case nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET: + return "audioworklet"_ns; + case nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET: + return "paintworklet"_ns; + 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 "image"_ns; + case nsIContentPolicy::TYPE_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD: + return "style"_ns; + case nsIContentPolicy::TYPE_OBJECT: + case nsIContentPolicy::TYPE_INTERNAL_OBJECT: + return "object"_ns; + case nsIContentPolicy::TYPE_INTERNAL_EMBED: + return "embed"_ns; + case nsIContentPolicy::TYPE_DOCUMENT: + return "document"_ns; + case nsIContentPolicy::TYPE_SUBDOCUMENT: + case nsIContentPolicy::TYPE_INTERNAL_IFRAME: + return "iframe"_ns; + case nsIContentPolicy::TYPE_INTERNAL_FRAME: + return "frame"_ns; + case nsIContentPolicy::TYPE_PING: + return "empty"_ns; + case nsIContentPolicy::TYPE_XMLHTTPREQUEST: + case nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST: + return "empty"_ns; + case nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE: + return "empty"_ns; + case nsIContentPolicy::TYPE_OBJECT_SUBREQUEST: + return "empty"_ns; + case nsIContentPolicy::TYPE_DTD: + case nsIContentPolicy::TYPE_INTERNAL_DTD: + case nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD: + return "empty"_ns; + case nsIContentPolicy::TYPE_FONT: + case nsIContentPolicy::TYPE_INTERNAL_FONT_PRELOAD: + case nsIContentPolicy::TYPE_UA_FONT: + return "font"_ns; + case nsIContentPolicy::TYPE_MEDIA: + return "empty"_ns; + case nsIContentPolicy::TYPE_INTERNAL_AUDIO: + return "audio"_ns; + case nsIContentPolicy::TYPE_INTERNAL_VIDEO: + return "video"_ns; + case nsIContentPolicy::TYPE_INTERNAL_TRACK: + return "track"_ns; + case nsIContentPolicy::TYPE_WEBSOCKET: + return "websocket"_ns; + case nsIContentPolicy::TYPE_CSP_REPORT: + return "report"_ns; + case nsIContentPolicy::TYPE_XSLT: + return "xslt"_ns; + case nsIContentPolicy::TYPE_BEACON: + return "empty"_ns; + case nsIContentPolicy::TYPE_FETCH: + case nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD: + return "empty"_ns; + case nsIContentPolicy::TYPE_WEB_MANIFEST: + return "manifest"_ns; + case nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD: + return "empty"_ns; + case nsIContentPolicy::TYPE_SPECULATIVE: + return "empty"_ns; + case nsIContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA: + return "empty"_ns; + case nsIContentPolicy::TYPE_WEB_IDENTITY: + return "webidentity"_ns; + case nsIContentPolicy::TYPE_WEB_TRANSPORT: + return "webtransport"_ns; + case nsIContentPolicy::TYPE_END: + case nsIContentPolicy::TYPE_INVALID: + break; + // Do not add default: so that compilers can catch the missing case. + } + + MOZ_CRASH("Unhandled nsContentPolicyType value"); +} + +// Helper function to determine if a ExpandedPrincipal is of the same-origin as +// a URI in the sec-fetch context. +void IsExpandedPrincipalSameOrigin( + nsCOMPtr<nsIExpandedPrincipal> aExpandedPrincipal, nsIURI* aURI, + bool* aRes) { + *aRes = false; + for (const auto& principal : aExpandedPrincipal->AllowList()) { + // Ignore extension principals to continue treating + // "moz-extension:"-requests as not "same-origin". + if (!mozilla::BasePrincipal::Cast(principal)->AddonPolicy()) { + // A ExpandedPrincipal usually has at most one ContentPrincipal, so we can + // check IsSameOrigin on it here and return early. + mozilla::BasePrincipal::Cast(principal)->IsSameOrigin(aURI, aRes); + return; + } + } +} + +// Helper function to determine whether a request (including involved +// redirects) is same-origin in the context of SecFetch. +bool IsSameOrigin(nsIHttpChannel* aHTTPChannel) { + nsCOMPtr<nsIURI> channelURI; + NS_GetFinalChannelURI(aHTTPChannel, getter_AddRefs(channelURI)); + + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + + if (mozilla::BasePrincipal::Cast(loadInfo->TriggeringPrincipal()) + ->AddonPolicy()) { + // If an extension triggered the load that has access to the URI then the + // load is considered as same-origin. + return mozilla::BasePrincipal::Cast(loadInfo->TriggeringPrincipal()) + ->AddonAllowsLoad(channelURI); + } + + bool isSameOrigin = false; + if (nsContentUtils::IsExpandedPrincipal(loadInfo->TriggeringPrincipal())) { + nsCOMPtr<nsIExpandedPrincipal> ep = + do_QueryInterface(loadInfo->TriggeringPrincipal()); + IsExpandedPrincipalSameOrigin(ep, channelURI, &isSameOrigin); + } else { + isSameOrigin = loadInfo->TriggeringPrincipal()->IsSameOrigin(channelURI); + } + + // if the initial request is not same-origin, we can return here + // because we already know it's not a same-origin request + if (!isSameOrigin) { + return false; + } + + // let's further check all the hoops in the redirectChain to + // ensure all involved redirects are same-origin + nsCOMPtr<nsIPrincipal> redirectPrincipal; + for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) { + entry->GetPrincipal(getter_AddRefs(redirectPrincipal)); + if (redirectPrincipal && !redirectPrincipal->IsSameOrigin(channelURI)) { + 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<mozIThirdPartyUtil> thirdPartyUtil = + do_GetService(THIRDPARTYUTIL_CONTRACTID); + if (!thirdPartyUtil) { + return false; + } + + nsAutoCString hostDomain; + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + nsresult rv = loadInfo->TriggeringPrincipal()->GetBaseDomain(hostDomain); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + nsAutoCString channelDomain; + nsCOMPtr<nsIURI> channelURI; + NS_GetFinalChannelURI(aHTTPChannel, getter_AddRefs(channelURI)); + rv = thirdPartyUtil->GetBaseDomain(channelURI, channelDomain); + mozilla::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 + if (!hostDomain.Equals(channelDomain) || + (!loadInfo->TriggeringPrincipal()->SchemeIs("https") && + !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost( + hostDomain))) { + return false; + } + + // let's further check all the hoops in the redirectChain to + // ensure all involved redirects are same-site and https + nsCOMPtr<nsIPrincipal> redirectPrincipal; + for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) { + entry->GetPrincipal(getter_AddRefs(redirectPrincipal)); + if (redirectPrincipal) { + redirectPrincipal->GetBaseDomain(hostDomain); + if (!hostDomain.Equals(channelDomain) || + !redirectPrincipal->SchemeIs("https")) { + 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) { + /* + * The goal is to distinguish between "webby" navigations that are controlled + * by a given website (e.g. links, the window.location setter,form + * submissions, etc.), and those that are not (e.g. user interaction with a + * user agent’s address bar, bookmarks, etc). + */ + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType(); + + // A request issued by the browser is always user initiated. + if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal() && + contentType == ExtContentPolicy::TYPE_OTHER) { + return true; + } + + // only requests wich result in type "document" are subject to + // user initiated actions in the context of SecFetch. + if (contentType != ExtContentPolicy::TYPE_DOCUMENT && + contentType != ExtContentPolicy::TYPE_SUBDOCUMENT) { + return false; + } + + // The load is considered user triggered if it was triggered by an external + // application. + if (loadInfo->GetLoadTriggeredFromExternal()) { + return true; + } + + // sec-fetch-site can only be user triggered if the load was user triggered. + if (!loadInfo->GetHasValidUserGestureActivation()) { + return false; + } + + // We can assert that the navigation must be "webby" if the load was triggered + // by a meta refresh. See also Bug 1647128. + if (loadInfo->GetIsMetaRefresh()) { + return false; + } + + // All web requests have a valid "original" referrer set in the + // ReferrerInfo which we can use to determine whether a request + // was triggered by a user or not. + nsCOMPtr<nsIReferrerInfo> referrerInfo = aHTTPChannel->GetReferrerInfo(); + if (referrerInfo) { + nsCOMPtr<nsIURI> originalReferrer; + referrerInfo->GetOriginalReferrer(getter_AddRefs(originalReferrer)); + if (originalReferrer) { + return false; + } + } + + return true; +} + +void mozilla::dom::SecFetch::AddSecFetchDest(nsIHttpChannel* aHTTPChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + nsContentPolicyType contentType = loadInfo->InternalContentPolicyType(); + nsCString dest = MapInternalContentPolicyTypeToDest(contentType); + + nsresult rv = + aHTTPChannel->SetRequestHeader("Sec-Fetch-Dest"_ns, dest, false); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void mozilla::dom::SecFetch::AddSecFetchMode(nsIHttpChannel* aHTTPChannel) { + nsAutoCString mode("no-cors"); + + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + uint32_t securityMode = loadInfo->GetSecurityMode(); + ExtContentPolicyType externalType = loadInfo->GetExternalContentPolicyType(); + + if (securityMode == + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT || + securityMode == nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED) { + mode = "same-origin"_ns; + } else if (securityMode == + nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) { + mode = "cors"_ns; + } 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_INHERITS_SEC_CONTEXT || + securityMode == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + "unhandled security mode"); + } + + if (externalType == ExtContentPolicy::TYPE_DOCUMENT || + externalType == ExtContentPolicy::TYPE_SUBDOCUMENT || + externalType == ExtContentPolicy::TYPE_OBJECT) { + mode = "navigate"_ns; + } else if (externalType == ExtContentPolicy::TYPE_WEBSOCKET) { + mode = "websocket"_ns; + } + + nsresult rv = + aHTTPChannel->SetRequestHeader("Sec-Fetch-Mode"_ns, mode, false); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void mozilla::dom::SecFetch::AddSecFetchSite(nsIHttpChannel* aHTTPChannel) { + nsAutoCString site("same-origin"); + + bool isSameOrigin = IsSameOrigin(aHTTPChannel); + if (!isSameOrigin) { + bool isSameSite = IsSameSite(aHTTPChannel); + if (isSameSite) { + site = "same-site"_ns; + } else { + site = "cross-site"_ns; + } + } + + if (IsUserTriggeredForSecFetchSite(aHTTPChannel)) { + site = "none"_ns; + } + + nsresult rv = + aHTTPChannel->SetRequestHeader("Sec-Fetch-Site"_ns, site, false); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void mozilla::dom::SecFetch::AddSecFetchUser(nsIHttpChannel* aHTTPChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + ExtContentPolicyType externalType = loadInfo->GetExternalContentPolicyType(); + + // sec-fetch-user only applies to loads of type document or subdocument + if (externalType != ExtContentPolicy::TYPE_DOCUMENT && + externalType != ExtContentPolicy::TYPE_SUBDOCUMENT) { + return; + } + + // sec-fetch-user only applies if the request is user triggered. + // requests triggered by an external application are considerd user triggered. + if (!loadInfo->GetLoadTriggeredFromExternal() && + !loadInfo->GetHasValidUserGestureActivation()) { + return; + } + + nsAutoCString user("?1"); + nsresult rv = + aHTTPChannel->SetRequestHeader("Sec-Fetch-User"_ns, user, false); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +void mozilla::dom::SecFetch::AddSecFetchHeader(nsIHttpChannel* aHTTPChannel) { + nsCOMPtr<nsIURI> 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; + } + + // If we're dealing with a system XMLHttpRequest or fetch, don't add + // Sec- headers. + nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); + if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { + ExtContentPolicy extType = loadInfo->GetExternalContentPolicyType(); + if (extType == ExtContentPolicy::TYPE_FETCH || + extType == ExtContentPolicy::TYPE_XMLHTTPREQUEST) { + return; + } + } + + AddSecFetchDest(aHTTPChannel); + AddSecFetchMode(aHTTPChannel); + AddSecFetchSite(aHTTPChannel); + AddSecFetchUser(aHTTPChannel); +} |