SecFetch.cpp (14881B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "SecFetch.h" 8 9 #include "mozIThirdPartyUtil.h" 10 #include "mozilla/BasePrincipal.h" 11 #include "mozilla/StaticPrefs_dom.h" 12 #include "mozilla/dom/RequestBinding.h" 13 #include "nsContentSecurityManager.h" 14 #include "nsContentUtils.h" 15 #include "nsIHttpChannel.h" 16 #include "nsIRedirectHistoryEntry.h" 17 #include "nsIReferrerInfo.h" 18 #include "nsMixedContentBlocker.h" 19 #include "nsNetUtil.h" 20 21 // Helper function which maps an internal content policy type 22 // to the corresponding destination for the context of SecFetch. 23 nsCString MapInternalContentPolicyTypeToDest(nsContentPolicyType aType) { 24 switch (aType) { 25 case nsIContentPolicy::TYPE_OTHER: 26 return "empty"_ns; 27 case nsIContentPolicy::TYPE_INTERNAL_SCRIPT: 28 case nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD: 29 case nsIContentPolicy::TYPE_INTERNAL_MODULE: 30 case nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD: 31 case nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS: 32 case nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT: 33 case nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT: 34 case nsIContentPolicy::TYPE_SCRIPT: 35 return "script"_ns; 36 case nsIContentPolicy::TYPE_JSON: 37 case nsIContentPolicy::TYPE_INTERNAL_JSON_PRELOAD: 38 return "json"_ns; 39 case nsIContentPolicy::TYPE_INTERNAL_WORKER: 40 case nsIContentPolicy::TYPE_INTERNAL_WORKER_STATIC_MODULE: 41 return "worker"_ns; 42 case nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER: 43 return "sharedworker"_ns; 44 case nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER: 45 return "serviceworker"_ns; 46 case nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET: 47 return "audioworklet"_ns; 48 case nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET: 49 return "paintworklet"_ns; 50 case nsIContentPolicy::TYPE_IMAGESET: 51 case nsIContentPolicy::TYPE_INTERNAL_IMAGE: 52 case nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD: 53 case nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON: 54 case nsIContentPolicy::TYPE_IMAGE: 55 return "image"_ns; 56 case nsIContentPolicy::TYPE_STYLESHEET: 57 case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET: 58 case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD: 59 return "style"_ns; 60 case nsIContentPolicy::TYPE_OBJECT: 61 case nsIContentPolicy::TYPE_INTERNAL_OBJECT: 62 return "object"_ns; 63 case nsIContentPolicy::TYPE_INTERNAL_EMBED: 64 return "embed"_ns; 65 case nsIContentPolicy::TYPE_DOCUMENT: 66 return "document"_ns; 67 case nsIContentPolicy::TYPE_SUBDOCUMENT: 68 case nsIContentPolicy::TYPE_INTERNAL_IFRAME: 69 return "iframe"_ns; 70 case nsIContentPolicy::TYPE_INTERNAL_FRAME: 71 return "frame"_ns; 72 case nsIContentPolicy::TYPE_PING: 73 return "empty"_ns; 74 case nsIContentPolicy::TYPE_XMLHTTPREQUEST: 75 case nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST_ASYNC: 76 case nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST_SYNC: 77 return "empty"_ns; 78 case nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE: 79 return "empty"_ns; 80 case nsIContentPolicy::TYPE_DTD: 81 case nsIContentPolicy::TYPE_INTERNAL_DTD: 82 case nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD: 83 return "empty"_ns; 84 case nsIContentPolicy::TYPE_FONT: 85 case nsIContentPolicy::TYPE_INTERNAL_FONT_PRELOAD: 86 case nsIContentPolicy::TYPE_UA_FONT: 87 return "font"_ns; 88 case nsIContentPolicy::TYPE_MEDIA: 89 return "empty"_ns; 90 case nsIContentPolicy::TYPE_INTERNAL_AUDIO: 91 return "audio"_ns; 92 case nsIContentPolicy::TYPE_INTERNAL_VIDEO: 93 return "video"_ns; 94 case nsIContentPolicy::TYPE_INTERNAL_TRACK: 95 return "track"_ns; 96 case nsIContentPolicy::TYPE_WEBSOCKET: 97 return "empty"_ns; 98 case nsIContentPolicy::TYPE_CSP_REPORT: 99 return "report"_ns; 100 case nsIContentPolicy::TYPE_XSLT: 101 return "xslt"_ns; 102 case nsIContentPolicy::TYPE_BEACON: 103 return "empty"_ns; 104 case nsIContentPolicy::TYPE_FETCH: 105 case nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD: 106 return "empty"_ns; 107 case nsIContentPolicy::TYPE_WEB_MANIFEST: 108 return "manifest"_ns; 109 case nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD: 110 return "empty"_ns; 111 case nsIContentPolicy::TYPE_SPECULATIVE: 112 return "empty"_ns; 113 case nsIContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA: 114 return "empty"_ns; 115 case nsIContentPolicy::TYPE_WEB_IDENTITY: 116 return "webidentity"_ns; 117 case nsIContentPolicy::TYPE_WEB_TRANSPORT: 118 return "webtransport"_ns; 119 case nsIContentPolicy::TYPE_INTERNAL_EXTERNAL_RESOURCE: 120 return "image"_ns; 121 case nsIContentPolicy::TYPE_END: 122 case nsIContentPolicy::TYPE_INVALID: 123 break; 124 // Do not add default: so that compilers can catch the missing case. 125 } 126 127 MOZ_CRASH("Unhandled nsContentPolicyType value"); 128 } 129 130 // Helper function to determine if a ExpandedPrincipal is of the same-origin as 131 // a URI in the sec-fetch context. 132 void IsExpandedPrincipalSameOrigin( 133 nsCOMPtr<nsIExpandedPrincipal> aExpandedPrincipal, nsIURI* aURI, 134 bool* aRes) { 135 *aRes = false; 136 for (const auto& principal : aExpandedPrincipal->AllowList()) { 137 // Ignore extension principals to continue treating 138 // "moz-extension:"-requests as not "same-origin". 139 if (!mozilla::BasePrincipal::Cast(principal)->AddonPolicy()) { 140 // A ExpandedPrincipal usually has at most one ContentPrincipal, so we can 141 // check IsSameOrigin on it here and return early. 142 mozilla::BasePrincipal::Cast(principal)->IsSameOrigin(aURI, aRes); 143 return; 144 } 145 } 146 } 147 148 // Helper function to determine whether a request (including involved 149 // redirects) is same-origin in the context of SecFetch. 150 bool IsSameOrigin(nsIHttpChannel* aHTTPChannel) { 151 nsCOMPtr<nsIURI> channelURI; 152 NS_GetFinalChannelURI(aHTTPChannel, getter_AddRefs(channelURI)); 153 154 nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); 155 156 if (mozilla::BasePrincipal::Cast(loadInfo->TriggeringPrincipal()) 157 ->AddonPolicy()) { 158 // If an extension triggered the load that has access to the URI then the 159 // load is considered as same-origin. 160 return mozilla::BasePrincipal::Cast(loadInfo->TriggeringPrincipal()) 161 ->AddonAllowsLoad(channelURI); 162 } 163 164 bool isSameOrigin = false; 165 if (nsContentUtils::IsExpandedPrincipal(loadInfo->TriggeringPrincipal())) { 166 nsCOMPtr<nsIExpandedPrincipal> ep = 167 do_QueryInterface(loadInfo->TriggeringPrincipal()); 168 IsExpandedPrincipalSameOrigin(ep, channelURI, &isSameOrigin); 169 } else { 170 isSameOrigin = loadInfo->TriggeringPrincipal()->IsSameOrigin(channelURI); 171 } 172 173 // if the initial request is not same-origin, we can return here 174 // because we already know it's not a same-origin request 175 if (!isSameOrigin) { 176 return false; 177 } 178 179 // let's further check all the hoops in the redirectChain to 180 // ensure all involved redirects are same-origin 181 nsCOMPtr<nsIPrincipal> redirectPrincipal; 182 for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) { 183 entry->GetPrincipal(getter_AddRefs(redirectPrincipal)); 184 if (redirectPrincipal && !redirectPrincipal->IsSameOrigin(channelURI)) { 185 return false; 186 } 187 } 188 189 // must be a same-origin request 190 return true; 191 } 192 193 // Helper function to determine whether a request (including involved 194 // redirects) is same-site in the context of SecFetch. 195 bool IsSameSite(nsIChannel* aHTTPChannel) { 196 nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil = 197 do_GetService(THIRDPARTYUTIL_CONTRACTID); 198 if (!thirdPartyUtil) { 199 return false; 200 } 201 202 nsAutoCString hostDomain; 203 nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); 204 nsresult rv = loadInfo->TriggeringPrincipal()->GetBaseDomain(hostDomain); 205 (void)NS_WARN_IF(NS_FAILED(rv)); 206 207 nsAutoCString channelDomain; 208 nsCOMPtr<nsIURI> channelURI; 209 NS_GetFinalChannelURI(aHTTPChannel, getter_AddRefs(channelURI)); 210 rv = thirdPartyUtil->GetBaseDomain(channelURI, channelDomain); 211 (void)NS_WARN_IF(NS_FAILED(rv)); 212 213 // if the initial request is not same-site, or not https, we can 214 // return here because we already know it's not a same-site request 215 if (!hostDomain.Equals(channelDomain) || 216 (!loadInfo->TriggeringPrincipal()->SchemeIs("https") && 217 !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost( 218 hostDomain))) { 219 return false; 220 } 221 222 // let's further check all the hoops in the redirectChain to 223 // ensure all involved redirects are same-site and https 224 nsCOMPtr<nsIPrincipal> redirectPrincipal; 225 for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) { 226 entry->GetPrincipal(getter_AddRefs(redirectPrincipal)); 227 if (redirectPrincipal) { 228 redirectPrincipal->GetBaseDomain(hostDomain); 229 if (!hostDomain.Equals(channelDomain) || 230 !redirectPrincipal->SchemeIs("https")) { 231 return false; 232 } 233 } 234 } 235 236 // must be a same-site request 237 return true; 238 } 239 240 // Helper function to determine whether a request was triggered 241 // by the end user in the context of SecFetch. 242 // The more secure/closed state to return for this function is "false". 243 // A user triggered action is less restricted because it is not cross-origin. 244 bool IsUserTriggeredForSecFetchSite(nsIHttpChannel* aHTTPChannel) { 245 /* 246 * The goal is to distinguish between "webby" navigations that are controlled 247 * by a given website (e.g. links, the window.location setter,form 248 * submissions, etc.), and those that are not (e.g. user interaction with a 249 * user agent’s address bar, bookmarks, etc). 250 */ 251 nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); 252 ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType(); 253 254 // A request issued by the browser is always user initiated. 255 if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { 256 return true; 257 } 258 259 // only requests wich result in type "document" are subject to 260 // user initiated actions in the context of SecFetch. 261 if (contentType != ExtContentPolicy::TYPE_DOCUMENT && 262 contentType != ExtContentPolicy::TYPE_SUBDOCUMENT) { 263 return false; 264 } 265 266 // The load is considered user triggered if it was triggered by an external 267 // application. 268 if (loadInfo->GetLoadTriggeredFromExternal()) { 269 return true; 270 } 271 272 // sec-fetch-site can only be user triggered if the load was user triggered. 273 if (!loadInfo->GetHasValidUserGestureActivation()) { 274 return false; 275 } 276 277 // We can assert that the navigation must be "webby" if the load was triggered 278 // by a meta refresh. See also Bug 1647128. 279 if (loadInfo->GetIsMetaRefresh()) { 280 return false; 281 } 282 283 // All web requests have a valid "original" referrer set in the 284 // ReferrerInfo which we can use to determine whether a request 285 // was triggered by a user or not. 286 nsCOMPtr<nsIReferrerInfo> referrerInfo = aHTTPChannel->GetReferrerInfo(); 287 if (referrerInfo) { 288 nsCOMPtr<nsIURI> originalReferrer; 289 referrerInfo->GetOriginalReferrer(getter_AddRefs(originalReferrer)); 290 if (!originalReferrer) { 291 return true; 292 } 293 } 294 295 return false; 296 } 297 298 void mozilla::dom::SecFetch::AddSecFetchDest(nsIHttpChannel* aHTTPChannel) { 299 nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); 300 nsContentPolicyType contentType = loadInfo->InternalContentPolicyType(); 301 nsCString dest = MapInternalContentPolicyTypeToDest(contentType); 302 303 nsresult rv = 304 aHTTPChannel->SetRequestHeader("Sec-Fetch-Dest"_ns, dest, false); 305 (void)NS_WARN_IF(NS_FAILED(rv)); 306 } 307 308 void mozilla::dom::SecFetch::AddSecFetchMode(nsIHttpChannel* aHTTPChannel) { 309 nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); 310 uint32_t securityMode = loadInfo->GetSecurityMode(); 311 ExtContentPolicyType externalType = loadInfo->GetExternalContentPolicyType(); 312 313 nsAutoCString mode; 314 if (externalType == ExtContentPolicy::TYPE_DOCUMENT || 315 externalType == ExtContentPolicy::TYPE_SUBDOCUMENT || 316 externalType == ExtContentPolicy::TYPE_OBJECT) { 317 mode = "navigate"_ns; 318 } else if (externalType == ExtContentPolicy::TYPE_WEBSOCKET) { 319 mode = "websocket"_ns; 320 } else { 321 mode = GetEnumString( 322 nsContentSecurityManager::SecurityModeToRequestMode(securityMode)); 323 } 324 325 nsresult rv = 326 aHTTPChannel->SetRequestHeader("Sec-Fetch-Mode"_ns, mode, false); 327 (void)NS_WARN_IF(NS_FAILED(rv)); 328 } 329 330 void mozilla::dom::SecFetch::AddSecFetchSite(nsIHttpChannel* aHTTPChannel) { 331 nsAutoCString site("same-origin"); 332 333 bool isSameOrigin = IsSameOrigin(aHTTPChannel); 334 if (!isSameOrigin) { 335 bool isSameSite = IsSameSite(aHTTPChannel); 336 if (isSameSite) { 337 site = "same-site"_ns; 338 } else { 339 site = "cross-site"_ns; 340 } 341 } 342 343 if (IsUserTriggeredForSecFetchSite(aHTTPChannel)) { 344 site = "none"_ns; 345 } 346 347 nsresult rv = 348 aHTTPChannel->SetRequestHeader("Sec-Fetch-Site"_ns, site, false); 349 (void)NS_WARN_IF(NS_FAILED(rv)); 350 } 351 352 void mozilla::dom::SecFetch::AddSecFetchUser(nsIHttpChannel* aHTTPChannel) { 353 nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); 354 ExtContentPolicyType externalType = loadInfo->GetExternalContentPolicyType(); 355 356 // sec-fetch-user only applies to loads of type document or subdocument 357 if (externalType != ExtContentPolicy::TYPE_DOCUMENT && 358 externalType != ExtContentPolicy::TYPE_SUBDOCUMENT) { 359 return; 360 } 361 362 // sec-fetch-user only applies if the request is user triggered. 363 // requests triggered by an external application are considerd user triggered. 364 if (!loadInfo->GetLoadTriggeredFromExternal() && 365 !loadInfo->GetHasValidUserGestureActivation()) { 366 return; 367 } 368 369 nsAutoCString user("?1"); 370 nsresult rv = 371 aHTTPChannel->SetRequestHeader("Sec-Fetch-User"_ns, user, false); 372 (void)NS_WARN_IF(NS_FAILED(rv)); 373 } 374 375 void mozilla::dom::SecFetch::AddSecFetchHeader(nsIHttpChannel* aHTTPChannel) { 376 nsCOMPtr<nsIURI> uri; 377 nsresult rv = aHTTPChannel->GetURI(getter_AddRefs(uri)); 378 if (NS_WARN_IF(NS_FAILED(rv))) { 379 return; 380 } 381 382 // if we are not dealing with a potentially trustworthy URL, then 383 // there is nothing to do here 384 if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) { 385 return; 386 } 387 388 // If we're dealing with a system XMLHttpRequest or fetch, don't add 389 // Sec- headers. 390 nsCOMPtr<nsILoadInfo> loadInfo = aHTTPChannel->LoadInfo(); 391 if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { 392 ExtContentPolicy extType = loadInfo->GetExternalContentPolicyType(); 393 if (extType == ExtContentPolicy::TYPE_FETCH || 394 extType == ExtContentPolicy::TYPE_XMLHTTPREQUEST) { 395 return; 396 } 397 } 398 399 AddSecFetchDest(aHTTPChannel); 400 AddSecFetchMode(aHTTPChannel); 401 AddSecFetchSite(aHTTPChannel); 402 AddSecFetchUser(aHTTPChannel); 403 }