/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AntiTrackingLog.h" #include "DynamicFpiRedirectHeuristic.h" #include "ContentBlockingAllowList.h" #include "ContentBlockingUserInteraction.h" #include "StorageAccessAPIHelper.h" #include "mozilla/net/HttpBaseChannel.h" #include "mozilla/net/UrlClassifierCommon.h" #include "mozilla/Telemetry.h" #include "nsContentUtils.h" #include "nsIChannel.h" #include "nsICookieJarSettings.h" #include "nsICookieService.h" #include "nsIEffectiveTLDService.h" #include "nsINavHistoryService.h" #include "nsIRedirectHistoryEntry.h" #include "nsIScriptError.h" #include "nsIURI.h" #include "nsNetCID.h" #include "nsNetUtil.h" #include "nsScriptSecurityManager.h" #include "nsToolkitCompsCID.h" namespace mozilla { namespace { nsresult GetBaseDomain(nsIURI* aURI, nsACString& aBaseDomain) { nsCOMPtr tldService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); if (!tldService) { return NS_ERROR_FAILURE; } return tldService->GetBaseDomain(aURI, 0, aBaseDomain); } // check if there's any interacting visit within the given seconds bool HasEligibleVisit( nsIURI* aURI, int64_t aSinceInSec = StaticPrefs:: privacy_restrict3rdpartystorage_heuristic_recently_visited_time()) { nsresult rv; nsAutoCString baseDomain; rv = GetBaseDomain(aURI, baseDomain); NS_ENSURE_SUCCESS(rv, false); nsCOMPtr histSrv = do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID); if (!histSrv) { return false; } nsCOMPtr histQuery; rv = histSrv->GetNewQuery(getter_AddRefs(histQuery)); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } rv = histQuery->SetDomain(baseDomain); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } rv = histQuery->SetDomainIsHost(false); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } PRTime beginTime = PR_Now() - PRTime(PR_USEC_PER_SEC) * aSinceInSec; rv = histQuery->SetBeginTime(beginTime); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } nsCOMPtr histQueryOpts; rv = histSrv->GetNewQueryOptions(getter_AddRefs(histQueryOpts)); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } rv = histQueryOpts->SetResultType(nsINavHistoryQueryOptions::RESULTS_AS_VISIT); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } rv = histQueryOpts->SetMaxResults(1); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } rv = histQueryOpts->SetQueryType( nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } nsCOMPtr histResult; rv = histSrv->ExecuteQuery(histQuery, histQueryOpts, getter_AddRefs(histResult)); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } nsCOMPtr histResultContainer; rv = histResult->GetRoot(getter_AddRefs(histResultContainer)); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } rv = histResultContainer->SetContainerOpen(true); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } uint32_t childCount = 0; rv = histResultContainer->GetChildCount(&childCount); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } rv = histResultContainer->SetContainerOpen(false); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } return childCount > 0; } void AddConsoleReport(nsIChannel* aNewChannel, nsIURI* aNewURI, const nsACString& aOldOrigin, const nsACString& aNewOrigin) { nsCOMPtr httpChannel = do_QueryInterface(aNewChannel); if (!httpChannel) { return; } nsAutoCString uri; nsresult rv = aNewURI->GetSpec(uri); if (NS_WARN_IF(NS_FAILED(rv))) { return; } AutoTArray params = {NS_ConvertUTF8toUTF16(aNewOrigin), NS_ConvertUTF8toUTF16(aOldOrigin)}; httpChannel->AddConsoleReport(nsIScriptError::warningFlag, ANTITRACKING_CONSOLE_CATEGORY, nsContentUtils::eNECKO_PROPERTIES, uri, 0, 0, "CookieAllowedForFpiByHeuristic"_ns, params); } bool ShouldRedirectHeuristicApplyTrackingResource(nsIChannel* aOldChannel, nsIURI* aOldURI, nsIChannel* aNewChannel, nsIURI* aNewURI) { nsCOMPtr classifiedOldChannel = do_QueryInterface(aOldChannel); if (!classifiedOldChannel) { LOG_SPEC2(("Ignoring redirect for %s to %s because there is not " "nsIClassifiedChannel interface", _spec1, _spec2), aOldURI, aNewURI); return false; } // We're looking at the first-party classification flags because we're // interested in first-party redirects. uint32_t oldClassificationFlags = classifiedOldChannel->GetFirstPartyClassificationFlags(); if (net::UrlClassifierCommon::IsTrackingClassificationFlag( oldClassificationFlags, NS_UsePrivateBrowsing(aOldChannel))) { // This is a redirect from tracking. LOG_SPEC2(("Ignoring redirect for %s to %s because it's from tracking ", _spec1, _spec2), aOldURI, aNewURI); return false; } return true; } } // namespace void DynamicFpiRedirectHeuristic(nsIChannel* aOldChannel, nsIURI* aOldURI, nsIChannel* aNewChannel, nsIURI* aNewURI) { MOZ_ASSERT(aOldChannel); MOZ_ASSERT(aOldURI); MOZ_ASSERT(aNewChannel); MOZ_ASSERT(aNewURI); nsresult rv; if (!StaticPrefs::privacy_antitracking_enableWebcompat() || !StaticPrefs:: privacy_restrict3rdpartystorage_heuristic_recently_visited()) { return; } nsCOMPtr oldChannel = do_QueryInterface(aOldChannel); nsCOMPtr newChannel = do_QueryInterface(aNewChannel); if (!oldChannel || !newChannel) { return; } LOG_SPEC(("Checking dfpi redirect-heuristic for %s", _spec), aOldURI); nsCOMPtr oldLoadInfo = aOldChannel->LoadInfo(); MOZ_ASSERT(oldLoadInfo); nsCOMPtr newLoadInfo = aNewChannel->LoadInfo(); MOZ_ASSERT(newLoadInfo); ExtContentPolicyType contentType = oldLoadInfo->GetExternalContentPolicyType(); if (contentType != ExtContentPolicy::TYPE_DOCUMENT || !aOldChannel->IsDocument()) { LOG_SPEC(("Ignoring redirect for %s because it's not a document", _spec), aOldURI); // We care about document redirects only. return; } nsCOMPtr cookieJarSettings; rv = oldLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't get the cookieJarSettings")); return; } int32_t behavior = cookieJarSettings->GetCookieBehavior(); if (behavior != nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { LOG( ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out " "early", behavior)); return; } nsIScriptSecurityManager* ssm = nsScriptSecurityManager::GetScriptSecurityManager(); MOZ_ASSERT(ssm); nsCOMPtr oldPrincipal; const nsTArray>& chain = oldLoadInfo->RedirectChain(); if (!chain.IsEmpty()) { rv = chain[0]->GetPrincipal(getter_AddRefs(oldPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't obtain the principal from the redirect chain")); return; } } else { rv = ssm->GetChannelResultPrincipal(aOldChannel, getter_AddRefs(oldPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't obtain the principal from the old channel")); return; } } nsCOMPtr newPrincipal; rv = ssm->GetChannelResultPrincipal(aNewChannel, getter_AddRefs(newPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't obtain the principal from the new channel")); return; } if (oldPrincipal->Equals(newPrincipal)) { LOG(("No permission needed for same principals.")); return; } if (!ShouldRedirectHeuristicApplyTrackingResource(aOldChannel, aOldURI, aNewChannel, aNewURI)) { LOG_SPEC2(("Ignoring redirect for %s to %s because tracking test failed", _spec1, _spec2), aOldURI, aNewURI); return; } if (!ContentBlockingUserInteraction::Exists(oldPrincipal) || !ContentBlockingUserInteraction::Exists(newPrincipal)) { LOG_SPEC2(("Ignoring redirect for %s to %s because no user-interaction on " "both pages", _spec1, _spec2), aOldURI, aNewURI); return; } nsAutoCString oldOrigin; rv = oldPrincipal->GetOriginNoSuffix(oldOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't get the origin from the old Principal")); return; } nsAutoCString newOrigin; rv = newPrincipal->GetOriginNoSuffix(newOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Can't get the origin from the new Principal")); return; } if (!HasEligibleVisit(aOldURI) || !HasEligibleVisit(aNewURI)) { LOG(("No previous visit record, bailing out early.")); return; } // Check if the new principal is a third party principal bool aResult; rv = newPrincipal->IsThirdPartyPrincipal(oldPrincipal, &aResult); if (NS_WARN_IF(NS_FAILED(rv))) { LOG(("Error while checking if new principal is third party")); return; } if (!aResult) { LOG(("New principal is a first party principal")); return; } LOG(("Adding a first-party storage exception for %s...", PromiseFlatCString(newOrigin).get())); LOG(("Saving the permission: oldOrigin=%s, grantedOrigin=%s", oldOrigin.get(), newOrigin.get())); AddConsoleReport(aNewChannel, aNewURI, oldOrigin, newOrigin); Telemetry::AccumulateCategorical( Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageGranted); Telemetry::AccumulateCategorical( Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::Redirect); // We don't care about this promise because the operation is actually sync. RefPtr promise = StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess( newPrincipal, oldPrincipal, StorageAccessAPIHelper::StorageAccessPromptChoices::eAllow, false, StaticPrefs::privacy_restrict3rdpartystorage_expiration_visited()); Unused << promise; } } // namespace mozilla