1486 lines
59 KiB
C++
1486 lines
59 KiB
C++
/* -*- 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 "StorageAccessAPIHelper.h"
|
|
#include "AntiTrackingUtils.h"
|
|
#include "TemporaryAccessGrantObserver.h"
|
|
|
|
#include "mozilla/BounceTrackingProtection.h"
|
|
#include "mozilla/ClearOnShutdown.h"
|
|
#include "mozilla/Components.h"
|
|
#include "mozilla/ContentBlockingAllowList.h"
|
|
#include "mozilla/ContentBlockingUserInteraction.h"
|
|
#include "mozilla/dom/BindingDeclarations.h"
|
|
#include "mozilla/dom/BrowsingContext.h"
|
|
#include "mozilla/dom/BrowsingContextGroup.h"
|
|
#include "mozilla/dom/ContentChild.h"
|
|
#include "mozilla/dom/ContentParent.h"
|
|
#include "mozilla/dom/Document.h"
|
|
#include "mozilla/dom/FeaturePolicy.h"
|
|
#include "mozilla/dom/WindowContext.h"
|
|
#include "mozilla/dom/WindowGlobalParent.h"
|
|
#include "mozilla/net/CookieJarSettings.h"
|
|
#include "mozilla/net/UrlClassifierFeatureFactory.h"
|
|
#include "mozilla/PermissionManager.h"
|
|
#include "mozilla/StaticPrefs_network.h"
|
|
#include "mozilla/StaticPrefs_privacy.h"
|
|
#include "mozilla/glean/AntitrackingMetrics.h"
|
|
#include "mozIThirdPartyUtil.h"
|
|
#include "nsContentUtils.h"
|
|
#include "nsIClassifiedChannel.h"
|
|
#include "nsICookiePermission.h"
|
|
#include "nsICookieService.h"
|
|
#include "nsIPermission.h"
|
|
#include "nsIPrincipal.h"
|
|
#include "nsIURI.h"
|
|
#include "nsIURIClassifier.h"
|
|
#include "nsIUrlClassifierFeature.h"
|
|
#include "nsIOService.h"
|
|
#include "nsIWebProgressListener.h"
|
|
#include "nsScriptSecurityManager.h"
|
|
#include "StorageAccess.h"
|
|
#include "nsStringFwd.h"
|
|
|
|
namespace mozilla {
|
|
|
|
LazyLogModule gAntiTrackingLog("AntiTracking");
|
|
|
|
}
|
|
|
|
using namespace mozilla;
|
|
using mozilla::dom::BrowsingContext;
|
|
using mozilla::dom::ContentChild;
|
|
using mozilla::dom::Document;
|
|
using mozilla::dom::WindowGlobalParent;
|
|
using mozilla::net::CookieJarSettings;
|
|
|
|
namespace {
|
|
|
|
bool GetTopLevelWindowId(BrowsingContext* aParentContext, uint32_t aBehavior,
|
|
uint64_t& aTopLevelInnerWindowId) {
|
|
MOZ_ASSERT(aParentContext);
|
|
|
|
aTopLevelInnerWindowId =
|
|
(aBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER)
|
|
? AntiTrackingUtils::GetTopLevelStorageAreaWindowId(aParentContext)
|
|
: AntiTrackingUtils::GetTopLevelAntiTrackingWindowId(aParentContext);
|
|
return aTopLevelInnerWindowId != 0;
|
|
}
|
|
|
|
// The list of feature names for trackers that have been annotated.
|
|
static StaticAutoPtr<nsTArray<nsCString>> sUrlClassifierFeatureNamesForTrackers;
|
|
|
|
// The list of features for trackers that have been annotated.
|
|
static StaticAutoPtr<nsTArray<RefPtr<nsIUrlClassifierFeature>>>
|
|
sUrlClassifierFeaturesForTracker;
|
|
|
|
const nsTArray<nsCString>& GetClassifierFeatureNamesForTrackers() {
|
|
if (!sUrlClassifierFeatureNamesForTrackers) {
|
|
sUrlClassifierFeatureNamesForTrackers = new nsTArray<nsCString>({
|
|
"emailtracking-protection"_ns,
|
|
"fingerprinting-annotation"_ns,
|
|
"socialtracking-annotation"_ns,
|
|
"tracking-annotation"_ns,
|
|
});
|
|
|
|
RunOnShutdown([] {
|
|
sUrlClassifierFeatureNamesForTrackers->Clear();
|
|
sUrlClassifierFeatureNamesForTrackers = nullptr;
|
|
});
|
|
}
|
|
return *sUrlClassifierFeatureNamesForTrackers;
|
|
}
|
|
|
|
const nsTArray<RefPtr<nsIUrlClassifierFeature>>&
|
|
GetClassifierFeaturesForTrackers() {
|
|
if (!sUrlClassifierFeaturesForTracker) {
|
|
sUrlClassifierFeaturesForTracker =
|
|
new nsTArray<RefPtr<nsIUrlClassifierFeature>>();
|
|
|
|
// Construct the list of classifier features.
|
|
for (const nsCString& featureName :
|
|
GetClassifierFeatureNamesForTrackers()) {
|
|
nsCOMPtr<nsIUrlClassifierFeature> feature =
|
|
net::UrlClassifierFeatureFactory::GetFeatureByName(featureName);
|
|
if (NS_WARN_IF(!feature)) {
|
|
continue;
|
|
}
|
|
sUrlClassifierFeaturesForTracker->AppendElement(feature);
|
|
}
|
|
MOZ_ASSERT(!sUrlClassifierFeaturesForTracker->IsEmpty(),
|
|
"At least one URL classifier feature must be present");
|
|
RunOnShutdown([] {
|
|
sUrlClassifierFeaturesForTracker->Clear();
|
|
sUrlClassifierFeaturesForTracker = nullptr;
|
|
});
|
|
}
|
|
return *sUrlClassifierFeaturesForTracker;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
|
|
StorageAccessAPIHelper::AllowAccessForHelper(
|
|
nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext,
|
|
ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason,
|
|
nsCOMPtr<nsIPrincipal>* aTrackingPrincipal, nsACString& aTrackingOrigin,
|
|
uint64_t* aTopLevelWindowId, uint32_t* aBehavior) {
|
|
MOZ_ASSERT(aParentContext);
|
|
MOZ_ASSERT(aTrackingPrincipal);
|
|
MOZ_ASSERT(aTopLevelWindowId);
|
|
MOZ_ASSERT(aBehavior);
|
|
|
|
switch (aReason) {
|
|
case ContentBlockingNotifier::eOpener:
|
|
if (!StaticPrefs::privacy_antitracking_enableWebcompat() ||
|
|
!StaticPrefs::
|
|
privacy_restrict3rdpartystorage_heuristic_window_open()) {
|
|
LOG(
|
|
("Bailing out early because the window open heuristic is disabled "
|
|
"by pref"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
break;
|
|
case ContentBlockingNotifier::eOpenerAfterUserInteraction:
|
|
if (!StaticPrefs::privacy_antitracking_enableWebcompat() ||
|
|
!StaticPrefs::
|
|
privacy_restrict3rdpartystorage_heuristic_opened_window_after_interaction()) {
|
|
LOG(
|
|
("Bailing out early because the window open after interaction "
|
|
"heuristic is disabled by pref"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) {
|
|
nsAutoCString origin;
|
|
aPrincipal->GetOriginNoSuffix(origin);
|
|
LOG(("Adding a first-party storage exception for %s, triggered by %s",
|
|
PromiseFlatCString(origin).get(),
|
|
AntiTrackingUtils::GrantedReasonToString(aReason).get()));
|
|
}
|
|
|
|
RefPtr<dom::WindowContext> parentWindowContext =
|
|
aParentContext->GetCurrentWindowContext();
|
|
if (!parentWindowContext) {
|
|
LOG(
|
|
("No window context found for our parent browsing context, bailing out "
|
|
"early"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
if (parentWindowContext->GetCookieBehavior().isNothing()) {
|
|
LOG(
|
|
("No cookie behaviour found for our parent window context, bailing "
|
|
"out early"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
// Only add storage permission when there is a reason to do so.
|
|
*aBehavior = *parentWindowContext->GetCookieBehavior();
|
|
if (!CookieJarSettings::IsRejectThirdPartyContexts(*aBehavior)) {
|
|
LOG(
|
|
("Disabled by network.cookie.cookieBehavior pref (%d), bailing out "
|
|
"early",
|
|
*aBehavior));
|
|
return StorageAccessPermissionGrantPromise::CreateAndResolve(true,
|
|
__func__);
|
|
}
|
|
|
|
MOZ_ASSERT(
|
|
*aBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER ||
|
|
*aBehavior ==
|
|
nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN);
|
|
|
|
// No need to continue when we are already in the allow list.
|
|
if (parentWindowContext->GetIsOnContentBlockingAllowList()) {
|
|
return StorageAccessPermissionGrantPromise::CreateAndResolve(true,
|
|
__func__);
|
|
}
|
|
|
|
// Make sure storage access isn't disabled
|
|
if (!aParentContext->IsTopContent() &&
|
|
Document::StorageAccessSandboxed(aParentContext->GetSandboxFlags())) {
|
|
LOG(("Our document is sandboxed"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
bool isParentThirdParty = parentWindowContext->GetIsThirdPartyWindow();
|
|
|
|
LOG(("The current resource is %s-party",
|
|
isParentThirdParty ? "third" : "first"));
|
|
|
|
// We are a first party resource.
|
|
if (!isParentThirdParty) {
|
|
nsAutoCString origin;
|
|
nsresult rv = aPrincipal->GetOriginNoSuffix(origin);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
LOG(("Can't get the origin from the URI"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
aTrackingOrigin = origin;
|
|
*aTrackingPrincipal = aPrincipal;
|
|
*aTopLevelWindowId = aParentContext->GetCurrentInnerWindowId();
|
|
if (NS_WARN_IF(!*aTopLevelWindowId)) {
|
|
LOG(("Top-level storage area window id not found, bailing out early"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
} else {
|
|
// We should be a 3rd party source.
|
|
if (*aBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER &&
|
|
!parentWindowContext->GetIsThirdPartyTrackingResourceWindow()) {
|
|
LOG(("Our window isn't a third-party tracking window"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
if (*aBehavior ==
|
|
nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
|
|
!isParentThirdParty) {
|
|
LOG(("Our window isn't a third-party window"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
if (!GetTopLevelWindowId(aParentContext,
|
|
// Don't request the ETP specific behaviour of
|
|
// allowing only singly-nested iframes here,
|
|
// because we are recording an allow permission.
|
|
nsICookieService::BEHAVIOR_ACCEPT,
|
|
*aTopLevelWindowId)) {
|
|
LOG(("Error while retrieving the parent window id, bailing out early"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
// If we can't get the principal and tracking origin at this point, the
|
|
// tracking principal will be gotten while running ::CompleteAllowAccessFor
|
|
// in the parent.
|
|
if (aParentContext->IsInProcess()) {
|
|
if (!AntiTrackingUtils::GetPrincipalAndTrackingOrigin(
|
|
aParentContext, getter_AddRefs(*aTrackingPrincipal),
|
|
aTrackingOrigin)) {
|
|
LOG(
|
|
("Error while computing the parent principal and tracking origin, "
|
|
"bailing out early"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The only case that aParentContext is not in-process is when the heuristic
|
|
// is triggered because of user interactions.
|
|
MOZ_ASSERT_IF(
|
|
!aParentContext->IsInProcess(),
|
|
aReason == ContentBlockingNotifier::eOpenerAfterUserInteraction);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// We MAY need information that is only accessible in the parent,
|
|
// so we need to determine whether we can run it in the current process (in
|
|
// most of cases it should be a child process).
|
|
//
|
|
// We will follow below algorithm to decide if we can continue to run in
|
|
// the current process, otherwise, we need to ask the parent to continue
|
|
// the work.
|
|
// 1. Check if aParentContext is an in-process browsing context. If it isn't,
|
|
// we cannot proceed in the content process because we need the
|
|
// principal of the parent window. Otherwise, we go to step 2.
|
|
// 2. Check if the grant reason is ePrivilegeStorageAccessForOriginAPI. In
|
|
// this case, we don't need to check the user interaction of the tracking
|
|
// origin. So, we can proceed in the content process. Otherwise, go to
|
|
// step 3.
|
|
// 2. tracking origin is not third-party with respect to the parent window
|
|
// (aParentContext). This is because we need to test whether the user
|
|
// has interacted with the tracking origin before, and this info is
|
|
// not supposed to be seen from cross-origin processes.
|
|
|
|
/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
|
|
StorageAccessAPIHelper::AllowAccessForOnParentProcess(
|
|
nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext,
|
|
ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason,
|
|
const StorageAccessAPIHelper::PerformPermissionGrant& aPerformFinalChecks) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
MOZ_ASSERT(aParentContext);
|
|
|
|
uint32_t behavior;
|
|
uint64_t topLevelWindowId;
|
|
nsCOMPtr<nsIPrincipal> trackingPrincipal;
|
|
nsAutoCString trackingOrigin;
|
|
|
|
RefPtr<StorageAccessPermissionGrantPromise> returnPromise =
|
|
AllowAccessForHelper(aPrincipal, aParentContext, aReason,
|
|
&trackingPrincipal, trackingOrigin,
|
|
&topLevelWindowId, &behavior);
|
|
if (returnPromise) {
|
|
return returnPromise;
|
|
}
|
|
|
|
return StorageAccessAPIHelper::CompleteAllowAccessForOnParentProcess(
|
|
aParentContext, topLevelWindowId, trackingPrincipal, trackingOrigin,
|
|
behavior, aReason, aPerformFinalChecks);
|
|
}
|
|
|
|
/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
|
|
StorageAccessAPIHelper::AllowAccessForOnChildProcess(
|
|
nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext,
|
|
ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason,
|
|
const StorageAccessAPIHelper::PerformPermissionGrant& aPerformFinalChecks) {
|
|
MOZ_ASSERT(XRE_IsContentProcess());
|
|
MOZ_ASSERT(aParentContext);
|
|
|
|
uint32_t behavior;
|
|
uint64_t topLevelWindowId;
|
|
nsCOMPtr<nsIPrincipal> trackingPrincipal;
|
|
nsAutoCString trackingOrigin;
|
|
|
|
RefPtr<StorageAccessPermissionGrantPromise> returnPromise =
|
|
AllowAccessForHelper(aPrincipal, aParentContext, aReason,
|
|
&trackingPrincipal, trackingOrigin,
|
|
&topLevelWindowId, &behavior);
|
|
if (returnPromise) {
|
|
return returnPromise;
|
|
}
|
|
|
|
// We should run in the parent process when the tracking origin is
|
|
// third-party with respect to it's parent window. This is because we can't
|
|
// test if the user has interacted with the third-party origin in the child
|
|
// process.
|
|
if (aParentContext->IsInProcess()) {
|
|
bool isThirdParty;
|
|
nsCOMPtr<nsIPrincipal> principal =
|
|
AntiTrackingUtils::GetPrincipal(aParentContext);
|
|
if (!principal) {
|
|
LOG(("Can't get the principal from the browsing context"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
Unused << trackingPrincipal->IsThirdPartyPrincipal(principal,
|
|
&isThirdParty);
|
|
if (aReason ==
|
|
ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI ||
|
|
!isThirdParty) {
|
|
return StorageAccessAPIHelper::CompleteAllowAccessForOnChildProcess(
|
|
aParentContext, topLevelWindowId, trackingPrincipal, trackingOrigin,
|
|
behavior, aReason, aPerformFinalChecks);
|
|
}
|
|
}
|
|
|
|
// Only support PerformPermissionGrant when we run ::CompleteAllowAccessFor in
|
|
// the same process. This callback is only used by eStorageAccessAPI,
|
|
// which is always runned in the same process.
|
|
MOZ_ASSERT(!aPerformFinalChecks);
|
|
|
|
ContentChild* cc = ContentChild::GetSingleton();
|
|
MOZ_ASSERT(cc);
|
|
|
|
RefPtr<BrowsingContext> bc = aParentContext;
|
|
return cc
|
|
->SendCompleteAllowAccessFor(aParentContext, topLevelWindowId,
|
|
trackingPrincipal, trackingOrigin, behavior,
|
|
aReason)
|
|
->Then(GetCurrentSerialEventTarget(), __func__,
|
|
[bc, trackingOrigin, behavior,
|
|
aReason](const ContentChild::CompleteAllowAccessForPromise::
|
|
ResolveOrRejectValue& aValue) {
|
|
if (aValue.IsResolve() && aValue.ResolveValue().isSome()) {
|
|
// we don't call OnAllowAccessFor in the parent when this is
|
|
// triggered by the opener heuristic, so we have to do it here.
|
|
// See storePermission below for the reason.
|
|
if (aReason == ContentBlockingNotifier::eOpener &&
|
|
!bc->IsDiscarded()) {
|
|
MOZ_ASSERT(bc->IsInProcess());
|
|
StorageAccessAPIHelper::OnAllowAccessFor(bc, trackingOrigin,
|
|
behavior, aReason);
|
|
}
|
|
return StorageAccessPermissionGrantPromise::CreateAndResolve(
|
|
aValue.ResolveValue().value(), __func__);
|
|
}
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(
|
|
false, __func__);
|
|
});
|
|
}
|
|
|
|
// CompleteAllowAccessFor is used to process the remaining work in
|
|
// AllowAccessFor that may need to access information not accessible
|
|
// in the current process.
|
|
// This API supports running running in the child process and the
|
|
// parent process. When running in the child, aParentContext must be in-process.
|
|
//
|
|
// Here lists the possible cases based on our heuristics:
|
|
// 1. eStorageAccessAPI
|
|
// aParentContext is the browsing context of the document that calls this
|
|
// API, so it is always in-process. Since the tracking origin is the
|
|
// document's origin, it's same-origin to the parent window.
|
|
// CompleteAllowAccessFor runs in the same process as AllowAccessFor.
|
|
//
|
|
// 2. eOpener
|
|
// aParentContext is the browsing context of the opener that calls this
|
|
// API, so it is always in-process. However, when the opener is a first
|
|
// party and it opens a third-party window, the tracking origin is
|
|
// origin of the third-party window. In this case, we should
|
|
// run this API in the parent, as for the other cases, we can run in the
|
|
// same process.
|
|
//
|
|
// 3. eOpenerAfterUserInteraction
|
|
// aParentContext is the browsing context of the opener window, but
|
|
// AllowAccessFor is called by the opened window. So as long as
|
|
// aParentContext is not in-process, we should run in the parent.
|
|
//
|
|
// 4. ePrivilegeStorageAccessForOriginAPI
|
|
// aParentContext is the browsing context of the top window which calls the
|
|
// privilege API. So, it is always in-process. And we don't need to check the
|
|
// user interaction permission for the tracking origin in this case. We can
|
|
// run in the same process.
|
|
/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
|
|
StorageAccessAPIHelper::CompleteAllowAccessForOnParentProcess(
|
|
dom::BrowsingContext* aParentContext, uint64_t aTopLevelWindowId,
|
|
nsIPrincipal* aTrackingPrincipal, const nsACString& aTrackingOrigin,
|
|
uint32_t aCookieBehavior,
|
|
ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason,
|
|
const PerformPermissionGrant& aPerformFinalChecks) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
MOZ_ASSERT(aParentContext);
|
|
|
|
nsCOMPtr<nsIPrincipal> trackingPrincipal;
|
|
nsAutoCString trackingOrigin;
|
|
if (!aTrackingPrincipal) {
|
|
// User interaction is the only case that tracking principal is not
|
|
// available.
|
|
MOZ_ASSERT(aReason == ContentBlockingNotifier::eOpenerAfterUserInteraction);
|
|
|
|
if (!AntiTrackingUtils::GetPrincipalAndTrackingOrigin(
|
|
aParentContext, getter_AddRefs(trackingPrincipal),
|
|
trackingOrigin)) {
|
|
LOG(
|
|
("Error while computing the parent principal and tracking origin, "
|
|
"bailing out early"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
} else {
|
|
trackingPrincipal = aTrackingPrincipal;
|
|
trackingOrigin = aTrackingOrigin;
|
|
}
|
|
|
|
LOG(("Tracking origin is %s", PromiseFlatCString(trackingOrigin).get()));
|
|
|
|
// We hardcode this block reason since the first-party storage access
|
|
// permission is granted for the purpose of blocking trackers.
|
|
// Note that if aReason is eOpenerAfterUserInteraction and the
|
|
// trackingPrincipal is not in a blocklist, we don't check the
|
|
// user-interaction state, because it could be that the current process has
|
|
// just sent the request to store the user-interaction permission into the
|
|
// parent, without having received the permission itself yet.
|
|
//
|
|
// For ePrivilegeStorageAccessForOriginAPI, we explicitly don't check the user
|
|
// interaction for the tracking origin.
|
|
|
|
bool isInPrefList = false;
|
|
trackingPrincipal->IsURIInPrefList(
|
|
"privacy.restrict3rdpartystorage."
|
|
"userInteractionRequiredForHosts",
|
|
&isInPrefList);
|
|
if (aReason != ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI &&
|
|
isInPrefList &&
|
|
!ContentBlockingUserInteraction::Exists(trackingPrincipal)) {
|
|
LOG_PRIN(("Tracking principal (%s) hasn't been interacted with before, "
|
|
"refusing to add a first-party storage permission to access it",
|
|
_spec),
|
|
trackingPrincipal);
|
|
ContentBlockingNotifier::OnDecision(
|
|
aParentContext, ContentBlockingNotifier::BlockingDecision::eBlock,
|
|
nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER);
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
// Ensure we can find the window before continuing, so we can safely
|
|
// execute storePermission.
|
|
if (aParentContext->IsInProcess() &&
|
|
(!aParentContext->GetDOMWindow() ||
|
|
!aParentContext->GetDOMWindow()->GetCurrentInnerWindow())) {
|
|
LOG(
|
|
("No window found for our parent browsing context, bailing out "
|
|
"early"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
auto storePermission =
|
|
[aParentContext, aTopLevelWindowId, trackingOrigin, trackingPrincipal,
|
|
aCookieBehavior,
|
|
aReason](int aAllowMode) -> RefPtr<StorageAccessPermissionGrantPromise> {
|
|
// We don't have the window, send an IPC to the content process that
|
|
// owns the parent window. But there is a special case, for window.open,
|
|
// we'll return to the content process we need to inform when this
|
|
// function is done. So we don't need to create an extra IPC for the case.
|
|
if (aReason != ContentBlockingNotifier::eOpener) {
|
|
dom::ContentParent* cp = aParentContext->Canonical()->GetContentParent();
|
|
if (!cp) {
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
Unused << cp->SendOnAllowAccessFor(aParentContext, trackingOrigin,
|
|
aCookieBehavior, aReason);
|
|
}
|
|
|
|
Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>
|
|
reportReason;
|
|
// We can directly report here if we can know the origin of the top.
|
|
ContentBlockingNotifier::ReportUnblockingToConsole(
|
|
aParentContext, NS_ConvertUTF8toUTF16(trackingOrigin), aReason);
|
|
// Set the report reason to nothing if we've already reported.
|
|
reportReason = Nothing();
|
|
|
|
LOG(("Saving the permission: trackingOrigin=%s", trackingOrigin.get()));
|
|
bool frameOnly = StaticPrefs::dom_storage_access_frame_only() &&
|
|
aReason == ContentBlockingNotifier::eStorageAccessAPI;
|
|
|
|
uint64_t innerWindowId = aParentContext->GetCurrentInnerWindowId();
|
|
|
|
return SaveAccessForOriginOnParentProcess(aTopLevelWindowId, aParentContext,
|
|
trackingPrincipal, aAllowMode,
|
|
frameOnly)
|
|
->Then(GetCurrentSerialEventTarget(), __func__,
|
|
[aReason, trackingPrincipal, innerWindowId](
|
|
ParentAccessGrantPromise::ResolveOrRejectValue&& aValue) {
|
|
if (!aValue.IsResolve()) {
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(
|
|
false, __func__);
|
|
}
|
|
// We only wish to observe user interaction in the case of a
|
|
// "normal" requestStorageAccess grant. We do not observe user
|
|
// interaction where the priveledged API is used. Acquiring
|
|
// the storageAccessAPI permission for the first time will only
|
|
// occur through the clicking accept on the doorhanger.
|
|
if (aReason == ContentBlockingNotifier::eStorageAccessAPI) {
|
|
ContentBlockingUserInteraction::Observe(trackingPrincipal);
|
|
RefPtr<dom::WindowContext> windowContext =
|
|
dom::WindowContext::GetById(innerWindowId);
|
|
if (windowContext) {
|
|
Unused << BounceTrackingProtection::RecordUserActivation(
|
|
windowContext);
|
|
}
|
|
}
|
|
return StorageAccessPermissionGrantPromise::CreateAndResolve(
|
|
StorageAccessAPIHelper::eAllow, __func__);
|
|
});
|
|
};
|
|
|
|
// Exclude trackers from opener heuristic if the pref is enabled.
|
|
if (aReason == ContentBlockingNotifier::eOpener &&
|
|
StaticPrefs::
|
|
privacy_restrict3rdpartystorage_heuristic_exclude_third_party_trackers()) {
|
|
return PerformTrackerCheck(trackingOrigin)
|
|
->Then(GetCurrentSerialEventTarget(), __func__,
|
|
[storePermission](
|
|
StorageAccessPermissionGrantPromise::ResolveOrRejectValue&&
|
|
aValue) {
|
|
if (aValue.IsResolve()) {
|
|
return storePermission(aValue.ResolveValue());
|
|
}
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(
|
|
false, __func__);
|
|
});
|
|
}
|
|
|
|
if (aPerformFinalChecks) {
|
|
return aPerformFinalChecks()->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[storePermission](
|
|
StorageAccessPermissionGrantPromise::ResolveOrRejectValue&&
|
|
aValue) {
|
|
if (aValue.IsResolve()) {
|
|
return storePermission(aValue.ResolveValue());
|
|
}
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
});
|
|
}
|
|
return storePermission(false);
|
|
}
|
|
|
|
// A helper class to handle the callback of the URL classifier feature check.
|
|
class CheckTrackerCallback final : public nsIUrlClassifierFeatureCallback {
|
|
public:
|
|
NS_DECL_ISUPPORTS
|
|
|
|
NS_IMETHOD
|
|
OnClassifyComplete(const nsTArray<RefPtr<nsIUrlClassifierFeatureResult>>&
|
|
aResults) override {
|
|
// If the result shows no tracker, we can resolve the promise.
|
|
if (aResults.IsEmpty()) {
|
|
mPromiseHolder.ResolveIfExists(true, __func__);
|
|
return NS_OK;
|
|
}
|
|
|
|
// Otherwise, we reject the promise.
|
|
mPromiseHolder.RejectIfExists(false, __func__);
|
|
return NS_OK;
|
|
}
|
|
|
|
RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
|
|
Promise() {
|
|
return mPromiseHolder.Ensure(__func__);
|
|
}
|
|
|
|
private:
|
|
~CheckTrackerCallback() = default;
|
|
|
|
MozPromiseHolder<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
|
|
mPromiseHolder;
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(CheckTrackerCallback, nsIUrlClassifierFeatureCallback);
|
|
|
|
RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
|
|
StorageAccessAPIHelper::PerformTrackerCheck(const nsACString& aTrackingOrigin) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
|
|
nsresult rv;
|
|
nsCOMPtr<nsIURIClassifier> uriClassifier =
|
|
do_GetService(NS_URICLASSIFIERSERVICE_CONTRACTID, &rv);
|
|
|
|
if (NS_FAILED(rv)) {
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
nsCOMPtr<nsIURI> trackingURI;
|
|
rv = NS_NewURI(getter_AddRefs(trackingURI), aTrackingOrigin);
|
|
if (NS_FAILED(rv)) {
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
auto callback = MakeRefPtr<CheckTrackerCallback>();
|
|
|
|
rv = uriClassifier->AsyncClassifyLocalWithFeatures(
|
|
trackingURI, GetClassifierFeaturesForTrackers(),
|
|
nsIUrlClassifierFeature::blocklist, callback, false);
|
|
if (NS_FAILED(rv)) {
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
return callback->Promise();
|
|
}
|
|
|
|
/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
|
|
StorageAccessAPIHelper::CompleteAllowAccessForOnChildProcess(
|
|
dom::BrowsingContext* aParentContext, uint64_t aTopLevelWindowId,
|
|
nsIPrincipal* aTrackingPrincipal, const nsACString& aTrackingOrigin,
|
|
uint32_t aCookieBehavior,
|
|
ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason,
|
|
const PerformPermissionGrant& aPerformFinalChecks) {
|
|
MOZ_ASSERT_IF(XRE_IsContentProcess(), aParentContext->IsInProcess());
|
|
MOZ_ASSERT(XRE_IsContentProcess());
|
|
MOZ_ASSERT(aParentContext);
|
|
MOZ_ASSERT(aTrackingPrincipal);
|
|
|
|
nsCOMPtr<nsIPrincipal> trackingPrincipal;
|
|
nsAutoCString trackingOrigin;
|
|
trackingOrigin = aTrackingOrigin;
|
|
trackingPrincipal = aTrackingPrincipal;
|
|
|
|
LOG(("Tracking origin is %s", PromiseFlatCString(trackingOrigin).get()));
|
|
|
|
// We hardcode this block reason since the first-party storage access
|
|
// permission is granted for the purpose of blocking trackers.
|
|
// Note that if aReason is eOpenerAfterUserInteraction and the
|
|
// trackingPrincipal is not in a blocklist, we don't check the
|
|
// user-interaction state, because it could be that the current process has
|
|
// just sent the request to store the user-interaction permission into the
|
|
// parent, without having received the permission itself yet.
|
|
//
|
|
// For ePrivilegeStorageAccessForOriginAPI, we explicitly don't check the user
|
|
// interaction for the tracking origin.
|
|
|
|
bool isInPrefList = false;
|
|
aTrackingPrincipal->IsURIInPrefList(
|
|
"privacy.restrict3rdpartystorage."
|
|
"userInteractionRequiredForHosts",
|
|
&isInPrefList);
|
|
if (aReason != ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI &&
|
|
isInPrefList &&
|
|
!ContentBlockingUserInteraction::Exists(aTrackingPrincipal)) {
|
|
LOG_PRIN(("Tracking principal (%s) hasn't been interacted with before, "
|
|
"refusing to add a first-party storage permission to access it",
|
|
_spec),
|
|
aTrackingPrincipal);
|
|
ContentBlockingNotifier::OnDecision(
|
|
aParentContext, ContentBlockingNotifier::BlockingDecision::eBlock,
|
|
nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER);
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
// Ensure we can find the window before continuing, so we can safely
|
|
// execute storePermission.
|
|
if (aParentContext->IsInProcess() &&
|
|
(!aParentContext->GetDOMWindow() ||
|
|
!aParentContext->GetDOMWindow()->GetCurrentInnerWindow())) {
|
|
LOG(
|
|
("No window found for our parent browsing context, bailing out "
|
|
"early"));
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
}
|
|
|
|
auto storePermission =
|
|
[aParentContext, aTopLevelWindowId, trackingOrigin, trackingPrincipal,
|
|
aCookieBehavior,
|
|
aReason](int aAllowMode) -> RefPtr<StorageAccessPermissionGrantPromise> {
|
|
// Inform the window we granted permission for. This has to be done in the
|
|
// window's process. As a child this is always the case.
|
|
StorageAccessAPIHelper::OnAllowAccessFor(aParentContext, trackingOrigin,
|
|
aCookieBehavior, aReason);
|
|
|
|
Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>
|
|
reportReason;
|
|
// We can directly report here if we can know the origin of the top.
|
|
if (aParentContext->Top()->IsInProcess()) {
|
|
ContentBlockingNotifier::ReportUnblockingToConsole(
|
|
aParentContext, NS_ConvertUTF8toUTF16(trackingOrigin), aReason);
|
|
|
|
// Set the report reason to nothing if we've already reported.
|
|
reportReason = Nothing();
|
|
} else {
|
|
// Set the report reason, so that we can know the reason when reporting
|
|
// in the parent.
|
|
reportReason.emplace(aReason);
|
|
}
|
|
|
|
ContentChild* cc = ContentChild::GetSingleton();
|
|
MOZ_ASSERT(cc);
|
|
|
|
LOG(
|
|
("Asking the parent process to save the permission for us: "
|
|
"trackingOrigin=%s",
|
|
trackingOrigin.get()));
|
|
|
|
// This is not really secure, because here we have the content process
|
|
// sending the request of storing a permission.
|
|
bool frameOnly = StaticPrefs::dom_storage_access_frame_only() &&
|
|
aReason == ContentBlockingNotifier::eStorageAccessAPI;
|
|
|
|
uint64_t innerWindowId = aParentContext->GetCurrentInnerWindowId();
|
|
|
|
return cc
|
|
->SendStorageAccessPermissionGrantedForOrigin(
|
|
aTopLevelWindowId, aParentContext, trackingPrincipal,
|
|
trackingOrigin, aAllowMode, reportReason, frameOnly)
|
|
->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[aReason, trackingPrincipal,
|
|
innerWindowId](const ContentChild::
|
|
StorageAccessPermissionGrantedForOriginPromise::
|
|
ResolveOrRejectValue& aValue) {
|
|
if (aValue.IsResolve()) {
|
|
if (aValue.ResolveValue() &&
|
|
(aReason == ContentBlockingNotifier::eStorageAccessAPI)) {
|
|
ContentBlockingUserInteraction::Observe(trackingPrincipal);
|
|
RefPtr<dom::WindowContext> windowContext =
|
|
dom::WindowContext::GetById(innerWindowId);
|
|
if (windowContext) {
|
|
Unused << BounceTrackingProtection::RecordUserActivation(
|
|
windowContext);
|
|
}
|
|
}
|
|
return StorageAccessPermissionGrantPromise::CreateAndResolve(
|
|
aValue.ResolveValue(), __func__);
|
|
}
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(
|
|
false, __func__);
|
|
});
|
|
};
|
|
|
|
if (aPerformFinalChecks) {
|
|
return aPerformFinalChecks()->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[storePermission](
|
|
StorageAccessPermissionGrantPromise::ResolveOrRejectValue&&
|
|
aValue) {
|
|
if (aValue.IsResolve()) {
|
|
return storePermission(aValue.ResolveValue());
|
|
}
|
|
return StorageAccessPermissionGrantPromise::CreateAndReject(false,
|
|
__func__);
|
|
});
|
|
}
|
|
return storePermission(false);
|
|
}
|
|
|
|
/* static */ void StorageAccessAPIHelper::OnAllowAccessFor(
|
|
dom::BrowsingContext* aParentContext, const nsACString& aTrackingOrigin,
|
|
uint32_t aCookieBehavior,
|
|
ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason) {
|
|
MOZ_ASSERT(aParentContext->IsInProcess());
|
|
|
|
// Let's inform the parent window and the other windows having the
|
|
// same tracking origin about the storage permission is granted
|
|
// if it is not a frame-only permission grant which does not propogate.
|
|
if (aReason != ContentBlockingNotifier::StorageAccessPermissionGrantedReason::
|
|
eStorageAccessAPI ||
|
|
!StaticPrefs::dom_storage_access_frame_only()) {
|
|
StorageAccessAPIHelper::UpdateAllowAccessOnCurrentProcess(aParentContext,
|
|
aTrackingOrigin);
|
|
}
|
|
|
|
// Let's inform the parent window.
|
|
nsCOMPtr<nsPIDOMWindowInner> parentInner =
|
|
AntiTrackingUtils::GetInnerWindow(aParentContext);
|
|
if (NS_WARN_IF(!parentInner)) {
|
|
return;
|
|
}
|
|
|
|
Document* doc = parentInner->GetExtantDoc();
|
|
if (NS_WARN_IF(!doc)) {
|
|
return;
|
|
}
|
|
|
|
if (!doc->GetChannel()) {
|
|
return;
|
|
}
|
|
|
|
glean::contentblocking::storage_access_granted_count
|
|
.EnumGet(glean::contentblocking::StorageAccessGrantedCountLabel::
|
|
eStoragegranted)
|
|
.Add();
|
|
|
|
nsCOMPtr<nsIURI> trackingURI;
|
|
nsresult rv = NS_NewURI(getter_AddRefs(trackingURI), aTrackingOrigin);
|
|
if (NS_FAILED(rv)) {
|
|
trackingURI = nullptr;
|
|
}
|
|
|
|
switch (aReason) {
|
|
case ContentBlockingNotifier::StorageAccessPermissionGrantedReason::
|
|
eStorageAccessAPI:
|
|
glean::contentblocking::storage_access_granted_count
|
|
.EnumGet(glean::contentblocking::StorageAccessGrantedCountLabel::
|
|
eStorageaccessapi)
|
|
.Add();
|
|
if (trackingURI) {
|
|
StorageAccessGrantTelemetryClassification::MaybeReportTracker(
|
|
static_cast<uint16_t>(
|
|
glean::contentblocking::StorageAccessGrantedCountLabel::
|
|
eStorageaccessapiCt),
|
|
trackingURI);
|
|
}
|
|
break;
|
|
case ContentBlockingNotifier::StorageAccessPermissionGrantedReason::
|
|
eOpenerAfterUserInteraction:
|
|
glean::contentblocking::storage_access_granted_count
|
|
.EnumGet(glean::contentblocking::StorageAccessGrantedCountLabel::
|
|
eOpenerafterui)
|
|
.Add();
|
|
if (trackingURI) {
|
|
StorageAccessGrantTelemetryClassification::MaybeReportTracker(
|
|
static_cast<uint16_t>(
|
|
glean::contentblocking::StorageAccessGrantedCountLabel::
|
|
eOpenerafteruiCt),
|
|
trackingURI);
|
|
}
|
|
break;
|
|
case ContentBlockingNotifier::StorageAccessPermissionGrantedReason::eOpener:
|
|
glean::contentblocking::storage_access_granted_count
|
|
.EnumGet(
|
|
glean::contentblocking::StorageAccessGrantedCountLabel::eOpener)
|
|
.Add();
|
|
if (trackingURI) {
|
|
StorageAccessGrantTelemetryClassification::MaybeReportTracker(
|
|
static_cast<uint16_t>(
|
|
glean::contentblocking::StorageAccessGrantedCountLabel::
|
|
eOpenerCt),
|
|
trackingURI);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// Theoratically this can be done in the parent process. But right now,
|
|
// we need the channel while notifying content blocking events, and
|
|
// we don't have a trivial way to obtain the channel in the parent
|
|
// via BrowsingContext. So we just ask the child to do the work.
|
|
ContentBlockingNotifier::OnEvent(
|
|
doc->GetChannel(), false,
|
|
nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER, aTrackingOrigin,
|
|
Some(aReason));
|
|
}
|
|
|
|
/* static */
|
|
RefPtr<mozilla::StorageAccessAPIHelper::ParentAccessGrantPromise>
|
|
StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess(
|
|
uint64_t aTopLevelWindowId, BrowsingContext* aParentContext,
|
|
nsIPrincipal* aTrackingPrincipal, int aAllowMode, bool aFrameOnly,
|
|
uint64_t aExpirationTime) {
|
|
MOZ_ASSERT(aTopLevelWindowId != 0);
|
|
MOZ_ASSERT(aTrackingPrincipal);
|
|
|
|
if (!aTrackingPrincipal || aTrackingPrincipal->IsSystemPrincipal() ||
|
|
aTrackingPrincipal->GetIsNullPrincipal() ||
|
|
aTrackingPrincipal->GetIsExpandedPrincipal()) {
|
|
LOG(("aTrackingPrincipal is of invalid principal type"));
|
|
return ParentAccessGrantPromise::CreateAndReject(false, __func__);
|
|
}
|
|
|
|
nsAutoCString trackingOrigin;
|
|
nsresult rv = aTrackingPrincipal->GetOriginNoSuffix(trackingOrigin);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return ParentAccessGrantPromise::CreateAndReject(false, __func__);
|
|
}
|
|
|
|
RefPtr<WindowGlobalParent> wgp =
|
|
WindowGlobalParent::GetByInnerWindowId(aTopLevelWindowId);
|
|
if (!wgp) {
|
|
LOG(("Can't get window global parent"));
|
|
return ParentAccessGrantPromise::CreateAndReject(false, __func__);
|
|
}
|
|
|
|
// If the permission is granted on a first-party window, also have to update
|
|
// the permission to all the other windows with the same tracking origin (in
|
|
// the same tab), if any, only it is not a frame-only permission grant which
|
|
// does not propogate.
|
|
if (!aFrameOnly) {
|
|
StorageAccessAPIHelper::UpdateAllowAccessOnParentProcess(aParentContext,
|
|
trackingOrigin);
|
|
}
|
|
|
|
return StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess(
|
|
wgp->DocumentPrincipal(), aTrackingPrincipal, aAllowMode, aFrameOnly,
|
|
aExpirationTime);
|
|
}
|
|
|
|
/* static */
|
|
RefPtr<mozilla::StorageAccessAPIHelper::ParentAccessGrantPromise>
|
|
StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess(
|
|
nsIPrincipal* aParentPrincipal, nsIPrincipal* aTrackingPrincipal,
|
|
int aAllowMode, bool aFrameOnly, uint64_t aExpirationTime) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
MOZ_ASSERT(aAllowMode == eAllow || aAllowMode == eAllowAutoGrant);
|
|
|
|
if (!aParentPrincipal || !aTrackingPrincipal) {
|
|
LOG(("Invalid input arguments passed"));
|
|
return ParentAccessGrantPromise::CreateAndReject(false, __func__);
|
|
};
|
|
|
|
if (aTrackingPrincipal->IsSystemPrincipal() ||
|
|
aTrackingPrincipal->GetIsNullPrincipal() ||
|
|
aTrackingPrincipal->GetIsExpandedPrincipal()) {
|
|
LOG(("aTrackingPrincipal is of invalid principal type"));
|
|
return ParentAccessGrantPromise::CreateAndReject(false, __func__);
|
|
}
|
|
|
|
nsAutoCString trackingOrigin;
|
|
nsresult rv = aTrackingPrincipal->GetOriginNoSuffix(trackingOrigin);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return ParentAccessGrantPromise::CreateAndReject(false, __func__);
|
|
}
|
|
|
|
LOG_PRIN(("Saving a first-party storage permission on %s for "
|
|
"trackingOrigin=%s",
|
|
_spec, trackingOrigin.get()),
|
|
aParentPrincipal);
|
|
|
|
if (NS_WARN_IF(!aParentPrincipal)) {
|
|
// The child process is sending something wrong. Let's ignore it.
|
|
LOG(("aParentPrincipal is null, bailing out early"));
|
|
return ParentAccessGrantPromise::CreateAndReject(false, __func__);
|
|
}
|
|
|
|
RefPtr<PermissionManager> permManager = PermissionManager::GetInstance();
|
|
if (NS_WARN_IF(!permManager)) {
|
|
LOG(("Permission manager is null, bailing out early"));
|
|
return ParentAccessGrantPromise::CreateAndReject(false, __func__);
|
|
}
|
|
|
|
// Remember that this pref is stored in seconds!
|
|
uint32_t expirationType = nsIPermissionManager::EXPIRE_TIME;
|
|
uint32_t expirationTime = aExpirationTime * 1000;
|
|
int64_t when = (PR_Now() / PR_USEC_PER_MSEC) + expirationTime;
|
|
|
|
uint32_t privateBrowsingId = 0;
|
|
rv = aParentPrincipal->GetPrivateBrowsingId(&privateBrowsingId);
|
|
if ((!NS_WARN_IF(NS_FAILED(rv)) && privateBrowsingId > 0) ||
|
|
(aAllowMode == eAllowAutoGrant)) {
|
|
// If we are coming from a private window or are automatically granting a
|
|
// permission, make sure to store a session-only permission which won't
|
|
// get persisted to disk.
|
|
expirationType = nsIPermissionManager::EXPIRE_SESSION;
|
|
when = 0;
|
|
}
|
|
|
|
nsAutoCString type;
|
|
if (aFrameOnly) {
|
|
bool success = AntiTrackingUtils::CreateStorageFramePermissionKey(
|
|
aTrackingPrincipal, type);
|
|
if (NS_WARN_IF(!success)) {
|
|
return ParentAccessGrantPromise::CreateAndReject(false, __func__);
|
|
}
|
|
} else {
|
|
AntiTrackingUtils::CreateStoragePermissionKey(trackingOrigin, type);
|
|
}
|
|
|
|
LOG(
|
|
("Computed permission key: %s, expiry: %u, proceeding to save in the "
|
|
"permission manager",
|
|
type.get(), expirationTime));
|
|
|
|
rv = permManager->AddFromPrincipal(aParentPrincipal, type,
|
|
nsIPermissionManager::ALLOW_ACTION,
|
|
expirationType, when);
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
|
|
if (StaticPrefs::privacy_antitracking_testing()) {
|
|
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
|
|
obs->NotifyObservers(nullptr, "antitracking-test-storage-access-perm-added",
|
|
nullptr);
|
|
}
|
|
|
|
if (NS_SUCCEEDED(rv) && (aAllowMode == eAllowAutoGrant)) {
|
|
// Make sure temporary access grants do not survive more than 24 hours.
|
|
TemporaryAccessGrantObserver::Create(permManager, aParentPrincipal, type);
|
|
}
|
|
|
|
LOG(("Result: %s", NS_SUCCEEDED(rv) ? "success" : "failure"));
|
|
return ParentAccessGrantPromise::CreateAndResolve(rv, __func__);
|
|
}
|
|
|
|
// static
|
|
Maybe<bool>
|
|
StorageAccessAPIHelper::CheckCookiesPermittedDecidesStorageAccessAPI(
|
|
nsICookieJarSettings* aCookieJarSettings,
|
|
nsIPrincipal* aRequestingPrincipal) {
|
|
MOZ_ASSERT(aCookieJarSettings);
|
|
MOZ_ASSERT(aRequestingPrincipal);
|
|
uint32_t cookiePermission = detail::CheckCookiePermissionForPrincipal(
|
|
aCookieJarSettings, aRequestingPrincipal);
|
|
if (cookiePermission == nsICookiePermission::ACCESS_ALLOW ||
|
|
cookiePermission == nsICookiePermission::ACCESS_SESSION) {
|
|
return Some(true);
|
|
}
|
|
|
|
if (cookiePermission == nsICookiePermission::ACCESS_DENY) {
|
|
return Some(false);
|
|
}
|
|
|
|
if (ContentBlockingAllowList::Check(aCookieJarSettings)) {
|
|
return Some(true);
|
|
}
|
|
return Nothing();
|
|
}
|
|
|
|
/* static */ RefPtr<MozPromise<Maybe<bool>, nsresult, true>>
|
|
StorageAccessAPIHelper::
|
|
AsyncCheckCookiesPermittedDecidesStorageAccessAPIOnChildProcess(
|
|
dom::BrowsingContext* aBrowsingContext,
|
|
nsIPrincipal* aRequestingPrincipal) {
|
|
MOZ_ASSERT(XRE_IsContentProcess());
|
|
|
|
ContentChild* cc = ContentChild::GetSingleton();
|
|
MOZ_ASSERT(cc);
|
|
|
|
return cc
|
|
->SendTestCookiePermissionDecided(aBrowsingContext, aRequestingPrincipal)
|
|
->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[](const ContentChild::TestCookiePermissionDecidedPromise::
|
|
ResolveOrRejectValue& aPromise) {
|
|
if (aPromise.IsResolve()) {
|
|
return MozPromise<Maybe<bool>, nsresult, true>::CreateAndResolve(
|
|
aPromise.ResolveValue(), __func__);
|
|
}
|
|
return MozPromise<Maybe<bool>, nsresult, true>::CreateAndReject(
|
|
NS_ERROR_UNEXPECTED, __func__);
|
|
});
|
|
}
|
|
|
|
// static
|
|
Maybe<bool> StorageAccessAPIHelper::CheckBrowserSettingsDecidesStorageAccessAPI(
|
|
nsICookieJarSettings* aCookieJarSettings, bool aThirdParty,
|
|
bool aIsOnThirdPartySkipList, bool aIsThirdPartyTracker) {
|
|
MOZ_ASSERT(aCookieJarSettings);
|
|
uint32_t behavior = aCookieJarSettings->GetCookieBehavior();
|
|
switch (behavior) {
|
|
case nsICookieService::BEHAVIOR_ACCEPT:
|
|
return Some(true);
|
|
case nsICookieService::BEHAVIOR_REJECT_FOREIGN:
|
|
if (!aThirdParty) {
|
|
return Some(true);
|
|
}
|
|
return Some(false);
|
|
case nsICookieService::BEHAVIOR_REJECT:
|
|
return Some(false);
|
|
case nsICookieService::BEHAVIOR_LIMIT_FOREIGN:
|
|
if (!aThirdParty) {
|
|
return Some(true);
|
|
}
|
|
return Some(false);
|
|
case nsICookieService::BEHAVIOR_REJECT_TRACKER:
|
|
if (!aIsThirdPartyTracker) {
|
|
return Some(true);
|
|
}
|
|
if (aIsOnThirdPartySkipList) {
|
|
return Some(true);
|
|
}
|
|
return Nothing();
|
|
case nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
|
|
if (aIsOnThirdPartySkipList) {
|
|
return Some(true);
|
|
}
|
|
return Nothing();
|
|
default:
|
|
MOZ_ASSERT_UNREACHABLE("Must not have undefined cookie behavior");
|
|
}
|
|
MOZ_ASSERT_UNREACHABLE("Must not have undefined cookie behavior");
|
|
return Nothing();
|
|
}
|
|
|
|
// static
|
|
Maybe<bool> StorageAccessAPIHelper::CheckCallingContextDecidesStorageAccessAPI(
|
|
Document* aDocument, bool aRequestingStorageAccess) {
|
|
MOZ_ASSERT(aDocument);
|
|
|
|
if (!aDocument->IsCurrentActiveDocument()) {
|
|
return Some(false);
|
|
}
|
|
|
|
if (aRequestingStorageAccess) {
|
|
// Perform a Permission Policy Request
|
|
dom::FeaturePolicy* policy = aDocument->FeaturePolicy();
|
|
MOZ_ASSERT(policy);
|
|
|
|
if (!policy->AllowsFeature(u"storage-access"_ns,
|
|
dom::Optional<nsAString>())) {
|
|
nsContentUtils::ReportToConsole(
|
|
nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
|
|
aDocument, nsContentUtils::eDOM_PROPERTIES,
|
|
"RequestStorageAccessPermissionsPolicy");
|
|
return Some(false);
|
|
}
|
|
}
|
|
|
|
RefPtr<BrowsingContext> bc = aDocument->GetBrowsingContext();
|
|
if (!bc) {
|
|
return Some(false);
|
|
}
|
|
|
|
// Check if NodePrincipal is not null
|
|
if (!aDocument->NodePrincipal()) {
|
|
return Some(false);
|
|
}
|
|
|
|
// If the document doesn't have a secure context, reject. The Static Pref is
|
|
// used to pass existing tests that do not fulfil this check.
|
|
if (StaticPrefs::dom_storage_access_dont_grant_insecure_contexts() &&
|
|
!aDocument->NodePrincipal()->GetIsOriginPotentiallyTrustworthy()) {
|
|
// Report the error to the console if we are requesting access
|
|
if (aRequestingStorageAccess) {
|
|
nsContentUtils::ReportToConsole(
|
|
nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
|
|
aDocument, nsContentUtils::eDOM_PROPERTIES,
|
|
"RequestStorageAccessNotSecureContext");
|
|
}
|
|
return Some(false);
|
|
}
|
|
|
|
// If the document has a null origin, reject.
|
|
if (aDocument->NodePrincipal()->GetIsNullPrincipal()) {
|
|
// Report an error to the console for this case if we are requesting access
|
|
if (aRequestingStorageAccess) {
|
|
nsContentUtils::ReportToConsole(
|
|
nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
|
|
aDocument, nsContentUtils::eDOM_PROPERTIES,
|
|
"RequestStorageAccessNullPrincipal");
|
|
}
|
|
return Some(false);
|
|
}
|
|
|
|
if (!AntiTrackingUtils::IsThirdPartyDocument(aDocument)) {
|
|
return Some(true);
|
|
}
|
|
|
|
if (aDocument->IsTopLevelContentDocument()) {
|
|
return Some(true);
|
|
}
|
|
|
|
if (aRequestingStorageAccess) {
|
|
if (aDocument->StorageAccessSandboxed()) {
|
|
nsContentUtils::ReportToConsole(
|
|
nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
|
|
aDocument, nsContentUtils::eDOM_PROPERTIES,
|
|
"RequestStorageAccessSandboxed");
|
|
return Some(false);
|
|
}
|
|
}
|
|
return Nothing();
|
|
}
|
|
|
|
// static
|
|
Maybe<bool>
|
|
StorageAccessAPIHelper::CheckSameSiteCallingContextDecidesStorageAccessAPI(
|
|
dom::Document* aDocument, bool aRequireUserActivation) {
|
|
MOZ_ASSERT(aDocument);
|
|
if (aRequireUserActivation) {
|
|
if (!aDocument->HasValidTransientUserGestureActivation()) {
|
|
// Report an error to the console for this case
|
|
nsContentUtils::ReportToConsole(
|
|
nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
|
|
aDocument, nsContentUtils::eDOM_PROPERTIES,
|
|
"RequestStorageAccessUserGesture");
|
|
return Some(false);
|
|
}
|
|
}
|
|
|
|
if (AntiTrackingUtils::IsThirdPartyDocument(aDocument)) {
|
|
return Some(false);
|
|
}
|
|
|
|
// If the document has a null origin, reject.
|
|
if (aDocument->NodePrincipal()->GetIsNullPrincipal()) {
|
|
// Report an error to the console for this case
|
|
nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
|
|
nsLiteralCString("requestStorageAccess"),
|
|
aDocument, nsContentUtils::eDOM_PROPERTIES,
|
|
"RequestStorageAccessNullPrincipal");
|
|
return Some(false);
|
|
}
|
|
return Maybe<bool>();
|
|
}
|
|
|
|
// static
|
|
Maybe<bool>
|
|
StorageAccessAPIHelper::CheckExistingPermissionDecidesStorageAccessAPI(
|
|
dom::Document* aDocument, bool aRequestingStorageAccess) {
|
|
MOZ_ASSERT(aDocument);
|
|
if (aDocument->StorageAccessSandboxed()) {
|
|
if (aRequestingStorageAccess) {
|
|
nsContentUtils::ReportToConsole(
|
|
nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
|
|
aDocument, nsContentUtils::eDOM_PROPERTIES,
|
|
"RequestStorageAccessSandboxed");
|
|
}
|
|
return Some(false);
|
|
}
|
|
if (aDocument->UsingStorageAccess()) {
|
|
return Some(true);
|
|
}
|
|
return Nothing();
|
|
}
|
|
|
|
// static
|
|
RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
|
|
StorageAccessAPIHelper::RequestStorageAccessAsyncHelper(
|
|
dom::Document* aDocument, nsPIDOMWindowInner* aInnerWindow,
|
|
dom::BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal,
|
|
bool aHasUserInteraction, bool aRequireUserInteraction, bool aFrameOnly,
|
|
ContentBlockingNotifier::StorageAccessPermissionGrantedReason aNotifier,
|
|
bool aRequireGrant) {
|
|
MOZ_ASSERT(aDocument);
|
|
MOZ_ASSERT(XRE_IsContentProcess());
|
|
|
|
if (!aRequireGrant) {
|
|
// Try to allow access for the given principal.
|
|
return StorageAccessAPIHelper::AllowAccessForOnChildProcess(
|
|
aPrincipal, aBrowsingContext, aNotifier);
|
|
}
|
|
|
|
RefPtr<nsIPrincipal> principal(aPrincipal);
|
|
|
|
// This is a lambda function that has some variables bound to it. It will be
|
|
// called later in CompleteAllowAccessFor inside of AllowAccessFor.
|
|
auto performPermissionGrant = aDocument->CreatePermissionGrantPromise(
|
|
aInnerWindow, principal, aHasUserInteraction, aRequireUserInteraction,
|
|
Nothing(), aFrameOnly);
|
|
|
|
// Try to allow access for the given principal.
|
|
return StorageAccessAPIHelper::AllowAccessForOnChildProcess(
|
|
principal, aBrowsingContext, aNotifier, performPermissionGrant);
|
|
}
|
|
|
|
// There are two methods to handle permission update:
|
|
// 1. UpdateAllowAccessOnCurrentProcess
|
|
// 2. UpdateAllowAccessOnParentProcess
|
|
//
|
|
// In general, UpdateAllowAccessOnCurrentProcess is used to propagate storage
|
|
// permission to same-origin frames in the same tab.
|
|
// UpdateAllowAccessOnParentProcess is used to propagate storage permission to
|
|
// same-origin frames in the same agent cluster.
|
|
//
|
|
// However, there is an exception in fission mode. When the heuristic is
|
|
// triggered by a first-party window, for instance, a first-party script calls
|
|
// window.open(tracker), we can't update 3rd-party frames's storage permission
|
|
// in the child process that triggers the permission update because the
|
|
// first-party and the 3rd-party are not in the same process. In this case, we
|
|
// should update the storage permission in UpdateAllowAccessOnParentProcess.
|
|
|
|
// This function is used to update permission to all in-process windows, so it
|
|
// can be called either from the parent or the child.
|
|
/* static */
|
|
void StorageAccessAPIHelper::UpdateAllowAccessOnCurrentProcess(
|
|
BrowsingContext* aParentContext, const nsACString& aTrackingOrigin) {
|
|
MOZ_ASSERT(aParentContext && aParentContext->IsInProcess());
|
|
|
|
bool useRemoteSubframes;
|
|
aParentContext->GetUseRemoteSubframes(&useRemoteSubframes);
|
|
|
|
if (useRemoteSubframes && aParentContext->IsTopContent()) {
|
|
// If we are a first-party and we are in fission mode, bail out early
|
|
// because we can't do anything here.
|
|
return;
|
|
}
|
|
|
|
BrowsingContext* top = aParentContext->Top();
|
|
|
|
// Propagate the storage permission to same-origin frames in the same tab.
|
|
top->PreOrderWalk([&](BrowsingContext* aContext) {
|
|
// Only check browsing contexts that are in-process.
|
|
if (aContext->IsInProcess()) {
|
|
nsAutoCString origin;
|
|
Unused << AntiTrackingUtils::GetPrincipalAndTrackingOrigin(
|
|
aContext, nullptr, origin);
|
|
|
|
if (aTrackingOrigin == origin) {
|
|
nsCOMPtr<nsPIDOMWindowInner> inner =
|
|
AntiTrackingUtils::GetInnerWindow(aContext);
|
|
if (inner) {
|
|
inner->SaveStorageAccessPermissionGranted();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/* static */
|
|
void StorageAccessAPIHelper::UpdateAllowAccessOnParentProcess(
|
|
BrowsingContext* aParentContext, const nsACString& aTrackingOrigin) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
|
|
nsAutoCString topKey;
|
|
nsCOMPtr<nsIPrincipal> topPrincipal =
|
|
AntiTrackingUtils::GetPrincipal(aParentContext->Top());
|
|
PermissionManager::GetKeyForPrincipal(topPrincipal, false, true, topKey);
|
|
|
|
// Propagate the storage permission to same-origin frames in the same
|
|
// agent-cluster.
|
|
for (const auto& topContext : aParentContext->Group()->Toplevels()) {
|
|
if (topContext == aParentContext->Top()) {
|
|
// In non-fission mode, storage permission is stored in the top-level,
|
|
// don't need to propagate it to tracker frames.
|
|
bool useRemoteSubframes;
|
|
aParentContext->GetUseRemoteSubframes(&useRemoteSubframes);
|
|
if (!useRemoteSubframes) {
|
|
continue;
|
|
}
|
|
// If parent context is third-party, we already propagate permission
|
|
// in the child process, skip propagating here.
|
|
RefPtr<dom::WindowContext> ctx =
|
|
aParentContext->GetCurrentWindowContext();
|
|
if (ctx && ctx->GetIsThirdPartyWindow()) {
|
|
continue;
|
|
}
|
|
} else {
|
|
nsCOMPtr<nsIPrincipal> principal =
|
|
AntiTrackingUtils::GetPrincipal(topContext);
|
|
if (!principal) {
|
|
continue;
|
|
}
|
|
|
|
nsAutoCString key;
|
|
PermissionManager::GetKeyForPrincipal(principal, false, true, key);
|
|
// Make sure we only apply to frames that have the same top-level.
|
|
if (topKey != key) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
topContext->PreOrderWalk([&](BrowsingContext* aContext) {
|
|
WindowGlobalParent* wgp = aContext->Canonical()->GetCurrentWindowGlobal();
|
|
if (!wgp) {
|
|
return;
|
|
}
|
|
|
|
nsAutoCString origin;
|
|
AntiTrackingUtils::GetPrincipalAndTrackingOrigin(aContext, nullptr,
|
|
origin);
|
|
if (aTrackingOrigin == origin) {
|
|
Unused << wgp->SendSaveStorageAccessPermissionGranted();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
NS_IMPL_ISUPPORTS(StorageAccessGrantTelemetryClassification,
|
|
nsIUrlClassifierFeatureCallback);
|
|
|
|
StorageAccessGrantTelemetryClassification::
|
|
StorageAccessGrantTelemetryClassification(uint16_t aType)
|
|
: mType(aType) {}
|
|
|
|
// static
|
|
void StorageAccessGrantTelemetryClassification::MaybeReportTracker(
|
|
uint16_t aType, nsIURI* aURI) {
|
|
MOZ_ASSERT(aURI);
|
|
nsresult rv = NS_OK;
|
|
nsCOMPtr<nsIURIClassifier> uriClassifier =
|
|
do_GetService(NS_URICLASSIFIERSERVICE_CONTRACTID, &rv);
|
|
NS_ENSURE_SUCCESS_VOID(rv);
|
|
NS_ENSURE_TRUE_VOID(uriClassifier);
|
|
|
|
const nsTArray<nsCString>& featureNames =
|
|
GetClassifierFeatureNamesForTrackers();
|
|
|
|
RefPtr<StorageAccessGrantTelemetryClassification> classification =
|
|
new StorageAccessGrantTelemetryClassification(aType);
|
|
|
|
rv = uriClassifier->AsyncClassifyLocalWithFeatureNames(
|
|
aURI, featureNames, nsIUrlClassifierFeature::blocklist, classification);
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
}
|
|
|
|
// nsIUrlClassifierFeatureCallback
|
|
NS_IMETHODIMP
|
|
StorageAccessGrantTelemetryClassification::OnClassifyComplete(
|
|
const nsTArray<RefPtr<nsIUrlClassifierFeatureResult>>& aResults) {
|
|
// If this URI is classified as a tracker, increment that category and the
|
|
// base value.
|
|
if (!aResults.IsEmpty()) {
|
|
// Because of compilation errors importing glean headers, we cannot use
|
|
// glean::contentblocking::StorageAccessGrantedCountLabel as the parameter
|
|
// to this constructor directly, so we do the next best thing and use its
|
|
// base type and make sure we don't go over the maximum defined value
|
|
if (static_cast<uint16_t>(glean::contentblocking::
|
|
StorageAccessGrantedCountLabel::e__Other__) <
|
|
mType) {
|
|
mType = static_cast<uint16_t>(
|
|
glean::contentblocking::StorageAccessGrantedCountLabel::e__Other__);
|
|
}
|
|
glean::contentblocking::StorageAccessGrantedCountLabel type{mType};
|
|
glean::contentblocking::storage_access_granted_count
|
|
.EnumGet(glean::contentblocking::StorageAccessGrantedCountLabel::
|
|
eStoragegrantedCt)
|
|
.Add();
|
|
glean::contentblocking::storage_access_granted_count.EnumGet(type).Add();
|
|
}
|
|
return NS_OK;
|
|
}
|