gecko-dev/dom/security/ReferrerInfo.cpp
CanadaHonk 2e45b3450f Bug 1622090 - Implement loading=lazy for <iframe> r=emilio
Initial implementation for <iframe loading=lazy>

Also cleaned up some image lazy loading to be generalised
(sharing an intersection observer and some enums/funcs in Element).

Unimplemented details:
 - Window load should not wait for in view lazy loading iframes.
   Unplanned for this patch and ship for now.
   (Chromium fail, WebKit pass)

We also pass some other WPTs other engines do not too already too.
Also added a WPT to ensure using parse-time base URI for lazy -> eager iframes.

Differential Revision: https://phabricator.services.mozilla.com/D190662
2023-10-18 14:13:29 +00:00

1745 lines
54 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 "mozilla/RefPtr.h"
#include "mozilla/dom/ReferrerPolicyBinding.h"
#include "nsIClassInfoImpl.h"
#include "nsIEffectiveTLDService.h"
#include "nsIHttpChannel.h"
#include "nsIObjectInputStream.h"
#include "nsIObjectOutputStream.h"
#include "nsIOService.h"
#include "nsIPipe.h"
#include "nsIURL.h"
#include "nsWhitespaceTokenizer.h"
#include "nsAlgorithm.h"
#include "nsContentUtils.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsScriptSecurityManager.h"
#include "nsStreamUtils.h"
#include "ReferrerInfo.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/ContentBlockingAllowList.h"
#include "mozilla/net/CookieJarSettings.h"
#include "mozilla/net/HttpBaseChannel.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/RequestBinding.h"
#include "mozilla/StaticPrefs_network.h"
#include "mozilla/StorageAccess.h"
#include "mozilla/StyleSheet.h"
#include "mozilla/Telemetry.h"
#include "nsIWebProgressListener.h"
static mozilla::LazyLogModule gReferrerInfoLog("ReferrerInfo");
#define LOG(msg) MOZ_LOG(gReferrerInfoLog, mozilla::LogLevel::Debug, msg)
#define LOG_ENABLED() MOZ_LOG_TEST(gReferrerInfoLog, mozilla::LogLevel::Debug)
using namespace mozilla::net;
namespace mozilla::dom {
// Implementation of ClassInfo is required to serialize/deserialize
NS_IMPL_CLASSINFO(ReferrerInfo, nullptr, nsIClassInfo::THREADSAFE,
REFERRERINFO_CID)
NS_IMPL_ISUPPORTS_CI(ReferrerInfo, nsIReferrerInfo, nsISerializable)
#define MAX_REFERRER_SENDING_POLICY 2
#define MAX_CROSS_ORIGIN_SENDING_POLICY 2
#define MAX_TRIMMING_POLICY 2
#define MIN_REFERRER_SENDING_POLICY 0
#define MIN_CROSS_ORIGIN_SENDING_POLICY 0
#define MIN_TRIMMING_POLICY 0
/*
* Default referrer policy to use
*/
enum DefaultReferrerPolicy : uint32_t {
eDefaultPolicyNoReferrer = 0,
eDefaultPolicySameOrgin = 1,
eDefaultPolicyStrictWhenXorigin = 2,
eDefaultPolicyNoReferrerWhenDownGrade = 3,
};
static uint32_t GetDefaultFirstPartyReferrerPolicyPref(bool aPrivateBrowsing) {
return aPrivateBrowsing
? StaticPrefs::network_http_referer_defaultPolicy_pbmode()
: StaticPrefs::network_http_referer_defaultPolicy();
}
static uint32_t GetDefaultThirdPartyReferrerPolicyPref(bool aPrivateBrowsing) {
return aPrivateBrowsing
? StaticPrefs::network_http_referer_defaultPolicy_trackers_pbmode()
: StaticPrefs::network_http_referer_defaultPolicy_trackers();
}
static ReferrerPolicy DefaultReferrerPolicyToReferrerPolicy(
uint32_t aDefaultToUse) {
switch (aDefaultToUse) {
case DefaultReferrerPolicy::eDefaultPolicyNoReferrer:
return ReferrerPolicy::No_referrer;
case DefaultReferrerPolicy::eDefaultPolicySameOrgin:
return ReferrerPolicy::Same_origin;
case DefaultReferrerPolicy::eDefaultPolicyStrictWhenXorigin:
return ReferrerPolicy::Strict_origin_when_cross_origin;
}
return ReferrerPolicy::No_referrer_when_downgrade;
}
struct LegacyReferrerPolicyTokenMap {
const char* mToken;
ReferrerPolicy mPolicy;
};
/*
* Parse ReferrerPolicy from token.
* The supported tokens are defined in ReferrerPolicy.webidl.
* The legacy tokens are "never", "default", "always" and
* "origin-when-crossorigin". The legacy tokens are only supported in meta
* referrer content
*
* @param aContent content string to be transformed into
* ReferrerPolicyEnum, e.g. "origin".
*/
ReferrerPolicy ReferrerPolicyFromToken(const nsAString& aContent,
bool allowedLegacyToken) {
nsString lowerContent(aContent);
ToLowerCase(lowerContent);
if (allowedLegacyToken) {
static const LegacyReferrerPolicyTokenMap sLegacyReferrerPolicyToken[] = {
{"never", ReferrerPolicy::No_referrer},
{"default", ReferrerPolicy::No_referrer_when_downgrade},
{"always", ReferrerPolicy::Unsafe_url},
{"origin-when-crossorigin", ReferrerPolicy::Origin_when_cross_origin},
};
uint8_t numStr = (sizeof(sLegacyReferrerPolicyToken) /
sizeof(sLegacyReferrerPolicyToken[0]));
for (uint8_t i = 0; i < numStr; i++) {
if (lowerContent.EqualsASCII(sLegacyReferrerPolicyToken[i].mToken)) {
return sLegacyReferrerPolicyToken[i].mPolicy;
}
}
}
// Supported tokes - ReferrerPolicyValues, are generated from
// ReferrerPolicy.webidl
for (uint8_t i = 0; ReferrerPolicyValues::strings[i].value; i++) {
if (lowerContent.EqualsASCII(ReferrerPolicyValues::strings[i].value)) {
return static_cast<enum ReferrerPolicy>(i);
}
}
// Return no referrer policy (empty string) if none of the previous match
return ReferrerPolicy::_empty;
}
// static
ReferrerPolicy ReferrerInfo::ReferrerPolicyFromMetaString(
const nsAString& aContent) {
// This is implemented as described in
// https://html.spec.whatwg.org/multipage/semantics.html#meta-referrer
// Meta referrer accepts both supported tokens in ReferrerPolicy.webidl and
// legacy tokens.
return ReferrerPolicyFromToken(aContent, true);
}
// static
ReferrerPolicy ReferrerInfo::ReferrerPolicyAttributeFromString(
const nsAString& aContent) {
// This is implemented as described in
// https://html.spec.whatwg.org/multipage/infrastructure.html#referrer-policy-attribute
// referrerpolicy attribute only accepts supported tokens in
// ReferrerPolicy.webidl
return ReferrerPolicyFromToken(aContent, false);
}
// static
ReferrerPolicy ReferrerInfo::ReferrerPolicyFromHeaderString(
const nsAString& aContent) {
// Multiple headers could be concatenated into one comma-separated
// list of policies. Need to tokenize the multiple headers.
ReferrerPolicyEnum referrerPolicy = ReferrerPolicy::_empty;
for (const auto& token : nsCharSeparatedTokenizer(aContent, ',').ToRange()) {
if (token.IsEmpty()) {
continue;
}
// Referrer-Policy header only accepts supported tokens in
// ReferrerPolicy.webidl
ReferrerPolicyEnum policy = ReferrerPolicyFromToken(token, false);
// If there are multiple policies available, the last valid policy should be
// used.
// https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
if (policy != ReferrerPolicy::_empty) {
referrerPolicy = policy;
}
}
return referrerPolicy;
}
// static
const char* ReferrerInfo::ReferrerPolicyToString(ReferrerPolicyEnum aPolicy) {
uint8_t index = static_cast<uint8_t>(aPolicy);
uint8_t referrerPolicyCount = ArrayLength(ReferrerPolicyValues::strings);
MOZ_ASSERT(index < referrerPolicyCount);
if (index >= referrerPolicyCount) {
return "";
}
return ReferrerPolicyValues::strings[index].value;
}
/* static */
uint32_t ReferrerInfo::GetUserReferrerSendingPolicy() {
return clamped<uint32_t>(
StaticPrefs::network_http_sendRefererHeader_DoNotUseDirectly(),
MIN_REFERRER_SENDING_POLICY, MAX_REFERRER_SENDING_POLICY);
}
/* static */
uint32_t ReferrerInfo::GetUserXOriginSendingPolicy() {
return clamped<uint32_t>(
StaticPrefs::network_http_referer_XOriginPolicy_DoNotUseDirectly(),
MIN_CROSS_ORIGIN_SENDING_POLICY, MAX_CROSS_ORIGIN_SENDING_POLICY);
}
/* static */
uint32_t ReferrerInfo::GetUserTrimmingPolicy() {
return clamped<uint32_t>(
StaticPrefs::network_http_referer_trimmingPolicy_DoNotUseDirectly(),
MIN_TRIMMING_POLICY, MAX_TRIMMING_POLICY);
}
/* static */
uint32_t ReferrerInfo::GetUserXOriginTrimmingPolicy() {
return clamped<uint32_t>(
StaticPrefs::
network_http_referer_XOriginTrimmingPolicy_DoNotUseDirectly(),
MIN_TRIMMING_POLICY, MAX_TRIMMING_POLICY);
}
/* static */
ReferrerPolicy ReferrerInfo::GetDefaultReferrerPolicy(nsIHttpChannel* aChannel,
nsIURI* aURI,
bool aPrivateBrowsing) {
bool thirdPartyTrackerIsolated = false;
if (aChannel && aURI) {
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
nsCOMPtr<nsICookieJarSettings> cjs;
Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cjs));
if (!cjs) {
bool shouldResistFingerprinting =
nsContentUtils::ShouldResistFingerprinting(
aChannel, RFPTarget::IsAlwaysEnabledForPrecompute);
cjs = aPrivateBrowsing
? net::CookieJarSettings::Create(CookieJarSettings::ePrivate,
shouldResistFingerprinting)
: net::CookieJarSettings::Create(CookieJarSettings::eRegular,
shouldResistFingerprinting);
}
// We only check if the channel is isolated if it's in the parent process
// with the rejection of third party contexts is enabled. We don't need to
// check this in content processes since the tracking state of the channel
// is unknown here and the referrer policy would be updated when the channel
// starts connecting in the parent process.
if (XRE_IsParentProcess() && cjs->GetRejectThirdPartyContexts()) {
uint32_t rejectedReason = 0;
thirdPartyTrackerIsolated =
!ShouldAllowAccessFor(aChannel, aURI, &rejectedReason) &&
rejectedReason !=
static_cast<uint32_t>(
nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN);
// Here we intentionally do not notify about the rejection reason, if any
// in order to avoid this check to have any visible side-effects (e.g. a
// web console report.)
}
}
// Select the appropriate pref starting with
// "network.http.referer.defaultPolicy" to use based on private-browsing
// ("pbmode") AND third-party trackers ("trackers").
return DefaultReferrerPolicyToReferrerPolicy(
thirdPartyTrackerIsolated
? GetDefaultThirdPartyReferrerPolicyPref(aPrivateBrowsing)
: GetDefaultFirstPartyReferrerPolicyPref(aPrivateBrowsing));
}
/* static */
bool ReferrerInfo::IsReferrerSchemeAllowed(nsIURI* aReferrer) {
NS_ENSURE_TRUE(aReferrer, false);
nsAutoCString scheme;
nsresult rv = aReferrer->GetScheme(scheme);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
return scheme.EqualsIgnoreCase("https") || scheme.EqualsIgnoreCase("http");
}
/* static */
bool ReferrerInfo::ShouldResponseInheritReferrerInfo(nsIChannel* aChannel) {
if (!aChannel) {
return false;
}
nsCOMPtr<nsIURI> channelURI;
nsresult rv = aChannel->GetURI(getter_AddRefs(channelURI));
NS_ENSURE_SUCCESS(rv, false);
bool isAbout = channelURI->SchemeIs("about");
if (!isAbout) {
return false;
}
nsAutoCString aboutSpec;
rv = channelURI->GetSpec(aboutSpec);
NS_ENSURE_SUCCESS(rv, false);
return aboutSpec.EqualsLiteral("about:srcdoc");
}
/* static */
nsresult ReferrerInfo::HandleSecureToInsecureReferral(
nsIURI* aOriginalURI, nsIURI* aURI, ReferrerPolicyEnum aPolicy,
bool& aAllowed) {
NS_ENSURE_ARG(aOriginalURI);
NS_ENSURE_ARG(aURI);
aAllowed = false;
bool referrerIsHttpsScheme = aOriginalURI->SchemeIs("https");
if (!referrerIsHttpsScheme) {
aAllowed = true;
return NS_OK;
}
// It's ok to send referrer for https-to-http scenarios if the referrer
// policy is "unsafe-url", "origin", or "origin-when-cross-origin".
// in other referrer policies, https->http is not allowed...
bool uriIsHttpsScheme = aURI->SchemeIs("https");
if (aPolicy != ReferrerPolicy::Unsafe_url &&
aPolicy != ReferrerPolicy::Origin_when_cross_origin &&
aPolicy != ReferrerPolicy::Origin && !uriIsHttpsScheme) {
return NS_OK;
}
aAllowed = true;
return NS_OK;
}
nsresult ReferrerInfo::HandleUserXOriginSendingPolicy(nsIURI* aURI,
nsIURI* aReferrer,
bool& aAllowed) const {
NS_ENSURE_ARG(aURI);
aAllowed = false;
nsAutoCString uriHost;
nsAutoCString referrerHost;
nsresult rv = aURI->GetAsciiHost(uriHost);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = aReferrer->GetAsciiHost(referrerHost);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Send an empty referrer if xorigin and leaving a .onion domain.
if (StaticPrefs::network_http_referer_hideOnionSource() &&
!uriHost.Equals(referrerHost) &&
StringEndsWith(referrerHost, ".onion"_ns)) {
return NS_OK;
}
switch (GetUserXOriginSendingPolicy()) {
// Check policy for sending referrer only when hosts match
case XOriginSendingPolicy::ePolicySendWhenSameHost: {
if (!uriHost.Equals(referrerHost)) {
return NS_OK;
}
break;
}
case XOriginSendingPolicy::ePolicySendWhenSameDomain: {
nsCOMPtr<nsIEffectiveTLDService> eTLDService =
do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
if (!eTLDService) {
// check policy for sending only when effective top level domain
// matches. this falls back on using host if eTLDService does not work
if (!uriHost.Equals(referrerHost)) {
return NS_OK;
}
break;
}
nsAutoCString uriDomain;
nsAutoCString referrerDomain;
uint32_t extraDomains = 0;
rv = eTLDService->GetBaseDomain(aURI, extraDomains, uriDomain);
if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
// uri is either an IP address, an alias such as 'localhost', an eTLD
// such as 'co.uk', or the empty string. Uses the normalized host in
// such cases.
rv = aURI->GetAsciiHost(uriDomain);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = eTLDService->GetBaseDomain(aReferrer, extraDomains, referrerDomain);
if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
// referrer is either an IP address, an alias such as 'localhost', an
// eTLD such as 'co.uk', or the empty string. Uses the normalized host
// in such cases.
rv = aReferrer->GetAsciiHost(referrerDomain);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!uriDomain.Equals(referrerDomain)) {
return NS_OK;
}
break;
}
default:
break;
}
aAllowed = true;
return NS_OK;
}
// This roughly implements Step 3.1. of
// https://fetch.spec.whatwg.org/#append-a-request-origin-header
/* static */
bool ReferrerInfo::ShouldSetNullOriginHeader(net::HttpBaseChannel* aChannel,
nsIURI* aOriginURI) {
MOZ_ASSERT(aChannel);
MOZ_ASSERT(aOriginURI);
// If requests mode is not "cors", then switch on requests referrer policy:
RequestMode requestMode = RequestMode::No_cors;
MOZ_ALWAYS_SUCCEEDS(aChannel->GetRequestMode(&requestMode));
if (requestMode == RequestMode::Cors) {
return false;
}
nsCOMPtr<nsIReferrerInfo> referrerInfo;
NS_ENSURE_SUCCESS(aChannel->GetReferrerInfo(getter_AddRefs(referrerInfo)),
false);
if (!referrerInfo) {
return false;
}
// "no-referrer":
enum ReferrerPolicy policy = referrerInfo->ReferrerPolicy();
if (policy == ReferrerPolicy::No_referrer) {
// Set serializedOrigin to `null`.
// Note: Returning true is the same as setting the serializedOrigin to null
// in this method.
return true;
}
// "no-referrer-when-downgrade":
// "strict-origin":
// "strict-origin-when-cross-origin":
// If requests origin is a tuple origin, its scheme is "https", and
// requests current URLs scheme is not "https", then set serializedOrigin
// to `null`.
bool allowed = false;
nsCOMPtr<nsIURI> uri;
NS_ENSURE_SUCCESS(aChannel->GetURI(getter_AddRefs(uri)), false);
if (NS_SUCCEEDED(ReferrerInfo::HandleSecureToInsecureReferral(
aOriginURI, uri, policy, allowed)) &&
!allowed) {
return true;
}
// "same-origin":
if (policy == ReferrerPolicy::Same_origin) {
// If requests origin is not same origin with requests current URLs
// origin, then set serializedOrigin to `null`.
return ReferrerInfo::IsCrossOriginRequest(aChannel);
}
// Otherwise:
// Do Nothing.
return false;
}
nsresult ReferrerInfo::HandleUserReferrerSendingPolicy(nsIHttpChannel* aChannel,
bool& aAllowed) const {
aAllowed = false;
uint32_t referrerSendingPolicy;
uint32_t loadFlags;
nsresult rv = aChannel->GetLoadFlags(&loadFlags);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (loadFlags & nsIHttpChannel::LOAD_INITIAL_DOCUMENT_URI) {
referrerSendingPolicy = ReferrerSendingPolicy::ePolicySendWhenUserTrigger;
} else {
referrerSendingPolicy = ReferrerSendingPolicy::ePolicySendInlineContent;
}
if (GetUserReferrerSendingPolicy() < referrerSendingPolicy) {
return NS_OK;
}
aAllowed = true;
return NS_OK;
}
/* static */
bool ReferrerInfo::IsCrossOriginRequest(nsIHttpChannel* aChannel) {
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
if (!loadInfo->TriggeringPrincipal()->GetIsContentPrincipal()) {
LOG(("no triggering URI via loadInfo, assuming load is cross-origin"));
return true;
}
if (LOG_ENABLED()) {
nsAutoCString triggeringURISpec;
loadInfo->TriggeringPrincipal()->GetAsciiSpec(triggeringURISpec);
LOG(("triggeringURI=%s\n", triggeringURISpec.get()));
}
nsCOMPtr<nsIURI> uri;
nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
if (NS_WARN_IF(NS_FAILED(rv))) {
return true;
}
return !loadInfo->TriggeringPrincipal()->IsSameOrigin(uri);
}
/* static */
bool ReferrerInfo::IsReferrerCrossOrigin(nsIHttpChannel* aChannel,
nsIURI* aReferrer) {
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
if (!loadInfo->TriggeringPrincipal()->GetIsContentPrincipal()) {
LOG(("no triggering URI via loadInfo, assuming load is cross-site"));
return true;
}
nsCOMPtr<nsIURI> uri;
nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
if (NS_WARN_IF(NS_FAILED(rv))) {
return true;
}
return !nsScriptSecurityManager::SecurityCompareURIs(uri, aReferrer);
}
/* static */
bool ReferrerInfo::IsCrossSiteRequest(nsIHttpChannel* aChannel) {
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
if (!loadInfo->TriggeringPrincipal()->GetIsContentPrincipal()) {
LOG(("no triggering URI via loadInfo, assuming load is cross-site"));
return true;
}
if (LOG_ENABLED()) {
nsAutoCString triggeringURISpec;
loadInfo->TriggeringPrincipal()->GetAsciiSpec(triggeringURISpec);
LOG(("triggeringURI=%s\n", triggeringURISpec.get()));
}
nsCOMPtr<nsIURI> uri;
nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
if (NS_WARN_IF(NS_FAILED(rv))) {
return true;
}
bool isCrossSite = true;
rv = loadInfo->TriggeringPrincipal()->IsThirdPartyURI(uri, &isCrossSite);
if (NS_FAILED(rv)) {
return true;
}
return isCrossSite;
}
ReferrerInfo::TrimmingPolicy ReferrerInfo::ComputeTrimmingPolicy(
nsIHttpChannel* aChannel, nsIURI* aReferrer) const {
uint32_t trimmingPolicy = GetUserTrimmingPolicy();
switch (mPolicy) {
case ReferrerPolicy::Origin:
case ReferrerPolicy::Strict_origin:
trimmingPolicy = TrimmingPolicy::ePolicySchemeHostPort;
break;
case ReferrerPolicy::Origin_when_cross_origin:
case ReferrerPolicy::Strict_origin_when_cross_origin:
if (trimmingPolicy != TrimmingPolicy::ePolicySchemeHostPort &&
IsReferrerCrossOrigin(aChannel, aReferrer)) {
// Ignore set trimmingPolicy if it is already the strictest
// policy.
trimmingPolicy = TrimmingPolicy::ePolicySchemeHostPort;
}
break;
// This function is called when a nonempty referrer value is allowed to
// send. For the next 3 policies: same-origin, no-referrer-when-downgrade,
// unsafe-url, without trimming we should have a full uri. And the trimming
// policy only depends on user prefs.
case ReferrerPolicy::Same_origin:
case ReferrerPolicy::No_referrer_when_downgrade:
case ReferrerPolicy::Unsafe_url:
if (trimmingPolicy != TrimmingPolicy::ePolicySchemeHostPort) {
// Ignore set trimmingPolicy if it is already the strictest
// policy. Apply the user cross-origin trimming policy if it's more
// restrictive than the general one.
if (GetUserXOriginTrimmingPolicy() != TrimmingPolicy::ePolicyFullURI &&
IsCrossOriginRequest(aChannel)) {
trimmingPolicy =
std::max(trimmingPolicy, GetUserXOriginTrimmingPolicy());
}
}
break;
case ReferrerPolicy::No_referrer:
case ReferrerPolicy::_empty:
default:
MOZ_ASSERT_UNREACHABLE("Unexpected value");
break;
}
return static_cast<TrimmingPolicy>(trimmingPolicy);
}
nsresult ReferrerInfo::LimitReferrerLength(
nsIHttpChannel* aChannel, nsIURI* aReferrer, TrimmingPolicy aTrimmingPolicy,
nsACString& aInAndOutTrimmedReferrer) const {
if (!StaticPrefs::network_http_referer_referrerLengthLimit()) {
return NS_OK;
}
if (aInAndOutTrimmedReferrer.Length() <=
StaticPrefs::network_http_referer_referrerLengthLimit()) {
return NS_OK;
}
nsAutoString referrerLengthLimit;
referrerLengthLimit.AppendInt(
StaticPrefs::network_http_referer_referrerLengthLimit());
if (aTrimmingPolicy == ePolicyFullURI ||
aTrimmingPolicy == ePolicySchemeHostPortPath) {
// If referrer header is over max Length, down to origin
nsresult rv = GetOriginFromReferrerURI(aReferrer, aInAndOutTrimmedReferrer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Step 6 within https://w3c.github.io/webappsec-referrer-policy/#strip-url
// states that the trailing "/" does not need to get stripped. However,
// GetOriginFromReferrerURI() also removes any trailing "/" hence we have to
// add it back here.
aInAndOutTrimmedReferrer.AppendLiteral("/");
if (aInAndOutTrimmedReferrer.Length() <=
StaticPrefs::network_http_referer_referrerLengthLimit()) {
AutoTArray<nsString, 2> params = {
referrerLengthLimit, NS_ConvertUTF8toUTF16(aInAndOutTrimmedReferrer)};
LogMessageToConsole(aChannel, "ReferrerLengthOverLimitation", params);
return NS_OK;
}
}
// If we end up here either the trimmingPolicy is equal to
// 'ePolicySchemeHostPort' or the 'origin' of any other policy is still over
// the length limit. If so, truncate the referrer entirely.
AutoTArray<nsString, 2> params = {
referrerLengthLimit, NS_ConvertUTF8toUTF16(aInAndOutTrimmedReferrer)};
LogMessageToConsole(aChannel, "ReferrerOriginLengthOverLimitation", params);
aInAndOutTrimmedReferrer.Truncate();
return NS_OK;
}
nsresult ReferrerInfo::GetOriginFromReferrerURI(nsIURI* aReferrer,
nsACString& aResult) const {
MOZ_ASSERT(aReferrer);
aResult.Truncate();
// We want the IDN-normalized PrePath. That's not something currently
// available and there doesn't yet seem to be justification for adding it to
// the interfaces, so just build it up from scheme+AsciiHostPort
nsAutoCString scheme, asciiHostPort;
nsresult rv = aReferrer->GetScheme(scheme);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
aResult = scheme;
aResult.AppendLiteral("://");
// Note we explicitly cleared UserPass above, so do not need to build it.
rv = aReferrer->GetAsciiHostPort(asciiHostPort);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
aResult.Append(asciiHostPort);
return NS_OK;
}
nsresult ReferrerInfo::TrimReferrerWithPolicy(nsIURI* aReferrer,
TrimmingPolicy aTrimmingPolicy,
nsACString& aResult) const {
MOZ_ASSERT(aReferrer);
if (aTrimmingPolicy == TrimmingPolicy::ePolicyFullURI) {
return aReferrer->GetAsciiSpec(aResult);
}
nsresult rv = GetOriginFromReferrerURI(aReferrer, aResult);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (aTrimmingPolicy == TrimmingPolicy::ePolicySchemeHostPortPath) {
nsCOMPtr<nsIURL> url(do_QueryInterface(aReferrer));
if (url) {
nsAutoCString path;
rv = url->GetFilePath(path);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
aResult.Append(path);
return NS_OK;
}
}
// Step 6 within https://w3c.github.io/webappsec-referrer-policy/#strip-url
// states that the trailing "/" does not need to get stripped. However,
// GetOriginFromReferrerURI() also removes any trailing "/" hence we have to
// add it back here.
aResult.AppendLiteral("/");
return NS_OK;
}
bool ReferrerInfo::ShouldIgnoreLessRestrictedPolicies(
nsIHttpChannel* aChannel, const ReferrerPolicyEnum aPolicy) const {
MOZ_ASSERT(aChannel);
// We only care about the less restricted policies.
if (aPolicy != ReferrerPolicy::Unsafe_url &&
aPolicy != ReferrerPolicy::No_referrer_when_downgrade &&
aPolicy != ReferrerPolicy::Origin_when_cross_origin) {
return false;
}
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
bool isPrivate = NS_UsePrivateBrowsing(aChannel);
// Return early if we don't want to ignore less restricted policies for the
// top navigation.
if (loadInfo->GetExternalContentPolicyType() ==
ExtContentPolicy::TYPE_DOCUMENT) {
bool isEnabledForTopNavigation =
isPrivate
? StaticPrefs::
network_http_referer_disallowCrossSiteRelaxingDefault_pbmode_top_navigation()
: StaticPrefs::
network_http_referer_disallowCrossSiteRelaxingDefault_top_navigation();
if (!isEnabledForTopNavigation) {
return false;
}
// We have to get the value of the contentBlockingAllowList earlier because
// the channel hasn't been opened yet here. Note that we only need to do
// this for first-party navigation. For third-party loads, the value is
// inherited from the parent.
if (XRE_IsParentProcess()) {
nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
Unused << loadInfo->GetCookieJarSettings(
getter_AddRefs(cookieJarSettings));
net::CookieJarSettings::Cast(cookieJarSettings)
->UpdateIsOnContentBlockingAllowList(aChannel);
}
}
// We don't ignore less restricted referrer policies if ETP is toggled off.
// This would affect iframe loads and top navigation. For iframes, it will
// stop ignoring if the first-party site toggled ETP off. For top navigation,
// it depends on the ETP toggle for the destination site.
if (ContentBlockingAllowList::Check(aChannel)) {
return false;
}
bool isCrossSite = IsCrossSiteRequest(aChannel);
bool isEnabled =
isPrivate
? StaticPrefs::
network_http_referer_disallowCrossSiteRelaxingDefault_pbmode()
: StaticPrefs::
network_http_referer_disallowCrossSiteRelaxingDefault();
if (!isEnabled) {
// Log the warning message to console to inform that we will ignore
// less restricted policies for cross-site requests in the future.
if (isCrossSite) {
nsCOMPtr<nsIURI> uri;
nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
NS_ENSURE_SUCCESS(rv, false);
AutoTArray<nsString, 1> params = {
NS_ConvertUTF8toUTF16(uri->GetSpecOrDefault())};
LogMessageToConsole(aChannel, "ReferrerPolicyDisallowRelaxingWarning",
params);
}
return false;
}
// Check if the channel is triggered by the system or the extension.
auto* triggerBasePrincipal =
BasePrincipal::Cast(loadInfo->TriggeringPrincipal());
if (triggerBasePrincipal->IsSystemPrincipal() ||
triggerBasePrincipal->AddonPolicy()) {
return false;
}
if (isCrossSite) {
// Log the console message to say that the less restricted policy was
// ignored.
nsCOMPtr<nsIURI> uri;
nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
NS_ENSURE_SUCCESS(rv, true);
uint32_t idx = static_cast<uint32_t>(aPolicy);
AutoTArray<nsString, 2> params = {
NS_ConvertUTF8toUTF16(
nsDependentCString(ReferrerPolicyValues::strings[idx].value)),
NS_ConvertUTF8toUTF16(uri->GetSpecOrDefault())};
LogMessageToConsole(aChannel, "ReferrerPolicyDisallowRelaxingMessage",
params);
}
return isCrossSite;
}
void ReferrerInfo::LogMessageToConsole(
nsIHttpChannel* aChannel, const char* aMsg,
const nsTArray<nsString>& aParams) const {
MOZ_ASSERT(aChannel);
nsCOMPtr<nsIURI> uri;
nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
uint64_t windowID = 0;
rv = aChannel->GetTopLevelContentWindowId(&windowID);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
if (!windowID) {
nsCOMPtr<nsILoadGroup> loadGroup;
rv = aChannel->GetLoadGroup(getter_AddRefs(loadGroup));
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
if (loadGroup) {
windowID = nsContentUtils::GetInnerWindowID(loadGroup);
}
}
nsAutoString localizedMsg;
rv = nsContentUtils::FormatLocalizedString(
nsContentUtils::eSECURITY_PROPERTIES, aMsg, aParams, localizedMsg);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
rv = nsContentUtils::ReportToConsoleByWindowID(
localizedMsg, nsIScriptError::infoFlag, "Security"_ns, windowID, uri);
Unused << NS_WARN_IF(NS_FAILED(rv));
}
ReferrerPolicy ReferrerPolicyIDLToReferrerPolicy(
nsIReferrerInfo::ReferrerPolicyIDL aReferrerPolicy) {
switch (aReferrerPolicy) {
case nsIReferrerInfo::EMPTY:
return ReferrerPolicy::_empty;
break;
case nsIReferrerInfo::NO_REFERRER:
return ReferrerPolicy::No_referrer;
break;
case nsIReferrerInfo::NO_REFERRER_WHEN_DOWNGRADE:
return ReferrerPolicy::No_referrer_when_downgrade;
break;
case nsIReferrerInfo::ORIGIN:
return ReferrerPolicy::Origin;
break;
case nsIReferrerInfo::ORIGIN_WHEN_CROSS_ORIGIN:
return ReferrerPolicy::Origin_when_cross_origin;
break;
case nsIReferrerInfo::UNSAFE_URL:
return ReferrerPolicy::Unsafe_url;
break;
case nsIReferrerInfo::SAME_ORIGIN:
return ReferrerPolicy::Same_origin;
break;
case nsIReferrerInfo::STRICT_ORIGIN:
return ReferrerPolicy::Strict_origin;
break;
case nsIReferrerInfo::STRICT_ORIGIN_WHEN_CROSS_ORIGIN:
return ReferrerPolicy::Strict_origin_when_cross_origin;
break;
default:
MOZ_ASSERT_UNREACHABLE("Invalid ReferrerPolicy value");
break;
}
return ReferrerPolicy::_empty;
}
nsIReferrerInfo::ReferrerPolicyIDL ReferrerPolicyToReferrerPolicyIDL(
ReferrerPolicy aReferrerPolicy) {
switch (aReferrerPolicy) {
case ReferrerPolicy::_empty:
return nsIReferrerInfo::EMPTY;
break;
case ReferrerPolicy::No_referrer:
return nsIReferrerInfo::NO_REFERRER;
break;
case ReferrerPolicy::No_referrer_when_downgrade:
return nsIReferrerInfo::NO_REFERRER_WHEN_DOWNGRADE;
break;
case ReferrerPolicy::Origin:
return nsIReferrerInfo::ORIGIN;
break;
case ReferrerPolicy::Origin_when_cross_origin:
return nsIReferrerInfo::ORIGIN_WHEN_CROSS_ORIGIN;
break;
case ReferrerPolicy::Unsafe_url:
return nsIReferrerInfo::UNSAFE_URL;
break;
case ReferrerPolicy::Same_origin:
return nsIReferrerInfo::SAME_ORIGIN;
break;
case ReferrerPolicy::Strict_origin:
return nsIReferrerInfo::STRICT_ORIGIN;
break;
case ReferrerPolicy::Strict_origin_when_cross_origin:
return nsIReferrerInfo::STRICT_ORIGIN_WHEN_CROSS_ORIGIN;
break;
default:
MOZ_ASSERT_UNREACHABLE("Invalid ReferrerPolicy value");
break;
}
return nsIReferrerInfo::EMPTY;
}
ReferrerInfo::ReferrerInfo()
: mOriginalReferrer(nullptr),
mPolicy(ReferrerPolicy::_empty),
mOriginalPolicy(ReferrerPolicy::_empty),
mSendReferrer(true),
mInitialized(false),
mOverridePolicyByDefault(false) {}
ReferrerInfo::ReferrerInfo(const Document& aDoc) : ReferrerInfo() {
InitWithDocument(&aDoc);
}
ReferrerInfo::ReferrerInfo(const Element& aElement) : ReferrerInfo() {
InitWithElement(&aElement);
}
ReferrerInfo::ReferrerInfo(const Element& aElement,
ReferrerPolicyEnum aOverridePolicy)
: ReferrerInfo(aElement) {
// Override referrer policy if not empty
if (aOverridePolicy != ReferrerPolicyEnum::_empty) {
mPolicy = aOverridePolicy;
mOriginalPolicy = aOverridePolicy;
}
}
ReferrerInfo::ReferrerInfo(nsIURI* aOriginalReferrer,
ReferrerPolicyEnum aPolicy, bool aSendReferrer,
const Maybe<nsCString>& aComputedReferrer)
: mOriginalReferrer(aOriginalReferrer),
mPolicy(aPolicy),
mOriginalPolicy(aPolicy),
mSendReferrer(aSendReferrer),
mInitialized(true),
mOverridePolicyByDefault(false),
mComputedReferrer(aComputedReferrer) {}
ReferrerInfo::ReferrerInfo(const ReferrerInfo& rhs)
: mOriginalReferrer(rhs.mOriginalReferrer),
mPolicy(rhs.mPolicy),
mOriginalPolicy(rhs.mOriginalPolicy),
mSendReferrer(rhs.mSendReferrer),
mInitialized(rhs.mInitialized),
mOverridePolicyByDefault(rhs.mOverridePolicyByDefault),
mComputedReferrer(rhs.mComputedReferrer) {}
already_AddRefed<ReferrerInfo> ReferrerInfo::Clone() const {
RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this));
return copy.forget();
}
already_AddRefed<ReferrerInfo> ReferrerInfo::CloneWithNewPolicy(
ReferrerPolicyEnum aPolicy) const {
RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this));
copy->mPolicy = aPolicy;
copy->mOriginalPolicy = aPolicy;
return copy.forget();
}
already_AddRefed<ReferrerInfo> ReferrerInfo::CloneWithNewSendReferrer(
bool aSendReferrer) const {
RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this));
copy->mSendReferrer = aSendReferrer;
return copy.forget();
}
already_AddRefed<ReferrerInfo> ReferrerInfo::CloneWithNewOriginalReferrer(
nsIURI* aOriginalReferrer) const {
RefPtr<ReferrerInfo> copy(new ReferrerInfo(*this));
copy->mOriginalReferrer = aOriginalReferrer;
return copy.forget();
}
NS_IMETHODIMP
ReferrerInfo::GetOriginalReferrer(nsIURI** aOriginalReferrer) {
*aOriginalReferrer = mOriginalReferrer;
NS_IF_ADDREF(*aOriginalReferrer);
return NS_OK;
}
NS_IMETHODIMP
ReferrerInfo::GetReferrerPolicy(
JSContext* aCx, nsIReferrerInfo::ReferrerPolicyIDL* aReferrerPolicy) {
*aReferrerPolicy = ReferrerPolicyToReferrerPolicyIDL(mPolicy);
return NS_OK;
}
NS_IMETHODIMP
ReferrerInfo::GetReferrerPolicyString(nsACString& aResult) {
aResult.AssignASCII(ReferrerPolicyToString(mPolicy));
return NS_OK;
}
ReferrerPolicy ReferrerInfo::ReferrerPolicy() { return mPolicy; }
NS_IMETHODIMP
ReferrerInfo::GetSendReferrer(bool* aSendReferrer) {
*aSendReferrer = mSendReferrer;
return NS_OK;
}
NS_IMETHODIMP
ReferrerInfo::Equals(nsIReferrerInfo* aOther, bool* aResult) {
NS_ENSURE_TRUE(aOther, NS_ERROR_INVALID_ARG);
MOZ_ASSERT(mInitialized);
if (aOther == this) {
*aResult = true;
return NS_OK;
}
*aResult = false;
ReferrerInfo* other = static_cast<ReferrerInfo*>(aOther);
MOZ_ASSERT(other->mInitialized);
if (mPolicy != other->mPolicy || mSendReferrer != other->mSendReferrer ||
mOverridePolicyByDefault != other->mOverridePolicyByDefault ||
mComputedReferrer != other->mComputedReferrer) {
return NS_OK;
}
if (!mOriginalReferrer != !other->mOriginalReferrer) {
// One or the other has mOriginalReferrer, but not both... not equal
return NS_OK;
}
bool originalReferrerEquals;
if (mOriginalReferrer &&
(NS_FAILED(mOriginalReferrer->Equals(other->mOriginalReferrer,
&originalReferrerEquals)) ||
!originalReferrerEquals)) {
return NS_OK;
}
*aResult = true;
return NS_OK;
}
NS_IMETHODIMP
ReferrerInfo::GetComputedReferrerSpec(nsAString& aComputedReferrerSpec) {
aComputedReferrerSpec.Assign(
mComputedReferrer.isSome()
? NS_ConvertUTF8toUTF16(mComputedReferrer.value())
: EmptyString());
return NS_OK;
}
already_AddRefed<nsIURI> ReferrerInfo::GetComputedReferrer() {
if (!mComputedReferrer.isSome() || mComputedReferrer.value().IsEmpty()) {
return nullptr;
}
nsCOMPtr<nsIURI> result;
nsresult rv = NS_NewURI(getter_AddRefs(result), mComputedReferrer.value());
if (NS_FAILED(rv)) {
return nullptr;
}
return result.forget();
}
HashNumber ReferrerInfo::Hash() const {
MOZ_ASSERT(mInitialized);
nsAutoCString originalReferrerSpec;
if (mOriginalReferrer) {
Unused << mOriginalReferrer->GetSpec(originalReferrerSpec);
}
return mozilla::AddToHash(
static_cast<uint32_t>(mPolicy), mSendReferrer, mOverridePolicyByDefault,
mozilla::HashString(originalReferrerSpec),
mozilla::HashString(mComputedReferrer.isSome() ? mComputedReferrer.value()
: ""_ns));
}
NS_IMETHODIMP
ReferrerInfo::Init(nsIReferrerInfo::ReferrerPolicyIDL aReferrerPolicy,
bool aSendReferrer, nsIURI* aOriginalReferrer) {
MOZ_ASSERT(!mInitialized);
if (mInitialized) {
return NS_ERROR_ALREADY_INITIALIZED;
};
mPolicy = ReferrerPolicyIDLToReferrerPolicy(aReferrerPolicy);
mOriginalPolicy = mPolicy;
mSendReferrer = aSendReferrer;
mOriginalReferrer = aOriginalReferrer;
mInitialized = true;
return NS_OK;
}
NS_IMETHODIMP
ReferrerInfo::InitWithDocument(const Document* aDocument) {
MOZ_ASSERT(!mInitialized);
if (mInitialized) {
return NS_ERROR_ALREADY_INITIALIZED;
};
mPolicy = aDocument->GetReferrerPolicy();
mOriginalPolicy = mPolicy;
mSendReferrer = true;
mOriginalReferrer = aDocument->GetDocumentURIAsReferrer();
mInitialized = true;
return NS_OK;
}
/**
* Check whether the given node has referrerpolicy attribute and parse
* referrer policy from the attribute.
* Currently, referrerpolicy attribute is supported in a, area, img, iframe,
* script, or link element.
*/
static ReferrerPolicy ReferrerPolicyFromAttribute(const Element& aElement) {
if (!aElement.IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area,
nsGkAtoms::script, nsGkAtoms::iframe,
nsGkAtoms::link, nsGkAtoms::img)) {
return ReferrerPolicy::_empty;
}
return aElement.GetReferrerPolicyAsEnum();
}
static bool HasRelNoReferrer(const Element& aElement) {
// rel=noreferrer is only supported in <a>, <area>, and <form>
if (!aElement.IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area,
nsGkAtoms::form) &&
!aElement.IsSVGElement(nsGkAtoms::a)) {
return false;
}
nsAutoString rel;
aElement.GetAttr(nsGkAtoms::rel, rel);
nsWhitespaceTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tok(rel);
while (tok.hasMoreTokens()) {
const nsAString& token = tok.nextToken();
if (token.LowerCaseEqualsLiteral("noreferrer")) {
return true;
}
}
return false;
}
NS_IMETHODIMP
ReferrerInfo::InitWithElement(const Element* aElement) {
MOZ_ASSERT(!mInitialized);
if (mInitialized) {
return NS_ERROR_ALREADY_INITIALIZED;
};
// Referrer policy from referrerpolicy attribute will have a higher priority
// than referrer policy from <meta> tag and Referrer-Policy header.
mPolicy = ReferrerPolicyFromAttribute(*aElement);
if (mPolicy == ReferrerPolicy::_empty) {
// Fallback to use document's referrer poicy if we don't have referrer
// policy from attribute.
mPolicy = aElement->OwnerDoc()->GetReferrerPolicy();
}
mOriginalPolicy = mPolicy;
mSendReferrer = !HasRelNoReferrer(*aElement);
mOriginalReferrer = aElement->OwnerDoc()->GetDocumentURIAsReferrer();
mInitialized = true;
return NS_OK;
}
/* static */
already_AddRefed<nsIReferrerInfo>
ReferrerInfo::CreateFromOtherAndPolicyOverride(
nsIReferrerInfo* aOther, ReferrerPolicyEnum aPolicyOverride) {
MOZ_ASSERT(aOther);
ReferrerPolicyEnum policy = aPolicyOverride != ReferrerPolicy::_empty
? aPolicyOverride
: aOther->ReferrerPolicy();
nsCOMPtr<nsIURI> referrer = aOther->GetComputedReferrer();
nsCOMPtr<nsIReferrerInfo> referrerInfo =
new ReferrerInfo(referrer, policy, aOther->GetSendReferrer());
return referrerInfo.forget();
}
/* static */
already_AddRefed<nsIReferrerInfo>
ReferrerInfo::CreateFromDocumentAndPolicyOverride(
Document* aDoc, ReferrerPolicyEnum aPolicyOverride) {
MOZ_ASSERT(aDoc);
ReferrerPolicyEnum policy = aPolicyOverride != ReferrerPolicy::_empty
? aPolicyOverride
: aDoc->GetReferrerPolicy();
nsCOMPtr<nsIReferrerInfo> referrerInfo =
new ReferrerInfo(aDoc->GetDocumentURIAsReferrer(), policy);
return referrerInfo.forget();
}
/* static */
already_AddRefed<nsIReferrerInfo> ReferrerInfo::CreateForFetch(
nsIPrincipal* aPrincipal, Document* aDoc) {
MOZ_ASSERT(aPrincipal);
nsCOMPtr<nsIReferrerInfo> referrerInfo;
if (!aPrincipal || aPrincipal->IsSystemPrincipal()) {
referrerInfo = new ReferrerInfo(nullptr);
return referrerInfo.forget();
}
if (!aDoc) {
aPrincipal->CreateReferrerInfo(ReferrerPolicy::_empty,
getter_AddRefs(referrerInfo));
return referrerInfo.forget();
}
// If it weren't for history.push/replaceState, we could just use the
// principal's URI here. But since we want changes to the URI effected
// by push/replaceState to be reflected in the XHR referrer, we have to
// be more clever.
//
// If the document's original URI (before any push/replaceStates) matches
// our principal, then we use the document's current URI (after
// push/replaceStates). Otherwise (if the document is, say, a data:
// URI), we just use the principal's URI.
nsCOMPtr<nsIURI> docCurURI = aDoc->GetDocumentURI();
nsCOMPtr<nsIURI> docOrigURI = aDoc->GetOriginalURI();
if (docCurURI && docOrigURI) {
bool equal = false;
aPrincipal->EqualsURI(docOrigURI, &equal);
if (equal) {
referrerInfo = new ReferrerInfo(docCurURI, aDoc->GetReferrerPolicy());
return referrerInfo.forget();
}
}
aPrincipal->CreateReferrerInfo(aDoc->GetReferrerPolicy(),
getter_AddRefs(referrerInfo));
return referrerInfo.forget();
}
/* static */
already_AddRefed<nsIReferrerInfo> ReferrerInfo::CreateForExternalCSSResources(
mozilla::StyleSheet* aExternalSheet, ReferrerPolicyEnum aPolicy) {
MOZ_ASSERT(aExternalSheet && !aExternalSheet->IsInline());
nsCOMPtr<nsIReferrerInfo> referrerInfo;
// Step 2
// https://w3c.github.io/webappsec-referrer-policy/#integration-with-css
// Use empty policy at the beginning and update it later from Referrer-Policy
// header.
referrerInfo = new ReferrerInfo(aExternalSheet->GetSheetURI(), aPolicy);
return referrerInfo.forget();
}
/* static */
already_AddRefed<nsIReferrerInfo>
ReferrerInfo::CreateForInternalCSSAndSVGResources(Document* aDocument) {
MOZ_ASSERT(aDocument);
return do_AddRef(new ReferrerInfo(aDocument->GetDocumentURI(),
aDocument->GetReferrerPolicy()));
}
nsresult ReferrerInfo::ComputeReferrer(nsIHttpChannel* aChannel) {
NS_ENSURE_ARG(aChannel);
MOZ_ASSERT(NS_IsMainThread());
// If the referrerInfo is passed around when redirect, just use the last
// computedReferrer to recompute
nsCOMPtr<nsIURI> referrer;
nsresult rv = NS_OK;
mOverridePolicyByDefault = false;
if (mComputedReferrer.isSome()) {
if (mComputedReferrer.value().IsEmpty()) {
return NS_OK;
}
rv = NS_NewURI(getter_AddRefs(referrer), mComputedReferrer.value());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
mComputedReferrer.reset();
// Emplace mComputedReferrer with an empty string, which means we have
// computed the referrer and the result referrer value is empty (not send
// referrer). So any early return later than this line will use that empty
// referrer.
mComputedReferrer.emplace(""_ns);
if (!mSendReferrer || !mOriginalReferrer ||
mPolicy == ReferrerPolicy::No_referrer) {
return NS_OK;
}
if (mPolicy == ReferrerPolicy::_empty ||
ShouldIgnoreLessRestrictedPolicies(aChannel, mOriginalPolicy)) {
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
OriginAttributes attrs = loadInfo->GetOriginAttributes();
bool isPrivate = attrs.mPrivateBrowsingId > 0;
nsCOMPtr<nsIURI> uri;
rv = aChannel->GetURI(getter_AddRefs(uri));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
mPolicy = GetDefaultReferrerPolicy(aChannel, uri, isPrivate);
mOverridePolicyByDefault = true;
}
// This is for the case where the ETP toggle is off. In this case, we need to
// reset the referrer and the policy if the original policy is different from
// the current policy in order to recompute the referrer policy with the
// original policy.
if (!mOverridePolicyByDefault && mOriginalPolicy != ReferrerPolicy::_empty &&
mPolicy != mOriginalPolicy) {
referrer = nullptr;
mPolicy = mOriginalPolicy;
}
if (mPolicy == ReferrerPolicy::No_referrer) {
return NS_OK;
}
bool isUserReferrerSendingAllowed = false;
rv = HandleUserReferrerSendingPolicy(aChannel, isUserReferrerSendingAllowed);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!isUserReferrerSendingAllowed) {
return NS_OK;
}
// Enforce Referrer allowlist, only http, https scheme are allowed
if (!IsReferrerSchemeAllowed(mOriginalReferrer)) {
return NS_OK;
}
nsCOMPtr<nsIURI> uri;
rv = aChannel->GetURI(getter_AddRefs(uri));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
bool isSecureToInsecureAllowed = false;
rv = HandleSecureToInsecureReferral(mOriginalReferrer, uri, mPolicy,
isSecureToInsecureAllowed);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!isSecureToInsecureAllowed) {
return NS_OK;
}
// Strip away any fragment per RFC 2616 section 14.36
// and Referrer Policy section 6.3.5.
if (!referrer) {
rv = NS_GetURIWithoutRef(mOriginalReferrer, getter_AddRefs(referrer));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
bool isUserXOriginAllowed = false;
rv = HandleUserXOriginSendingPolicy(uri, referrer, isUserXOriginAllowed);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!isUserXOriginAllowed) {
return NS_OK;
}
// Handle user pref network.http.referer.spoofSource, send spoofed referrer if
// desired
if (StaticPrefs::network_http_referer_spoofSource()) {
nsCOMPtr<nsIURI> userSpoofReferrer;
rv = NS_GetURIWithoutRef(uri, getter_AddRefs(userSpoofReferrer));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
referrer = userSpoofReferrer;
}
// strip away any userpass; we don't want to be giving out passwords ;-)
// This is required by Referrer Policy stripping algorithm.
nsCOMPtr<nsIURI> exposableURI = nsIOService::CreateExposableURI(referrer);
referrer = exposableURI;
// Don't send referrer when the request is cross-origin and policy is
// "same-origin".
if (mPolicy == ReferrerPolicy::Same_origin &&
IsReferrerCrossOrigin(aChannel, referrer)) {
return NS_OK;
}
TrimmingPolicy trimmingPolicy = ComputeTrimmingPolicy(aChannel, referrer);
nsAutoCString trimmedReferrer;
// We first trim the referrer according to the policy by calling
// 'TrimReferrerWithPolicy' and right after we have to call
// 'LimitReferrerLength' (using the same arguments) because the trimmed
// referrer might exceed the allowed max referrer length.
rv = TrimReferrerWithPolicy(referrer, trimmingPolicy, trimmedReferrer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = LimitReferrerLength(aChannel, referrer, trimmingPolicy, trimmedReferrer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// finally, remember the referrer spec.
mComputedReferrer.reset();
mComputedReferrer.emplace(trimmedReferrer);
return NS_OK;
}
/* ===== nsISerializable implementation ====== */
nsresult ReferrerInfo::ReadTailDataBeforeGecko100(
const uint32_t& aData, nsIObjectInputStream* aInputStream) {
MOZ_ASSERT(aInputStream);
nsCOMPtr<nsIInputStream> reader;
nsCOMPtr<nsIOutputStream> writer;
// We need to create a new pipe in order to read the aData and the rest of
// the input stream together in the old format. This would also help us with
// handling big endian correctly.
NS_NewPipe(getter_AddRefs(reader), getter_AddRefs(writer));
nsCOMPtr<nsIBinaryOutputStream> binaryPipeWriter =
NS_NewObjectOutputStream(writer);
// Write back the aData so that we can read bytes from it and handle big
// endian correctly.
nsresult rv = binaryPipeWriter->Write32(aData);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
nsCOMPtr<nsIBinaryInputStream> binaryPipeReader =
NS_NewObjectInputStream(reader);
rv = binaryPipeReader->ReadBoolean(&mSendReferrer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
bool isComputed;
rv = binaryPipeReader->ReadBoolean(&isComputed);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// We need to handle the following string if isComputed is true.
if (isComputed) {
// Comsume the following 2 bytes from the input stream. They are the half
// part of the length prefix of the following string.
uint16_t data;
rv = aInputStream->Read16(&data);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Write the bytes to the pipe so that we can read the length of the string.
rv = binaryPipeWriter->Write16(data);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
uint32_t length;
rv = binaryPipeReader->Read32(&length);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Consume the string body from the input stream.
nsAutoCString computedReferrer;
rv = NS_ConsumeStream(aInputStream, length, computedReferrer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
mComputedReferrer.emplace(computedReferrer);
// Read the remaining two bytes and write to the pipe.
uint16_t remain;
rv = aInputStream->Read16(&remain);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = binaryPipeWriter->Write16(remain);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
rv = binaryPipeReader->ReadBoolean(&mInitialized);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = binaryPipeReader->ReadBoolean(&mOverridePolicyByDefault);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
NS_IMETHODIMP
ReferrerInfo::Read(nsIObjectInputStream* aStream) {
bool nonNull;
nsresult rv = aStream->ReadBoolean(&nonNull);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (nonNull) {
nsAutoCString spec;
nsresult rv = aStream->ReadCString(spec);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = NS_NewURI(getter_AddRefs(mOriginalReferrer), spec);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
mOriginalReferrer = nullptr;
}
// ReferrerPolicy.webidl has different order with ReferrerPolicyIDL. We store
// to disk using the order of ReferrerPolicyIDL, so we convert to
// ReferrerPolicyIDL to make it be compatible to the old format.
uint32_t policy;
rv = aStream->Read32(&policy);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
mPolicy = ReferrerPolicyIDLToReferrerPolicy(
static_cast<nsIReferrerInfo::ReferrerPolicyIDL>(policy));
uint32_t originalPolicy;
rv = aStream->Read32(&originalPolicy);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1784045#c6 for more
// details.
//
// We need to differentiate the old format and the new format here in order
// to be able to read both formats. The check here helps us with verifying
// which format it is.
if (MOZ_UNLIKELY(originalPolicy > 0xFF)) {
mOriginalPolicy = mPolicy;
return ReadTailDataBeforeGecko100(originalPolicy, aStream);
}
mOriginalPolicy = ReferrerPolicyIDLToReferrerPolicy(
static_cast<nsIReferrerInfo::ReferrerPolicyIDL>(originalPolicy));
rv = aStream->ReadBoolean(&mSendReferrer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
bool isComputed;
rv = aStream->ReadBoolean(&isComputed);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (isComputed) {
nsAutoCString computedReferrer;
rv = aStream->ReadCString(computedReferrer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
mComputedReferrer.emplace(computedReferrer);
}
rv = aStream->ReadBoolean(&mInitialized);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = aStream->ReadBoolean(&mOverridePolicyByDefault);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
NS_IMETHODIMP
ReferrerInfo::Write(nsIObjectOutputStream* aStream) {
bool nonNull = (mOriginalReferrer != nullptr);
nsresult rv = aStream->WriteBoolean(nonNull);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (nonNull) {
nsAutoCString spec;
nsresult rv = mOriginalReferrer->GetSpec(spec);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = aStream->WriteStringZ(spec.get());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
rv = aStream->Write32(ReferrerPolicyToReferrerPolicyIDL(mPolicy));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = aStream->Write32(ReferrerPolicyToReferrerPolicyIDL(mOriginalPolicy));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = aStream->WriteBoolean(mSendReferrer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
bool isComputed = mComputedReferrer.isSome();
rv = aStream->WriteBoolean(isComputed);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (isComputed) {
rv = aStream->WriteStringZ(mComputedReferrer.value().get());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
rv = aStream->WriteBoolean(mInitialized);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = aStream->WriteBoolean(mOverridePolicyByDefault);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
void ReferrerInfo::RecordTelemetry(nsIHttpChannel* aChannel) {
#ifdef DEBUG
MOZ_ASSERT(!mTelemetryRecorded);
mTelemetryRecorded = true;
#endif // DEBUG
// The telemetry probe has 18 buckets. The first 9 buckets are for same-site
// requests and the rest 9 buckets are for cross-site requests.
uint32_t telemetryOffset =
IsCrossSiteRequest(aChannel)
? static_cast<uint32_t>(ReferrerPolicy::EndGuard_)
: 0;
Telemetry::Accumulate(Telemetry::REFERRER_POLICY_COUNT,
static_cast<uint32_t>(mPolicy) + telemetryOffset);
}
} // namespace mozilla::dom