/* -*- 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/net/CookieServiceChild.h" #include "mozilla/net/NeckoChannelParams.h" #include "mozilla/LoadInfo.h" #include "mozilla/BasePrincipal.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/Document.h" #include "mozilla/ipc/URIUtils.h" #include "mozilla/net/NeckoChild.h" #include "mozilla/StaticPrefs_network.h" #include "mozilla/StoragePrincipalHelper.h" #include "nsNetCID.h" #include "nsNetUtil.h" #include "nsIChannel.h" #include "nsIClassifiedChannel.h" #include "nsIHttpChannel.h" #include "nsIEffectiveTLDService.h" #include "nsIURI.h" #include "nsIPrefBranch.h" #include "nsServiceManagerUtils.h" #include "mozilla/Telemetry.h" #include "mozilla/TimeStamp.h" #include "ThirdPartyUtil.h" using namespace mozilla::ipc; namespace mozilla { namespace net { // Pref string constants static const char kCookieMoveIntervalSecs[] = "network.cookie.move.interval_sec"; static StaticRefPtr gCookieChildService; static uint32_t gMoveCookiesIntervalSeconds = 10; already_AddRefed CookieServiceChild::GetSingleton() { if (!gCookieChildService) { gCookieChildService = new CookieServiceChild(); ClearOnShutdown(&gCookieChildService); } return do_AddRef(gCookieChildService); } NS_IMPL_ISUPPORTS(CookieServiceChild, nsICookieService, nsIObserver, nsITimerCallback, nsISupportsWeakReference) CookieServiceChild::CookieServiceChild() { NS_ASSERTION(IsNeckoChild(), "not a child process"); auto* cc = static_cast(gNeckoChild->Manager()); if (cc->IsShuttingDown()) { return; } // This corresponds to Release() in DeallocPCookieService. NS_ADDREF_THIS(); NeckoChild::InitNeckoChild(); // Create a child PCookieService actor. gNeckoChild->SendPCookieServiceConstructor(this); mThirdPartyUtil = ThirdPartyUtil::GetInstance(); NS_ASSERTION(mThirdPartyUtil, "couldn't get ThirdPartyUtil service"); mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); NS_ASSERTION(mTLDService, "couldn't get TLDService"); // Init our prefs and observer. nsCOMPtr prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID); NS_WARNING_ASSERTION(prefBranch, "no prefservice"); if (prefBranch) { prefBranch->AddObserver(kCookieMoveIntervalSecs, this, true); PrefChanged(prefBranch); } nsCOMPtr observerService = services::GetObserverService(); if (observerService) { observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); } } void CookieServiceChild::MoveCookies() { TimeStamp start = TimeStamp::Now(); for (auto iter = mCookiesMap.Iter(); !iter.Done(); iter.Next()) { CookiesList* cookiesList = iter.UserData(); CookiesList newCookiesList; for (uint32_t i = 0; i < cookiesList->Length(); ++i) { Cookie* cookie = cookiesList->ElementAt(i); RefPtr newCookie = cookie->Clone(); newCookiesList.AppendElement(newCookie); } *cookiesList = std::move(newCookiesList); } Telemetry::AccumulateTimeDelta(Telemetry::COOKIE_TIME_MOVING_MS, start); } NS_IMETHODIMP CookieServiceChild::Notify(nsITimer* aTimer) { if (aTimer == mCookieTimer) { MoveCookies(); } else { MOZ_CRASH("Unknown timer"); } return NS_OK; } CookieServiceChild::~CookieServiceChild() { gCookieChildService = nullptr; } void CookieServiceChild::TrackCookieLoad(nsIChannel* aChannel) { if (!CanSend()) { return; } uint32_t rejectedReason = 0; ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel( aChannel, true, nullptr, RequireThirdPartyCheck, &rejectedReason); nsCOMPtr uri; aChannel->GetURI(getter_AddRefs(uri)); nsCOMPtr loadInfo = aChannel->LoadInfo(); OriginAttributes attrs = loadInfo->GetOriginAttributes(); StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( aChannel, attrs); bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel); bool isSameSiteForeign = CookieCommons::IsSameSiteForeign(aChannel, uri); SendPrepareCookieList( uri, result.contains(ThirdPartyAnalysis::IsForeign), result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource), result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource), result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted), rejectedReason, isSafeTopLevelNav, isSameSiteForeign, attrs); } IPCResult CookieServiceChild::RecvRemoveAll() { mCookiesMap.Clear(); return IPC_OK(); } IPCResult CookieServiceChild::RecvRemoveCookie(const CookieStruct& aCookie, const OriginAttributes& aAttrs) { nsCString baseDomain; CookieCommons::GetBaseDomainFromHost(mTLDService, aCookie.host(), baseDomain); CookieKey key(baseDomain, aAttrs); CookiesList* cookiesList = nullptr; mCookiesMap.Get(key, &cookiesList); if (!cookiesList) { return IPC_OK(); } for (uint32_t i = 0; i < cookiesList->Length(); i++) { Cookie* cookie = cookiesList->ElementAt(i); if (cookie->Name().Equals(aCookie.name()) && cookie->Host().Equals(aCookie.host()) && cookie->Path().Equals(aCookie.path())) { cookiesList->RemoveElementAt(i); break; } } return IPC_OK(); } IPCResult CookieServiceChild::RecvAddCookie(const CookieStruct& aCookie, const OriginAttributes& aAttrs) { RefPtr cookie = Cookie::Create(aCookie, aAttrs); RecordDocumentCookie(cookie, aAttrs); return IPC_OK(); } IPCResult CookieServiceChild::RecvRemoveBatchDeletedCookies( nsTArray&& aCookiesList, nsTArray&& aAttrsList) { MOZ_ASSERT(aCookiesList.Length() == aAttrsList.Length()); for (uint32_t i = 0; i < aCookiesList.Length(); i++) { CookieStruct cookieStruct = aCookiesList.ElementAt(i); RecvRemoveCookie(cookieStruct, aAttrsList.ElementAt(i)); } return IPC_OK(); } IPCResult CookieServiceChild::RecvTrackCookiesLoad( nsTArray&& aCookiesList, const OriginAttributes& aAttrs) { for (uint32_t i = 0; i < aCookiesList.Length(); i++) { RefPtr cookie = Cookie::Create(aCookiesList[i], aAttrs); cookie->SetIsHttpOnly(false); RecordDocumentCookie(cookie, aAttrs); } return IPC_OK(); } void CookieServiceChild::PrefChanged(nsIPrefBranch* aPrefBranch) { int32_t val; if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kCookieMoveIntervalSecs, &val))) { gMoveCookiesIntervalSeconds = clamped(val, 0, 3600); if (gMoveCookiesIntervalSeconds && !mCookieTimer) { NS_NewTimerWithCallback(getter_AddRefs(mCookieTimer), this, gMoveCookiesIntervalSeconds * 1000, nsITimer::TYPE_REPEATING_SLACK_LOW_PRIORITY); } if (!gMoveCookiesIntervalSeconds && mCookieTimer) { mCookieTimer->Cancel(); mCookieTimer = nullptr; } if (mCookieTimer) { mCookieTimer->SetDelay(gMoveCookiesIntervalSeconds * 1000); } } } uint32_t CookieServiceChild::CountCookiesFromHashTable( const nsACString& aBaseDomain, const OriginAttributes& aOriginAttrs) { CookiesList* cookiesList = nullptr; nsCString baseDomain; CookieKey key(aBaseDomain, aOriginAttrs); mCookiesMap.Get(key, &cookiesList); return cookiesList ? cookiesList->Length() : 0; } /* static */ bool CookieServiceChild::RequireThirdPartyCheck( nsILoadInfo* aLoadInfo) { if (!aLoadInfo) { return false; } nsCOMPtr cookieJarSettings; nsresult rv = aLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } uint32_t cookieBehavior = cookieJarSettings->GetCookieBehavior(); return cookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN || cookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN || cookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || cookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN || StaticPrefs::network_cookie_thirdparty_sessionOnly() || StaticPrefs::network_cookie_thirdparty_nonsecureSessionOnly(); } void CookieServiceChild::RecordDocumentCookie(Cookie* aCookie, const OriginAttributes& aAttrs) { nsAutoCString baseDomain; CookieCommons::GetBaseDomainFromHost(mTLDService, aCookie->Host(), baseDomain); CookieKey key(baseDomain, aAttrs); CookiesList* cookiesList = nullptr; mCookiesMap.Get(key, &cookiesList); if (!cookiesList) { cookiesList = mCookiesMap.LookupOrAdd(key); } for (uint32_t i = 0; i < cookiesList->Length(); i++) { Cookie* cookie = cookiesList->ElementAt(i); if (cookie->Name().Equals(aCookie->Name()) && cookie->Host().Equals(aCookie->Host()) && cookie->Path().Equals(aCookie->Path())) { if (cookie->Value().Equals(aCookie->Value()) && cookie->Expiry() == aCookie->Expiry() && cookie->IsSecure() == aCookie->IsSecure() && cookie->SameSite() == aCookie->SameSite() && cookie->IsSession() == aCookie->IsSession() && cookie->IsHttpOnly() == aCookie->IsHttpOnly()) { cookie->SetLastAccessed(aCookie->LastAccessed()); return; } cookiesList->RemoveElementAt(i); break; } } int64_t currentTime = PR_Now() / PR_USEC_PER_SEC; if (aCookie->Expiry() <= currentTime) { return; } cookiesList->AppendElement(aCookie); } NS_IMETHODIMP CookieServiceChild::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* /*aData*/) { if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { if (mCookieTimer) { mCookieTimer->Cancel(); mCookieTimer = nullptr; } nsCOMPtr observerService = services::GetObserverService(); if (observerService) { observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); } } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { nsCOMPtr prefBranch = do_QueryInterface(aSubject); if (prefBranch) { PrefChanged(prefBranch); } } else { MOZ_ASSERT(false, "unexpected topic!"); } return NS_OK; } NS_IMETHODIMP CookieServiceChild::GetCookieStringFromDocument(Document* aDocument, nsACString& aCookieString) { NS_ENSURE_ARG(aDocument); aCookieString.Truncate(); nsCOMPtr principal = aDocument->EffectiveStoragePrincipal(); if (!CookieCommons::IsSchemeSupported(principal)) { return NS_OK; } nsICookie::schemeType schemeType = CookieCommons::PrincipalToSchemeType(principal); nsAutoCString baseDomain; nsresult rv = CookieCommons::GetBaseDomain(principal, baseDomain); if (NS_WARN_IF(NS_FAILED(rv))) { return NS_OK; } CookieKey key(baseDomain, principal->OriginAttributesRef()); CookiesList* cookiesList = nullptr; mCookiesMap.Get(key, &cookiesList); if (!cookiesList) { return NS_OK; } nsAutoCString hostFromURI; principal->GetAsciiHost(hostFromURI); nsAutoCString pathFromURI; principal->GetFilePath(pathFromURI); bool thirdParty = true; nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow(); // in gtests we don't have a window, let's consider those requests as 3rd // party. if (innerWindow) { thirdParty = nsContentUtils::IsThirdPartyWindowOrChannel(innerWindow, nullptr, nullptr); } bool isPotentiallyTrustworthy = principal->GetIsOriginPotentiallyTrustworthy(); int64_t currentTimeInUsec = PR_Now(); int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC; cookiesList->Sort(CompareCookiesForSending()); for (uint32_t i = 0; i < cookiesList->Length(); i++) { Cookie* cookie = cookiesList->ElementAt(i); // check the host, since the base domain lookup is conservative. if (!CookieCommons::DomainMatches(cookie, hostFromURI)) { continue; } // We don't show HttpOnly cookies in content processes. if (cookie->IsHttpOnly()) { continue; } if (thirdParty && !CookieCommons::ShouldIncludeCrossSiteCookieForDocument(cookie)) { continue; } // if the cookie is secure and the host scheme isn't, we can't send it if (cookie->IsSecure() && !isPotentiallyTrustworthy) { continue; } if (!CookieCommons::MaybeCompareScheme(cookie, schemeType)) { continue; } // if the nsIURI path doesn't match the cookie path, don't send it back if (!CookieCommons::PathMatches(cookie, pathFromURI)) { continue; } // check if the cookie has expired if (cookie->Expiry() <= currentTime) { continue; } if (!cookie->Name().IsEmpty() || !cookie->Value().IsEmpty()) { if (!aCookieString.IsEmpty()) { aCookieString.AppendLiteral("; "); } if (!cookie->Name().IsEmpty()) { aCookieString.Append(cookie->Name().get()); aCookieString.AppendLiteral("="); aCookieString.Append(cookie->Value().get()); } else { aCookieString.Append(cookie->Value().get()); } } } return NS_OK; } NS_IMETHODIMP CookieServiceChild::GetCookieStringFromHttp(nsIURI* /*aHostURI*/, nsIChannel* /*aChannel*/, nsACString& /*aCookieString*/) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP CookieServiceChild::SetCookieStringFromDocument( Document* aDocument, const nsACString& aCookieString) { NS_ENSURE_ARG(aDocument); nsCOMPtr documentURI; nsAutoCString baseDomain; OriginAttributes attrs; // This function is executed in this context, I don't need to keep objects // alive. auto hasExistingCookiesLambda = [&](const nsACString& aBaseDomain, const OriginAttributes& aAttrs) { return !!CountCookiesFromHashTable(aBaseDomain, aAttrs); }; RefPtr cookie = CookieCommons::CreateCookieFromDocument( aDocument, aCookieString, PR_Now(), mTLDService, mThirdPartyUtil, hasExistingCookiesLambda, getter_AddRefs(documentURI), baseDomain, attrs); if (!cookie) { return NS_OK; } bool thirdParty = true; nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow(); // in gtests we don't have a window, let's consider those requests as 3rd // party. if (innerWindow) { thirdParty = nsContentUtils::IsThirdPartyWindowOrChannel(innerWindow, nullptr, nullptr); } if (thirdParty && !CookieCommons::ShouldIncludeCrossSiteCookieForDocument(cookie)) { return NS_OK; } CookieKey key(baseDomain, attrs); CookiesList* cookies = mCookiesMap.Get(key); if (cookies) { // We need to see if the cookie we're setting would overwrite an httponly // one. This would not affect anything we send over the net (those come // from the parent, which already checks this), but script could see an // inconsistent view of things. for (uint32_t i = 0; i < cookies->Length(); ++i) { RefPtr existingCookie = cookies->ElementAt(i); if (existingCookie->Name().Equals(cookie->Name()) && existingCookie->Host().Equals(cookie->Host()) && existingCookie->Path().Equals(cookie->Path()) && existingCookie->IsHttpOnly()) { // Can't overwrite an httponly cookie from a script context. return NS_OK; } } } RecordDocumentCookie(cookie, attrs); if (CanSend()) { nsTArray cookiesToSend; cookiesToSend.AppendElement(cookie->ToIPC()); // Asynchronously call the parent. SendSetCookies(baseDomain, attrs, documentURI, false, cookiesToSend); } return NS_OK; } NS_IMETHODIMP CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI, const nsACString& aCookieString, nsIChannel* aChannel) { NS_ENSURE_ARG(aHostURI); NS_ENSURE_ARG(aChannel); if (!CookieCommons::IsSchemeSupported(aHostURI)) { return NS_OK; } // Fast past: don't bother sending IPC messages about nullprincipal'd // documents. nsAutoCString scheme; aHostURI->GetScheme(scheme); if (scheme.EqualsLiteral("moz-nullprincipal")) { return NS_OK; } nsCOMPtr loadInfo = aChannel->LoadInfo(); uint32_t rejectedReason = 0; ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel( aChannel, false, aHostURI, RequireThirdPartyCheck, &rejectedReason); nsCString cookieString(aCookieString); OriginAttributes attrs = loadInfo->GetOriginAttributes(); StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( aChannel, attrs); bool requireHostMatch; nsCString baseDomain; CookieCommons::GetBaseDomain(mTLDService, aHostURI, baseDomain, requireHostMatch); nsCOMPtr cookieJarSettings = CookieCommons::GetCookieJarSettings(aChannel); nsCOMPtr crc = do_QueryInterface(aChannel); CookieStatus cookieStatus = CookieService::CheckPrefs( crc, cookieJarSettings, aHostURI, result.contains(ThirdPartyAnalysis::IsForeign), result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource), result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource), result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted), aCookieString, CountCookiesFromHashTable(baseDomain, attrs), attrs, &rejectedReason); if (cookieStatus != STATUS_ACCEPTED && cookieStatus != STATUS_ACCEPT_SESSION) { return NS_OK; } CookieKey key(baseDomain, attrs); nsTArray cookiesToSend; int64_t currentTimeInUsec = PR_Now(); bool addonAllowsLoad = false; nsCOMPtr finalChannelURI; NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalChannelURI)); addonAllowsLoad = BasePrincipal::Cast(loadInfo->TriggeringPrincipal()) ->AddonAllowsLoad(finalChannelURI); bool isForeignAndNotAddon = false; if (!addonAllowsLoad) { mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeignAndNotAddon); } bool moreCookies; do { CookieStruct cookieData; bool canSetCookie = false; moreCookies = CookieService::CanSetCookie( aHostURI, baseDomain, cookieData, requireHostMatch, cookieStatus, cookieString, true, isForeignAndNotAddon, crc, canSetCookie); if (!canSetCookie) { continue; } // check permissions from site permission list. if (!CookieCommons::CheckCookiePermission(aChannel, cookieData)) { COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieString, "cookie rejected by permission manager"); CookieLogging::LogMessageToConsole( crc, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY, "CookieRejectedByPermissionManager"_ns, AutoTArray{ NS_ConvertUTF8toUTF16(cookieData.name()), }); CookieCommons::NotifyRejected( aHostURI, aChannel, nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION, OPERATION_WRITE); continue; } RefPtr cookie = Cookie::Create(cookieData, attrs); MOZ_ASSERT(cookie); cookie->SetLastAccessed(currentTimeInUsec); cookie->SetCreationTime( Cookie::GenerateUniqueCreationTime(currentTimeInUsec)); RecordDocumentCookie(cookie, attrs); cookiesToSend.AppendElement(cookieData); } while (moreCookies); // Asynchronously call the parent. if (CanSend() && !cookiesToSend.IsEmpty()) { SendSetCookies(baseDomain, attrs, aHostURI, true, cookiesToSend); } return NS_OK; } NS_IMETHODIMP CookieServiceChild::RunInTransaction( nsICookieTransactionCallback* /*aCallback*/) { return NS_ERROR_NOT_IMPLEMENTED; } } // namespace net } // namespace mozilla