Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "Cookie.h"
#include "CookieCommons.h"
#include "CookieLogging.h"
#include "CookieParser.h"
#include "CookieService.h"
#include "mozilla/ContentBlockingNotifier.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/StaticPrefs_network.h"
#include "mozilla/StorageAccess.h"
#include "mozilla/dom/nsMixedContentBlocker.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/dom/WorkerCommon.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/net/CookieJarSettings.h"
#include "mozilla/Unused.h"
#include "mozIThirdPartyUtil.h"
#include "nsContentUtils.h"
#include "nsICookiePermission.h"
#include "nsICookieService.h"
#include "nsIEffectiveTLDService.h"
#include "nsIGlobalObject.h"
#include "nsIHttpChannel.h"
#include "nsIRedirectHistoryEntry.h"
#include "nsIWebProgressListener.h"
#include "nsNetUtil.h"
#include "nsSandboxFlags.h"
#include "nsScriptSecurityManager.h"
#include "ThirdPartyUtil.h"
namespace mozilla {
using dom::Document;
namespace net {
// static
bool CookieCommons::DomainMatches(Cookie* aCookie, const nsACString& aHost) {
// first, check for an exact host or domain cookie match, e.g. "google.com"
// or ".google.com"; second a subdomain match, e.g.
// host = "mail.google.com", cookie domain = ".google.com".
return aCookie->RawHost() == aHost ||
(aCookie->IsDomain() && StringEndsWith(aHost, aCookie->Host()));
}
// static
bool CookieCommons::PathMatches(Cookie* aCookie, const nsACString& aPath) {
return PathMatches(aCookie->Path(), aPath);
}
// static
bool CookieCommons::PathMatches(const nsACString& aCookiePath,
const nsACString& aPath) {
// if our cookie path is empty we can't really perform our prefix check, and
// also we can't check the last character of the cookie path, so we would
// never return a successful match.
if (aCookiePath.IsEmpty()) {
return false;
}
// if the cookie path and the request path are identical, they match.
if (aCookiePath.Equals(aPath)) {
return true;
}
// if the cookie path is a prefix of the request path, and the last character
// of the cookie path is %x2F ("/"), they match.
bool isPrefix = StringBeginsWith(aPath, aCookiePath);
if (isPrefix && aCookiePath.Last() == '/') {
return true;
}
// if the cookie path is a prefix of the request path, and the first character
// of the request path that is not included in the cookie path is a %x2F ("/")
// character, they match.
uint32_t cookiePathLen = aCookiePath.Length();
return isPrefix && aPath[cookiePathLen] == '/';
}
// Get the base domain for aHostURI; e.g. for "www.bbc.co.uk", this would be
// "bbc.co.uk". Only properly-formed URI's are tolerated, though a trailing
// dot may be present. If aHostURI is an IP address, an alias such as
// 'localhost', an eTLD such as 'co.uk', or the empty string, aBaseDomain will
// be the exact host, and aRequireHostMatch will be true to indicate that
// substring matches should not be performed.
nsresult CookieCommons::GetBaseDomain(nsIEffectiveTLDService* aTLDService,
nsIURI* aHostURI, nsACString& aBaseDomain,
bool& aRequireHostMatch) {
// get the base domain. this will fail if the host contains a leading dot,
// more than one trailing dot, or is otherwise malformed.
nsresult rv = aTLDService->GetBaseDomain(aHostURI, 0, aBaseDomain);
aRequireHostMatch = rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS;
if (aRequireHostMatch) {
// aHostURI is either an IP address, an alias such as 'localhost', an eTLD
// such as 'co.uk', or the empty string. use the host as a key in such
// cases.
rv = nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, aBaseDomain);
}
NS_ENSURE_SUCCESS(rv, rv);
// aHost (and thus aBaseDomain) may be the string '.'. If so, fail.
if (aBaseDomain.Length() == 1 && aBaseDomain.Last() == '.') {
return NS_ERROR_INVALID_ARG;
}
// block any URIs without a host that aren't file:// URIs.
if (aBaseDomain.IsEmpty() && !aHostURI->SchemeIs("file")) {
return NS_ERROR_INVALID_ARG;
}
return NS_OK;
}
nsresult CookieCommons::GetBaseDomain(nsIPrincipal* aPrincipal,
nsACString& aBaseDomain) {
MOZ_ASSERT(aPrincipal);
// for historical reasons we use ascii host for file:// URLs.
if (aPrincipal->SchemeIs("file")) {
return nsContentUtils::GetHostOrIPv6WithBrackets(aPrincipal, aBaseDomain);
}
nsresult rv = aPrincipal->GetBaseDomain(aBaseDomain);
if (NS_FAILED(rv)) {
return rv;
}
nsContentUtils::MaybeFixIPv6Host(aBaseDomain);
return NS_OK;
}
// Get the base domain for aHost; e.g. for "www.bbc.co.uk", this would be
// "bbc.co.uk". This is done differently than GetBaseDomain(mTLDService, ): it
// is assumed that aHost is already normalized, and it may contain a leading dot
// (indicating that it represents a domain). A trailing dot may be present.
// If aHost is an IP address, an alias such as 'localhost', an eTLD such as
// 'co.uk', or the empty string, aBaseDomain will be the exact host, and a
// leading dot will be treated as an error.
nsresult CookieCommons::GetBaseDomainFromHost(
nsIEffectiveTLDService* aTLDService, const nsACString& aHost,
nsCString& aBaseDomain) {
// aHost must not be the string '.'.
if (aHost.Length() == 1 && aHost.Last() == '.') {
return NS_ERROR_INVALID_ARG;
}
// aHost may contain a leading dot; if so, strip it now.
bool domain = !aHost.IsEmpty() && aHost.First() == '.';
// get the base domain. this will fail if the host contains a leading dot,
// more than one trailing dot, or is otherwise malformed.
nsresult rv = aTLDService->GetBaseDomainFromHost(Substring(aHost, domain), 0,
aBaseDomain);
if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
// aHost is either an IP address, an alias such as 'localhost', an eTLD
// such as 'co.uk', or the empty string. use the host as a key in such
// cases; however, we reject any such hosts with a leading dot, since it
// doesn't make sense for them to be domain cookies.
if (domain) {
return NS_ERROR_INVALID_ARG;
}
aBaseDomain = aHost;
return NS_OK;
}
return rv;
}
/* static */ bool CookieCommons::IsIPv6BaseDomain(
const nsACString& aBaseDomain) {
return aBaseDomain.Contains(':');
}
namespace {
void NotifyRejectionToObservers(nsIURI* aHostURI, CookieOperation aOperation) {
if (aOperation == OPERATION_WRITE) {
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
if (os) {
os->NotifyObservers(aHostURI, "cookie-rejected", nullptr);
}
} else {
MOZ_ASSERT(aOperation == OPERATION_READ);
}
}
} // namespace
// Notify observers that a cookie was rejected due to the users' prefs.
void CookieCommons::NotifyRejected(nsIURI* aHostURI, nsIChannel* aChannel,
uint32_t aRejectedReason,
CookieOperation aOperation) {
NotifyRejectionToObservers(aHostURI, aOperation);
ContentBlockingNotifier::OnDecision(
aChannel, ContentBlockingNotifier::BlockingDecision::eBlock,
aRejectedReason);
}
bool CookieCommons::CheckPathSize(const CookieStruct& aCookieData) {
return aCookieData.path().Length() <= kMaxBytesPerPath;
}
bool CookieCommons::CheckNameAndValueSize(const CookieStruct& aCookieData) {
// reject cookie if it's over the size limit, per RFC2109
return (aCookieData.name().Length() + aCookieData.value().Length()) <=
kMaxBytesPerCookie;
}
bool CookieCommons::CheckName(const CookieStruct& aCookieData) {
const char illegalNameCharacters[] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, 0x0D,
0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19,
0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x3D, 0x7F, 0x00};
const auto* start = aCookieData.name().BeginReading();
const auto* end = aCookieData.name().EndReading();
auto charFilter = [&](unsigned char c) {
if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) {
return true;
}
return std::find(std::begin(illegalNameCharacters),
std::end(illegalNameCharacters),
c) != std::end(illegalNameCharacters);
};
return std::find_if(start, end, charFilter) == end;
}
bool CookieCommons::CheckValue(const CookieStruct& aCookieData) {
// reject cookie if value contains an RFC 6265 disallowed character - see
// NOTE: this is not the full set of characters disallowed by 6265 - notably
// 0x09, 0x20, 0x22, 0x2C, and 0x5C are missing from this list.
const char illegalCharacters[] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x7F, 0x00};
const auto* start = aCookieData.value().BeginReading();
const auto* end = aCookieData.value().EndReading();
auto charFilter = [&](unsigned char c) {
if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) {
return true;
}
return std::find(std::begin(illegalCharacters), std::end(illegalCharacters),
c) != std::end(illegalCharacters);
};
return std::find_if(start, end, charFilter) == end;
}
// static
bool CookieCommons::CheckCookiePermission(nsIChannel* aChannel,
CookieStruct& aCookieData) {
if (!aChannel) {
// No channel, let's assume this is a system-principal request.
return true;
}
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
nsresult rv =
loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
if (NS_WARN_IF(NS_FAILED(rv))) {
return true;
}
nsIScriptSecurityManager* ssm =
nsScriptSecurityManager::GetScriptSecurityManager();
MOZ_ASSERT(ssm);
nsCOMPtr<nsIPrincipal> channelPrincipal;
rv = ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(channelPrincipal));
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
return CheckCookiePermission(channelPrincipal, cookieJarSettings,
aCookieData);
}
// static
bool CookieCommons::CheckCookiePermission(
nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings,
CookieStruct& aCookieData) {
MOZ_ASSERT(aPrincipal);
MOZ_ASSERT(aCookieJarSettings);
if (!aPrincipal->GetIsContentPrincipal()) {
return true;
}
uint32_t cookiePermission = nsICookiePermission::ACCESS_DEFAULT;
nsresult rv =
aCookieJarSettings->CookiePermission(aPrincipal, &cookiePermission);
if (NS_WARN_IF(NS_FAILED(rv))) {
return true;
}
if (cookiePermission == nsICookiePermission::ACCESS_ALLOW) {
return true;
}
if (cookiePermission == nsICookiePermission::ACCESS_SESSION) {
aCookieData.isSession() = true;
return true;
}
if (cookiePermission == nsICookiePermission::ACCESS_DENY) {
return false;
}
return true;
}
namespace {
CookieStatus CookieStatusForWindow(nsPIDOMWindowInner* aWindow,
nsIURI* aDocumentURI) {
MOZ_ASSERT(aWindow);
MOZ_ASSERT(aDocumentURI);
ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance();
if (thirdPartyUtil) {
bool isThirdParty = true;
nsresult rv = thirdPartyUtil->IsThirdPartyWindow(
aWindow->GetOuterWindow(), aDocumentURI, &isThirdParty);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Third-party window check failed.");
if (NS_SUCCEEDED(rv) && !isThirdParty) {
return STATUS_ACCEPTED;
}
}
return STATUS_ACCEPTED;
}
bool CheckCookieStringFromDocument(const nsACString& aCookieString) {
// If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F character (CTL
// characters excluding HTAB): Abort these steps and ignore the
// set-cookie-string entirely.
const char illegalCharacters[] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x7F, 0x00};
const auto* start = aCookieString.BeginReading();
const auto* end = aCookieString.EndReading();
auto charFilter = [&](unsigned char c) {
if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) {
return true;
}
return std::find(std::begin(illegalCharacters), std::end(illegalCharacters),
c) != std::end(illegalCharacters);
};
return std::find_if(start, end, charFilter) == end;
}
} // namespace
// static
already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument(
CookieParser& aCookieParser, Document* aDocument,
const nsACString& aCookieString, int64_t currentTimeInUsec,
nsIEffectiveTLDService* aTLDService, mozIThirdPartyUtil* aThirdPartyUtil,
nsACString& aBaseDomain, OriginAttributes& aAttrs) {
if (!CookieCommons::IsSchemeSupported(aCookieParser.HostURI())) {
return nullptr;
}
if (!CheckCookieStringFromDocument(aCookieString)) {
return nullptr;
}
nsAutoCString baseDomain;
bool requireHostMatch = false;
nsresult rv = CookieCommons::GetBaseDomain(
aTLDService, aCookieParser.HostURI(), baseDomain, requireHostMatch);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow();
if (NS_WARN_IF(!innerWindow)) {
return nullptr;
}
bool isForeignAndNotAddon = false;
if (!BasePrincipal::Cast(aDocument->NodePrincipal())->AddonPolicy()) {
rv = aThirdPartyUtil->IsThirdPartyWindow(innerWindow->GetOuterWindow(),
aCookieParser.HostURI(),
&isForeignAndNotAddon);
if (NS_WARN_IF(NS_FAILED(rv))) {
isForeignAndNotAddon = true;
}
}
bool mustBePartitioned =
isForeignAndNotAddon &&
aDocument->CookieJarSettings()->GetCookieBehavior() ==
nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
!aDocument->UsingStorageAccess();
// If we are here, we have been already accepted by the anti-tracking.
// We just need to check if we have to be in session-only mode.
CookieStatus cookieStatus =
CookieStatusForWindow(innerWindow, aCookieParser.HostURI());
MOZ_ASSERT(cookieStatus == STATUS_ACCEPTED ||
cookieStatus == STATUS_ACCEPT_SESSION);
nsCString cookieString(aCookieString);
aCookieParser.Parse(baseDomain, requireHostMatch, cookieStatus, cookieString,
EmptyCString(), false, isForeignAndNotAddon,
mustBePartitioned, aDocument->IsInPrivateBrowsing());
if (!aCookieParser.ContainsCookie()) {
return nullptr;
}
// check permissions from site permission list.
if (!CookieCommons::CheckCookiePermission(aDocument->NodePrincipal(),
aDocument->CookieJarSettings(),
aCookieParser.CookieData())) {
NotifyRejectionToObservers(aCookieParser.HostURI(), OPERATION_WRITE);
ContentBlockingNotifier::OnDecision(
innerWindow, ContentBlockingNotifier::BlockingDecision::eBlock,
nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION);
return nullptr;
}
// CHIPS - If the partitioned attribute is set, store cookie in partitioned
// cookie jar independent of context. If the cookies are stored in the
// partitioned cookie jar anyway no special treatment of CHIPS cookies
// necessary.
bool needPartitioned = StaticPrefs::network_cookie_CHIPS_enabled() &&
aCookieParser.CookieData().isPartitioned();
nsCOMPtr<nsIPrincipal> cookiePrincipal =
needPartitioned ? aDocument->PartitionedPrincipal()
: aDocument->EffectiveCookiePrincipal();
MOZ_ASSERT(cookiePrincipal);
nsCOMPtr<nsICookieService> service =
do_GetService(NS_COOKIESERVICE_CONTRACTID);
if (!service) {
return nullptr;
}
// Check if limit-foreign is required.
uint32_t dummyRejectedReason = 0;
if (aDocument->CookieJarSettings()->GetLimitForeignContexts() &&
!service->HasExistingCookies(baseDomain,
cookiePrincipal->OriginAttributesRef()) &&
!ShouldAllowAccessFor(innerWindow, aCookieParser.HostURI(),
&dummyRejectedReason)) {
return nullptr;
}
RefPtr<Cookie> cookie = Cookie::Create(
aCookieParser.CookieData(), cookiePrincipal->OriginAttributesRef());
MOZ_ASSERT(cookie);
cookie->SetLastAccessed(currentTimeInUsec);
cookie->SetCreationTime(
Cookie::GenerateUniqueCreationTime(currentTimeInUsec));
aBaseDomain = baseDomain;
aAttrs = cookiePrincipal->OriginAttributesRef();
return cookie.forget();
}
// static
already_AddRefed<nsICookieJarSettings> CookieCommons::GetCookieJarSettings(
nsIChannel* aChannel) {
nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
bool shouldResistFingerprinting = nsContentUtils::ShouldResistFingerprinting(
aChannel, RFPTarget::IsAlwaysEnabledForPrecompute);
if (aChannel) {
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
nsresult rv =
loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
if (NS_WARN_IF(NS_FAILED(rv))) {
cookieJarSettings =
CookieJarSettings::GetBlockingAll(shouldResistFingerprinting);
}
} else {
cookieJarSettings = CookieJarSettings::Create(CookieJarSettings::eRegular,
shouldResistFingerprinting);
}
MOZ_ASSERT(cookieJarSettings);
return cookieJarSettings.forget();
}
// static
bool CookieCommons::ShouldIncludeCrossSiteCookie(Cookie* aCookie,
bool aPartitionForeign,
bool aInPrivateBrowsing,
bool aUsingStorageAccess) {
MOZ_ASSERT(aCookie);
int32_t sameSiteAttr = 0;
aCookie->GetSameSite(&sameSiteAttr);
return ShouldIncludeCrossSiteCookie(
sameSiteAttr, aCookie->IsPartitioned() && aCookie->RawIsPartitioned(),
aPartitionForeign, aInPrivateBrowsing, aUsingStorageAccess);
}
// static
bool CookieCommons::ShouldIncludeCrossSiteCookie(int32_t aSameSiteAttr,
bool aCookiePartitioned,
bool aPartitionForeign,
bool aInPrivateBrowsing,
bool aUsingStorageAccess) {
// CHIPS - If a third-party has storage access it can access both it's
// partitioned and unpartitioned cookie jars, else its cookies are blocked.
//
// Note that we will only include partitioned cookies that have "partitioned"
// attribution if we enable opt-in partitioning.
if (aPartitionForeign &&
(StaticPrefs::network_cookie_cookieBehavior_optInPartitioning() ||
(aInPrivateBrowsing &&
StaticPrefs::
network_cookie_cookieBehavior_optInPartitioning_pbmode())) &&
!aCookiePartitioned && !aUsingStorageAccess) {
return false;
}
return aSameSiteAttr == nsICookie::SAMESITE_NONE;
}
// static
bool CookieCommons::IsFirstPartyPartitionedCookieWithoutCHIPS(
Cookie* aCookie, const nsACString& aBaseDomain,
const OriginAttributes& aOriginAttributes) {
MOZ_ASSERT(aCookie);
// The cookie is set with partitioned attribute. This is a CHIPS cookies.
if (aCookie->RawIsPartitioned()) {
return false;
}
// The originAttributes is not partitioned. This is not a partitioned cookie.
if (aOriginAttributes.mPartitionKey.IsEmpty()) {
return false;
}
nsAutoString scheme;
nsAutoString baseDomain;
int32_t port;
bool foreignByAncestorContext;
// Bail out early if the partition key is not valid.
if (!OriginAttributes::ParsePartitionKey(aOriginAttributes.mPartitionKey,
scheme, baseDomain, port,
foreignByAncestorContext)) {
return false;
}
// Check whether the base domain of the cookie match the base domain in the
// partitionKey and it is not an ABA context
return aBaseDomain.Equals(NS_ConvertUTF16toUTF8(baseDomain)) &&
!foreignByAncestorContext;
}
bool CookieCommons::IsSafeTopLevelNav(nsIChannel* aChannel) {
if (!aChannel) {
return false;
}
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
nsCOMPtr<nsIInterceptionInfo> interceptionInfo = loadInfo->InterceptionInfo();
if ((loadInfo->GetExternalContentPolicyType() !=
ExtContentPolicy::TYPE_DOCUMENT &&
loadInfo->GetExternalContentPolicyType() !=
ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) &&
!interceptionInfo) {
return false;
}
if (interceptionInfo &&
interceptionInfo->GetExtContentPolicyType() !=
ExtContentPolicy::TYPE_DOCUMENT &&
interceptionInfo->GetExtContentPolicyType() !=
ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD &&
interceptionInfo->GetExtContentPolicyType() !=
ExtContentPolicy::TYPE_INVALID) {
return false;
}
return NS_IsSafeMethodNav(aChannel);
}
// This function determines if two schemes are equal in the context of
// "Schemeful SameSite cookies".
//
// Two schemes are considered equal:
// - if the "network.cookie.sameSite.schemeful" pref is set to false.
// OR
// - if one of the schemes is not http or https.
// OR
// - if both schemes are equal AND both are either http or https.
bool IsSameSiteSchemeEqual(const nsACString& aFirstScheme,
const nsACString& aSecondScheme) {
if (!StaticPrefs::network_cookie_sameSite_schemeful()) {
return true;
}
auto isSchemeHttpOrHttps = [](const nsACString& scheme) -> bool {
return scheme.EqualsLiteral("http") || scheme.EqualsLiteral("https");
};
if (!isSchemeHttpOrHttps(aFirstScheme) ||
!isSchemeHttpOrHttps(aSecondScheme)) {
return true;
}
return aFirstScheme.Equals(aSecondScheme);
}
bool CookieCommons::IsSameSiteForeign(nsIChannel* aChannel, nsIURI* aHostURI,
bool* aHadCrossSiteRedirects) {
*aHadCrossSiteRedirects = false;
if (!aChannel) {
return false;
}
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
// Do not treat loads triggered by web extensions as foreign
nsCOMPtr<nsIURI> channelURI;
NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI));
nsCOMPtr<nsIInterceptionInfo> interceptionInfo = loadInfo->InterceptionInfo();
RefPtr<BasePrincipal> triggeringPrincipal;
ExtContentPolicy contentPolicyType;
if (interceptionInfo && interceptionInfo->TriggeringPrincipal()) {
triggeringPrincipal =
BasePrincipal::Cast(interceptionInfo->TriggeringPrincipal());
contentPolicyType = interceptionInfo->GetExtContentPolicyType();
} else {
triggeringPrincipal = BasePrincipal::Cast(loadInfo->TriggeringPrincipal());
contentPolicyType = loadInfo->GetExternalContentPolicyType();
if (triggeringPrincipal->AddonPolicy() &&
triggeringPrincipal->AddonAllowsLoad(channelURI)) {
return false;
}
}
const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& redirectChain(
interceptionInfo && interceptionInfo->TriggeringPrincipal()
? interceptionInfo->RedirectChain()
: loadInfo->RedirectChain());
nsAutoCString hostScheme, otherScheme;
aHostURI->GetScheme(hostScheme);
bool isForeign = true;
nsresult rv;
if (contentPolicyType == ExtContentPolicy::TYPE_DOCUMENT ||
contentPolicyType == ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) {
// for loads of TYPE_DOCUMENT we query the hostURI from the
// triggeringPrincipal which returns the URI of the document that caused the
// navigation.
rv = triggeringPrincipal->IsThirdPartyChannel(aChannel, &isForeign);
triggeringPrincipal->GetScheme(otherScheme);
} else {
// If the load is caused by FetchEvent.request or NavigationPreload request,
// check the original InterceptedHttpChannel is a third-party channel or
// not.
if (interceptionInfo && interceptionInfo->TriggeringPrincipal()) {
isForeign = interceptionInfo->FromThirdParty();
if (isForeign) {
return true;
}
}
nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil =
do_GetService(THIRDPARTYUTIL_CONTRACTID);
if (!thirdPartyUtil) {
return true;
}
rv = thirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign);
channelURI->GetScheme(otherScheme);
}
// if we are dealing with a cross origin request, we can return here
// because we already know the request is 'foreign'.
if (NS_FAILED(rv) || isForeign) {
return true;
}
if (!IsSameSiteSchemeEqual(otherScheme, hostScheme)) {
// If the two schemes are not of the same http(s) scheme then we
// consider the request as foreign.
return true;
}
// for loads of TYPE_SUBDOCUMENT we have to perform an additional test,
// because a cross-origin iframe might perform a navigation to a same-origin
// iframe which would send same-site cookies. Hence, if the iframe navigation
// was triggered by a cross-origin triggeringPrincipal, we treat the load as
// foreign.
if (contentPolicyType == ExtContentPolicy::TYPE_SUBDOCUMENT) {
rv = triggeringPrincipal->IsThirdPartyChannel(aChannel, &isForeign);
if (NS_FAILED(rv) || isForeign) {
return true;
}
}
// for the purpose of same-site cookies we have to treat any cross-origin
// redirects as foreign. E.g. cross-site to same-site redirect is a problem
// with regards to CSRF.
nsCOMPtr<nsIPrincipal> redirectPrincipal;
for (nsIRedirectHistoryEntry* entry : redirectChain) {
entry->GetPrincipal(getter_AddRefs(redirectPrincipal));
if (redirectPrincipal) {
rv = redirectPrincipal->IsThirdPartyChannel(aChannel, &isForeign);
// if at any point we encounter a cross-origin redirect we can return.
if (NS_FAILED(rv) || isForeign) {
*aHadCrossSiteRedirects = isForeign;
return true;
}
nsAutoCString redirectScheme;
redirectPrincipal->GetScheme(redirectScheme);
if (!IsSameSiteSchemeEqual(redirectScheme, hostScheme)) {
// If the two schemes are not of the same http(s) scheme then we
// consider the request as foreign.
*aHadCrossSiteRedirects = true;
return true;
}
}
}
return isForeign;
}
// static
nsICookie::schemeType CookieCommons::URIToSchemeType(nsIURI* aURI) {
MOZ_ASSERT(aURI);
nsAutoCString scheme;
nsresult rv = aURI->GetScheme(scheme);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nsICookie::SCHEME_UNSET;
}
return SchemeToSchemeType(scheme);
}
// static
nsICookie::schemeType CookieCommons::PrincipalToSchemeType(
nsIPrincipal* aPrincipal) {
MOZ_ASSERT(aPrincipal);
nsAutoCString scheme;
nsresult rv = aPrincipal->GetScheme(scheme);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nsICookie::SCHEME_UNSET;
}
return SchemeToSchemeType(scheme);
}
// static
nsICookie::schemeType CookieCommons::SchemeToSchemeType(
const nsACString& aScheme) {
MOZ_ASSERT(IsSchemeSupported(aScheme));
if (aScheme.Equals("https")) {
return nsICookie::SCHEME_HTTPS;
}
if (aScheme.Equals("http")) {
return nsICookie::SCHEME_HTTP;
}
if (aScheme.Equals("file")) {
return nsICookie::SCHEME_FILE;
}
MOZ_CRASH("Unsupported scheme type");
}
// static
bool CookieCommons::IsSchemeSupported(nsIPrincipal* aPrincipal) {
MOZ_ASSERT(aPrincipal);
nsAutoCString scheme;
nsresult rv = aPrincipal->GetScheme(scheme);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
return IsSchemeSupported(scheme);
}
// static
bool CookieCommons::IsSchemeSupported(nsIURI* aURI) {
MOZ_ASSERT(aURI);
nsAutoCString scheme;
nsresult rv = aURI->GetScheme(scheme);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
return IsSchemeSupported(scheme);
}
// static
bool CookieCommons::IsSchemeSupported(const nsACString& aScheme) {
return aScheme.Equals("https") || aScheme.Equals("http") ||
aScheme.Equals("file");
}
// static
bool CookieCommons::ChipsLimitEnabledAndChipsCookie(
const Cookie& cookie, dom::BrowsingContext* aBrowsingContext) {
bool tcpEnabled = false;
if (aBrowsingContext) {
dom::CanonicalBrowsingContext* canonBC = aBrowsingContext->Canonical();
if (canonBC) {
dom::WindowGlobalParent* windowParent = canonBC->GetCurrentWindowGlobal();
if (windowParent) {
nsCOMPtr<nsICookieJarSettings> cjs = windowParent->CookieJarSettings();
tcpEnabled = cjs->GetPartitionForeign();
}
}
} else {
// calls coming from addNative have no document, channel or browsingContext
// to determine if TCP is enabled, so we just create a cookieJarSettings to
// check the pref.
nsCOMPtr<nsICookieJarSettings> cjs;
cjs = CookieJarSettings::Create(CookieJarSettings::eRegular, false);
tcpEnabled = cjs->GetPartitionForeign();
}
return StaticPrefs::network_cookie_CHIPS_enabled() &&
StaticPrefs::network_cookie_chips_partitionLimitEnabled() &&
cookie.IsPartitioned() && cookie.RawIsPartitioned() && tcpEnabled;
}
void CookieCommons::ComposeCookieString(nsTArray<RefPtr<Cookie>>& aCookieList,
nsACString& aCookieString) {
for (Cookie* cookie : aCookieList) {
// check if we have anything to write
if (!cookie->Name().IsEmpty() || !cookie->Value().IsEmpty()) {
// if we've already added a cookie to the return list, append a "; " so
// that subsequent cookies are delimited in the final list.
if (!aCookieString.IsEmpty()) {
aCookieString.AppendLiteral("; ");
}
if (!cookie->Name().IsEmpty()) {
// we have a name and value - write both
aCookieString += cookie->Name() + "="_ns + cookie->Value();
} else {
// just write value
aCookieString += cookie->Value();
}
}
}
}
// static
CookieCommons::SecurityChecksResult
CookieCommons::CheckGlobalAndRetrieveCookiePrincipals(
Document* aDocument, nsIPrincipal** aCookiePrincipal,
nsIPrincipal** aCookiePartitionedPrincipal) {
MOZ_ASSERT(aCookiePrincipal);
nsCOMPtr<nsIPrincipal> cookiePrincipal;
nsCOMPtr<nsIPrincipal> cookiePartitionedPrincipal;
if (!NS_IsMainThread()) {
MOZ_ASSERT(!aDocument);
dom::WorkerPrivate* workerPrivate = dom::GetCurrentThreadWorkerPrivate();
MOZ_ASSERT(workerPrivate);
StorageAccess storageAccess = workerPrivate->StorageAccess();
if (storageAccess == StorageAccess::eDeny) {
return SecurityChecksResult::eDoNotContinue;
}
cookiePrincipal = workerPrivate->GetPrincipal();
if (NS_WARN_IF(!cookiePrincipal) || cookiePrincipal->GetIsNullPrincipal()) {
return SecurityChecksResult::eSecurityError;
}
// CHIPS - If CHIPS is enabled the partitioned cookie jar is always
// available (and therefore the partitioned principal), the unpartitioned
// cookie jar is only available in first-party or third-party with
// storageAccess contexts. In both cases, the Worker will have storage
// access.
bool isCHIPS =
StaticPrefs::network_cookie_CHIPS_enabled() &&
!workerPrivate->CookieJarSettings()->GetBlockingAllContexts();
bool workerHasStorageAccess =
workerPrivate->StorageAccess() == StorageAccess::eAllow;
if (isCHIPS && workerHasStorageAccess) {
// Assert that the cookie principal is unpartitioned.
MOZ_ASSERT(
cookiePrincipal->OriginAttributesRef().mPartitionKey.IsEmpty());
// Only retrieve the partitioned originAttributes if the partitionKey is
// set. The partitionKey could be empty for partitionKey in partitioned
// originAttributes if the aWorker is for privilege context, such as the
// extension's background page.
nsCOMPtr<nsIPrincipal> partitionedPrincipal =
workerPrivate->GetPartitionedPrincipal();
if (partitionedPrincipal && !partitionedPrincipal->OriginAttributesRef()
.mPartitionKey.IsEmpty()) {
cookiePartitionedPrincipal = partitionedPrincipal;
}
}
} else {
if (!aDocument) {
return SecurityChecksResult::eDoNotContinue;
}
// If the document's sandboxed origin flag is set, then reading cookies
// is prohibited.
if (aDocument->GetSandboxFlags() & SANDBOXED_ORIGIN) {
return SecurityChecksResult::eSandboxedError;
}
cookiePrincipal = aDocument->EffectiveCookiePrincipal();
if (NS_WARN_IF(!cookiePrincipal) || cookiePrincipal->GetIsNullPrincipal()) {
return SecurityChecksResult::eSecurityError;
}
if (aDocument->CookieAccessDisabled()) {
return SecurityChecksResult::eDoNotContinue;
}
// GTests do not create an inner window and because of these a few security
// checks will block this method.
if (!StaticPrefs::dom_cookie_testing_enabled()) {
StorageAccess storageAccess = CookieAllowedForDocument(aDocument);
if (storageAccess == StorageAccess::eDeny) {
return SecurityChecksResult::eDoNotContinue;
}
if (ShouldPartitionStorage(storageAccess) &&
!StoragePartitioningEnabled(storageAccess,
aDocument->CookieJarSettings())) {
return SecurityChecksResult::eDoNotContinue;
}
// If the document is a cookie-averse Document... return the empty string.
if (aDocument->IsCookieAverse()) {
return SecurityChecksResult::eDoNotContinue;
}
}
// CHIPS - If CHIPS is enabled the partitioned cookie jar is always
// available (and therefore the partitioned principal), the unpartitioned
// cookie jar is only available in first-party or third-party with
// storageAccess contexts. In both cases, the aDocument will have storage
// access.
bool isCHIPS = StaticPrefs::network_cookie_CHIPS_enabled() &&
!aDocument->CookieJarSettings()->GetBlockingAllContexts();
bool documentHasStorageAccess = false;
nsresult rv = aDocument->HasStorageAccessSync(documentHasStorageAccess);
if (NS_WARN_IF(NS_FAILED(rv))) {
return SecurityChecksResult::eDoNotContinue;
}
if (isCHIPS && documentHasStorageAccess) {
// Assert that the cookie principal is unpartitioned.
MOZ_ASSERT(
cookiePrincipal->OriginAttributesRef().mPartitionKey.IsEmpty());
// Only append the partitioned originAttributes if the partitionKey is
// set. The partitionKey could be empty for partitionKey in partitioned
// originAttributes if the aDocument is for privilege context, such as the
// extension's background page.
if (!aDocument->PartitionedPrincipal()
->OriginAttributesRef()
.mPartitionKey.IsEmpty()) {
cookiePartitionedPrincipal = aDocument->PartitionedPrincipal();
}
}
}
if (!IsSchemeSupported(cookiePrincipal)) {
return SecurityChecksResult::eDoNotContinue;
}
cookiePrincipal.forget(aCookiePrincipal);
if (aCookiePartitionedPrincipal) {
cookiePartitionedPrincipal.forget(aCookiePartitionedPrincipal);
}
return SecurityChecksResult::eContinue;
}
// static
void CookieCommons::GetServerDateHeader(nsIChannel* aChannel,
nsACString& aServerDateHeader) {
if (!aChannel) {
return;
}
nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aChannel);
if (NS_WARN_IF(!channel)) {
return;
}
Unused << channel->GetResponseHeader("Date"_ns, aServerDateHeader);
}
} // namespace net
} // namespace mozilla