gecko-dev/dom/security/SecFetch.cpp

404 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- 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_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 agents 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) {
// if sec-fetch-* is prefed off, then there is nothing to do
if (!StaticPrefs::dom_security_secFetch_enabled()) {
return;
}
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;
}
AddSecFetchDest(aHTTPChannel);
AddSecFetchMode(aHTTPChannel);
AddSecFetchSite(aHTTPChannel);
AddSecFetchUser(aHTTPChannel);
}