/* -*- 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 "CookieService.h" #include "mozilla/ContentBlocking.h" #include "mozilla/ConsoleReportCollector.h" #include "mozilla/ContentBlockingNotifier.h" #include "mozilla/ScopeExit.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/nsMixedContentBlocker.h" #include "mozilla/net/CookieJarSettings.h" #include "mozIThirdPartyUtil.h" #include "nsContentUtils.h" #include "nsICookiePermission.h" #include "nsICookieService.h" #include "nsIEffectiveTLDService.h" #include "nsIRedirectHistoryEntry.h" #include "nsIWebProgressListener.h" #include "nsNetUtil.h" #include "nsScriptSecurityManager.h" constexpr auto CONSOLE_SCHEMEFUL_CATEGORY = "cookieSchemeful"_ns; 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) { nsCString cookiePath(aCookie->GetFilePath()); // 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 (cookiePath.IsEmpty()) { return false; } // if the cookie path and the request path are identical, they match. if (cookiePath.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, cookiePath); if (isPrefix && cookiePath.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 = cookiePath.Length(); if (isPrefix && aPath[cookiePathLen] == '/') { return true; } return false; } // 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; } 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, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x00}; return aCookieData.name().FindCharInSet(illegalNameCharacters, 0) == -1; } bool CookieCommons::CheckHttpValue(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, 0x5C, and 0x7F are missing from this list. This is // for parity with Chrome. This only applies to cookies set via the Set-Cookie // header, as document.cookie is defined to be UTF-8. Hooray for // symmetry! 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, 0x00}; return aCookieData.value().FindCharInSet(illegalCharacters, 0) == -1; } // 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; } // Here we can have any legacy permission value. // now we need to figure out what type of accept policy we're dealing with // if we accept cookies normally, just bail and return if (StaticPrefs::network_cookie_lifetimePolicy() == nsICookieService::ACCEPT_NORMALLY) { return true; } // declare this here since it'll be used in all of the remaining cases int64_t currentTime = PR_Now() / PR_USEC_PER_SEC; int64_t delta = aCookieData.expiry() - currentTime; // We are accepting the cookie, but, // if it's not a session cookie, we may have to limit its lifetime. if (!aCookieData.isSession() && delta > 0) { if (StaticPrefs::network_cookie_lifetimePolicy() == nsICookieService::ACCEPT_SESSION) { // limit lifetime to session aCookieData.isSession() = true; } } return true; } namespace { CookieStatus CookieStatusForWindow(nsPIDOMWindowInner* aWindow, nsIURI* aDocumentURI) { MOZ_ASSERT(aWindow); MOZ_ASSERT(aDocumentURI); if (!nsContentUtils::IsThirdPartyWindowOrChannel(aWindow, nullptr, aDocumentURI)) { return STATUS_ACCEPTED; } if (StaticPrefs::network_cookie_thirdparty_sessionOnly()) { return STATUS_ACCEPT_SESSION; } if (StaticPrefs::network_cookie_thirdparty_nonsecureSessionOnly() && !nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aDocumentURI)) { return STATUS_ACCEPT_SESSION; } return STATUS_ACCEPTED; } } // namespace // static already_AddRefed CookieCommons::CreateCookieFromDocument( Document* aDocument, const nsACString& aCookieString, int64_t currentTimeInUsec, nsIEffectiveTLDService* aTLDService, mozIThirdPartyUtil* aThirdPartyUtil, std::function&& aHasExistingCookiesLambda, nsIURI** aDocumentURI, nsACString& aBaseDomain, OriginAttributes& aAttrs) { nsCOMPtr storagePrincipal = aDocument->EffectiveStoragePrincipal(); MOZ_ASSERT(storagePrincipal); nsCOMPtr principalURI; auto* basePrincipal = BasePrincipal::Cast(aDocument->NodePrincipal()); basePrincipal->GetURI(getter_AddRefs(principalURI)); if (NS_WARN_IF(!principalURI)) { // Document's principal is not a content or null (may be system), so // can't set cookies return nullptr; } if (!CookieCommons::IsSchemeSupported(principalURI)) { return nullptr; } nsAutoCString baseDomain; bool requireHostMatch = false; nsresult rv = CookieCommons::GetBaseDomain(aTLDService, principalURI, baseDomain, requireHostMatch); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow(); if (NS_WARN_IF(!innerWindow)) { return nullptr; } // Check if limit-foreign is required. uint32_t dummyRejectedReason = 0; if (aDocument->CookieJarSettings()->GetLimitForeignContexts() && !aHasExistingCookiesLambda(baseDomain, storagePrincipal->OriginAttributesRef()) && !ContentBlocking::ShouldAllowAccessFor(innerWindow, principalURI, &dummyRejectedReason)) { return nullptr; } bool isForeignAndNotAddon = false; if (!BasePrincipal::Cast(aDocument->NodePrincipal())->AddonPolicy()) { rv = aThirdPartyUtil->IsThirdPartyWindow( innerWindow->GetOuterWindow(), principalURI, &isForeignAndNotAddon); if (NS_WARN_IF(NS_FAILED(rv))) { isForeignAndNotAddon = true; } } // 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, principalURI); MOZ_ASSERT(cookieStatus == STATUS_ACCEPTED || cookieStatus == STATUS_ACCEPT_SESSION); // Console report takes care of the correct reporting at the exit of this // method. RefPtr crc = new ConsoleReportCollector(); auto scopeExit = MakeScopeExit([&] { crc->FlushConsoleReports(aDocument); }); nsCString cookieString(aCookieString); CookieStruct cookieData; bool canSetCookie = false; CookieService::CanSetCookie(principalURI, baseDomain, cookieData, requireHostMatch, cookieStatus, cookieString, false, isForeignAndNotAddon, crc, canSetCookie); if (!canSetCookie) { return nullptr; } // check permissions from site permission list. if (!CookieCommons::CheckCookiePermission(aDocument->NodePrincipal(), aDocument->CookieJarSettings(), cookieData)) { NotifyRejectionToObservers(principalURI, OPERATION_WRITE); ContentBlockingNotifier::OnDecision( innerWindow, ContentBlockingNotifier::BlockingDecision::eBlock, nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION); return nullptr; } RefPtr cookie = Cookie::Create(cookieData, storagePrincipal->OriginAttributesRef()); MOZ_ASSERT(cookie); cookie->SetLastAccessed(currentTimeInUsec); cookie->SetCreationTime( Cookie::GenerateUniqueCreationTime(currentTimeInUsec)); aBaseDomain = baseDomain; aAttrs = storagePrincipal->OriginAttributesRef(); principalURI.forget(aDocumentURI); return cookie.forget(); } // static already_AddRefed CookieCommons::GetCookieJarSettings( nsIChannel* aChannel) { nsCOMPtr cookieJarSettings; if (aChannel) { nsCOMPtr loadInfo = aChannel->LoadInfo(); nsresult rv = loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); if (NS_WARN_IF(NS_FAILED(rv))) { cookieJarSettings = CookieJarSettings::GetBlockingAll(); } } else { cookieJarSettings = CookieJarSettings::Create(); } MOZ_ASSERT(cookieJarSettings); return cookieJarSettings.forget(); } // static bool CookieCommons::ShouldIncludeCrossSiteCookieForDocument(Cookie* aCookie) { MOZ_ASSERT(aCookie); int32_t sameSiteAttr = 0; aCookie->GetSameSite(&sameSiteAttr); return sameSiteAttr == nsICookie::SAMESITE_NONE; } bool CookieCommons::IsSafeTopLevelNav(nsIChannel* aChannel) { if (!aChannel) { return false; } nsCOMPtr loadInfo = aChannel->LoadInfo(); if (loadInfo->GetExternalContentPolicyType() != ExtContentPolicy::TYPE_DOCUMENT && loadInfo->GetExternalContentPolicyType() != ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) { return false; } return NS_IsSafeMethodNav(aChannel); } bool CookieCommons::IsSameSiteForeign(nsIChannel* aChannel, nsIURI* aHostURI) { 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)); RefPtr triggeringPrincipal = BasePrincipal::Cast(loadInfo->TriggeringPrincipal()); if (triggeringPrincipal->AddonPolicy() && triggeringPrincipal->AddonAllowsLoad(channelURI)) { return false; } bool isForeign = true; nsresult rv; if (loadInfo->GetExternalContentPolicyType() == ExtContentPolicy::TYPE_DOCUMENT || loadInfo->GetExternalContentPolicyType() == 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); } else { nsCOMPtr thirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID); if (!thirdPartyUtil) { return true; } rv = thirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign); } // 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; } // 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 (loadInfo->GetExternalContentPolicyType() == ExtContentPolicy::TYPE_SUBDOCUMENT) { rv = loadInfo->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 : loadInfo->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) { return true; } } } return isForeign; } namespace { bool MaybeCompareSchemeInternal(Cookie* aCookie, nsICookie::schemeType aSchemeType) { MOZ_ASSERT(aCookie); // This is an old cookie without a scheme yet. Let's consider it valid. if (aCookie->SchemeMap() == nsICookie::SCHEME_UNSET) { return true; } return !!(aCookie->SchemeMap() & aSchemeType); } } // namespace // static bool CookieCommons::MaybeCompareSchemeWithLogging( nsIConsoleReportCollector* aCRC, nsIURI* aHostURI, Cookie* aCookie, nsICookie::schemeType aSchemeType) { MOZ_ASSERT(aCookie); MOZ_ASSERT(aHostURI); if (MaybeCompareSchemeInternal(aCookie, aSchemeType)) { return true; } nsAutoCString uri; nsresult rv = aHostURI->GetSpec(uri); if (NS_WARN_IF(NS_FAILED(rv))) { return !StaticPrefs::network_cookie_sameSite_schemeful(); } if (!StaticPrefs::network_cookie_sameSite_schemeful()) { CookieLogging::LogMessageToConsole( aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SCHEMEFUL_CATEGORY, "CookieSchemefulRejectForBeta"_ns, AutoTArray{NS_ConvertUTF8toUTF16(aCookie->Name()), NS_ConvertUTF8toUTF16(uri)}); return true; } CookieLogging::LogMessageToConsole( aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SCHEMEFUL_CATEGORY, "CookieSchemefulReject"_ns, AutoTArray{NS_ConvertUTF8toUTF16(aCookie->Name()), NS_ConvertUTF8toUTF16(uri)}); return false; } // static bool CookieCommons::MaybeCompareScheme(Cookie* aCookie, nsICookie::schemeType aSchemeType) { MOZ_ASSERT(aCookie); if (!StaticPrefs::network_cookie_sameSite_schemeful()) { return true; } return MaybeCompareSchemeInternal(aCookie, aSchemeType); } // 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"); } } // namespace net } // namespace mozilla