Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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/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/PermissionManager.h"
#include "mozilla/StaticPrefs_network.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/Telemetry.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;
}
} // 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();
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;
return SaveAccessForOriginOnParentProcess(aTopLevelWindowId, aParentContext,
trackingPrincipal, aAllowMode,
frameOnly)
->Then(GetCurrentSerialEventTarget(), __func__,
[aReason, trackingPrincipal](
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);
}
return StorageAccessPermissionGrantPromise::CreateAndResolve(
StorageAccessAPIHelper::eAllow, __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 */ 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;
return cc
->SendStorageAccessPermissionGrantedForOrigin(
aTopLevelWindowId, aParentContext, trackingPrincipal,
trackingOrigin, aAllowMode, reportReason, frameOnly)
->Then(
GetCurrentSerialEventTarget(), __func__,
[aReason, trackingPrincipal](
const ContentChild::
StorageAccessPermissionGrantedForOriginPromise::
ResolveOrRejectValue& aValue) {
if (aValue.IsResolve()) {
if (aValue.ResolveValue() &&
(aReason == ContentBlockingNotifier::eStorageAccessAPI)) {
ContentBlockingUserInteraction::Observe(trackingPrincipal);
}
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;
}
Telemetry::AccumulateCategorical(
Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageGranted);
switch (aReason) {
case ContentBlockingNotifier::StorageAccessPermissionGrantedReason::
eStorageAccessAPI:
Telemetry::AccumulateCategorical(
Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageAccessAPI);
break;
case ContentBlockingNotifier::StorageAccessPermissionGrantedReason::
eOpenerAfterUserInteraction:
Telemetry::AccumulateCategorical(
Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::OpenerAfterUI);
break;
case ContentBlockingNotifier::StorageAccessPermissionGrantedReason::eOpener:
Telemetry::AccumulateCategorical(
Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::Opener);
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__);
}
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();
}
});
}
}