/* -*- 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 "nsReadableUtils.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 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 // https://bugzilla.mozilla.org/show_bug.cgi?id=1191423 // 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 loadInfo = aChannel->LoadInfo(); nsCOMPtr 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 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 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); nsCOMPtr loadInfo = aDocument->GetChannel() ? aDocument->GetChannel()->LoadInfo() : nullptr; const bool on3pcbException = loadInfo && loadInfo->GetIsOn3PCBExceptionList(); aCookieParser.Parse(baseDomain, requireHostMatch, cookieStatus, cookieString, EmptyCString(), false, isForeignAndNotAddon, mustBePartitioned, aDocument->IsInPrivateBrowsing(), on3pcbException); 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 cookiePrincipal = needPartitioned ? aDocument->PartitionedPrincipal() : aDocument->EffectiveCookiePrincipal(); MOZ_ASSERT(cookiePrincipal); nsCOMPtr 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(), true, &dummyRejectedReason)) { return nullptr; } RefPtr 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 CookieCommons::GetCookieJarSettings( nsIChannel* aChannel) { nsCOMPtr cookieJarSettings; bool shouldResistFingerprinting = nsContentUtils::ShouldResistFingerprinting( aChannel, RFPTarget::IsAlwaysEnabledForPrecompute); if (aChannel) { nsCOMPtr 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, nsIURI* aHostURI, bool aPartitionForeign, bool aInPrivateBrowsing, bool aUsingStorageAccess, bool aOn3pcbException) { MOZ_ASSERT(aCookie); int32_t sameSiteAttr = 0; aCookie->GetSameSite(&sameSiteAttr); return ShouldIncludeCrossSiteCookie( aHostURI, sameSiteAttr, aCookie->IsPartitioned() && aCookie->RawIsPartitioned(), aPartitionForeign, aInPrivateBrowsing, aUsingStorageAccess, aOn3pcbException); } // static bool CookieCommons::ShouldIncludeCrossSiteCookie( nsIURI* aHostURI, int32_t aSameSiteAttr, bool aCookiePartitioned, bool aPartitionForeign, bool aInPrivateBrowsing, bool aUsingStorageAccess, bool aOn3pcbException) { if (aSameSiteAttr == nsICookie::SAMESITE_UNSET) { bool laxByDefault = StaticPrefs::network_cookie_sameSite_laxByDefault() && !nsContentUtils::IsURIInPrefList( aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts"); aSameSiteAttr = laxByDefault ? nsICookie::SAMESITE_LAX : nsICookie::SAMESITE_NONE; } // 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 && !aOn3pcbException) { 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; } // static bool CookieCommons::ShouldEnforceSessionForOriginAttributes( const OriginAttributes& aOriginAttributes) { // We don't need to enforce session for unpartitioned OAs. if (aOriginAttributes.mPartitionKey.IsEmpty()) { return false; } // We enforce session cookies if the partitionKey is from a null principal. // This ensures that we don't create dangling cookies that cannot be deleted. // A partitionKey from a null principal ends with ".mozilla". if (StringEndsWith(aOriginAttributes.mPartitionKey, u".mozilla"_ns)) { return true; } return false; } bool CookieCommons::IsSafeTopLevelNav(nsIChannel* aChannel) { if (!aChannel) { return false; } nsCOMPtr loadInfo = aChannel->LoadInfo(); nsCOMPtr 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 loadInfo = aChannel->LoadInfo(); // Do not treat loads triggered by web extensions as foreign nsCOMPtr channelURI; NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI)); nsCOMPtr interceptionInfo = loadInfo->InterceptionInfo(); RefPtr 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>& 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 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 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 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 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>& 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 cookiePrincipal; nsCOMPtr 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) { // 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 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) { // 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 channel = do_QueryInterface(aChannel); if (NS_WARN_IF(!channel)) { return; } Unused << channel->GetResponseHeader("Date"_ns, aServerDateHeader); } // static int64_t CookieCommons::MaybeReduceExpiry(int64_t aCurrentTimeInSec, int64_t aExpiryInSec) { int64_t maxageCap = StaticPrefs::network_cookie_maxageCap(); if (maxageCap) { aExpiryInSec = std::min(aExpiryInSec, aCurrentTimeInSec + maxageCap); } return aExpiryInSec; } } // namespace net } // namespace mozilla