Skip to content

Commit

Permalink
Bug 1914417 - Refactor notification permission checker using principa…
Browse files Browse the repository at this point in the history
…l partition key r=asuth,bvandersloot

This patch keeps the current status where Firefox and Chrome both allows notification permission in ABA context although the spec does not cover it. (whatwg/notifications#177, also listed in https://privacycg.github.io/storage-partitioning/#remaining-user-agent-state)

The child patch in bug 1914203 will add a telemetry to see whether the usage is low enough so that we can block ABA access, to make it more consistent with storage partitioning.

Differential Revision: https://phabricator.services.mozilla.com/D219908
  • Loading branch information
saschanaz committed Sep 20, 2024
1 parent e2198f4 commit b710b85
Show file tree
Hide file tree
Showing 17 changed files with 275 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ const { PermissionTestUtils } = ChromeUtils.importESModule(
);

let notificationURL =
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
"http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
let oldShowFavicons;

add_task(async function test_notificationClose() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ const ALERT_SERVICE = Cc["@mozilla.org/alerts-service;1"]
.QueryInterface(Ci.nsIAlertsDoNotDisturb);

const PAGE =
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
"http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";

// The amount of time in seconds that we will wait for a notification
// to show up before we decide that it's not coming.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"use strict";

var notificationURL =
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
"http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
var expectedURL = "about:preferences#privacy";

add_task(async function test_settingsOpen_observer() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ const { PermissionTestUtils } = ChromeUtils.importESModule(

var tab;
var notificationURL =
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
"http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
var alertWindowClosed = false;
var permRemoved = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ const { PermissionTestUtils } = ChromeUtils.importESModule(
var tab;
var notification;
var notificationURL =
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
"http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
var newWindowOpenedFromTab;

add_task(async function test_notificationPreventDefaultAndSwitchTabs() {
Expand Down
200 changes: 72 additions & 128 deletions dom/notification/Notification.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/JSONStringWriteFuncs.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/AppNotificationServiceOptionsBinding.h"
Expand All @@ -32,6 +31,7 @@
#include "mozilla/dom/WorkerScope.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "Navigator.h"
#include "NotificationUtils.h"
#include "nsComponentManagerUtils.h"
#include "nsContentPermissionHelper.h"
#include "nsContentUtils.h"
Expand All @@ -55,6 +55,8 @@

namespace mozilla::dom {

using namespace notification;

struct NotificationStrings {
const nsString mID;
const nsString mTitle;
Expand Down Expand Up @@ -211,10 +213,12 @@ class NotificationPermissionRequest : public ContentPermissionRequestBase,
NS_IMETHOD Allow(JS::Handle<JS::Value> choices) override;

NotificationPermissionRequest(nsIPrincipal* aPrincipal,
nsIPrincipal* aEffectiveStoragePrincipal,
nsPIDOMWindowInner* aWindow, Promise* aPromise,
NotificationPermissionCallback* aCallback)
: ContentPermissionRequestBase(aPrincipal, aWindow, "notification"_ns,
"desktop-notification"_ns),
mEffectiveStoragePrincipal(aEffectiveStoragePrincipal),
mPermission(NotificationPermission::Default),
mPromise(aPromise),
mCallback(aCallback) {
Expand All @@ -231,6 +235,7 @@ class NotificationPermissionRequest : public ContentPermissionRequestBase,

MOZ_CAN_RUN_SCRIPT nsresult ResolvePromise();
nsresult DispatchResolvePromise();
nsCOMPtr<nsIPrincipal> mEffectiveStoragePrincipal;
NotificationPermission mPermission;
RefPtr<Promise> mPromise;
RefPtr<NotificationPermissionCallback> mCallback;
Expand All @@ -253,22 +258,33 @@ class ReleaseNotificationControlRunnable final
};

class GetPermissionRunnable final : public WorkerMainThreadRunnable {
NotificationPermission mPermission;

public:
explicit GetPermissionRunnable(WorkerPrivate* aWorker)
explicit GetPermissionRunnable(WorkerPrivate* aWorker,
bool aUseRegularPrincipal,
PermissionCheckPurpose aPurpose)
: WorkerMainThreadRunnable(aWorker, "Notification :: Get Permission"_ns),
mPermission(NotificationPermission::Denied) {}
mUseRegularPrincipal(aUseRegularPrincipal),
mPurpose(aPurpose) {}

bool MainThreadRun() override {
ErrorResult result;
MOZ_ASSERT(mWorkerRef);
mPermission = Notification::GetPermissionInternal(
mWorkerRef->Private()->GetPrincipal(), result);
WorkerPrivate* workerPrivate = mWorkerRef->Private();
nsIPrincipal* principal = workerPrivate->GetPrincipal();
nsIPrincipal* effectiveStoragePrincipal =
mUseRegularPrincipal ? principal
: workerPrivate->GetPartitionedPrincipal();
mPermission =
GetNotificationPermission(principal, effectiveStoragePrincipal,
workerPrivate->IsSecureContext(), mPurpose);
return true;
}

NotificationPermission GetPermission() { return mPermission; }

private:
NotificationPermission mPermission = NotificationPermission::Denied;
bool mUseRegularPrincipal;
PermissionCheckPurpose mPurpose;
};

class FocusWindowRunnable final : public Runnable {
Expand Down Expand Up @@ -487,30 +503,14 @@ NS_IMPL_QUERY_INTERFACE_CYCLE_COLLECTION_INHERITED(

NS_IMETHODIMP
NotificationPermissionRequest::Run() {
bool isSystem = mPrincipal->IsSystemPrincipal();
bool blocked = false;
if (isSystem) {
if (IsNotificationAllowedFor(mPrincipal)) {
mPermission = NotificationPermission::Granted;
} else if (mPrincipal->GetIsInPrivateBrowsing() &&
!StaticPrefs::dom_webnotifications_privateBrowsing_enabled()) {
} else if (IsNotificationForbiddenFor(
mPrincipal, mEffectiveStoragePrincipal,
mWindow->IsSecureContext(),
PermissionCheckPurpose::PermissionRequest,
mWindow->GetExtantDoc())) {
mPermission = NotificationPermission::Denied;
blocked = true;
} else {
// File are automatically granted permission.

if (mPrincipal->SchemeIs("file")) {
mPermission = NotificationPermission::Granted;
} else if (!mWindow->IsSecureContext()) {
mPermission = NotificationPermission::Denied;
blocked = true;
nsCOMPtr<Document> doc = mWindow->GetExtantDoc();
if (doc) {
nsContentUtils::ReportToConsole(
nsIScriptError::errorFlag, "DOM"_ns, doc,
nsContentUtils::eDOM_PROPERTIES,
"NotificationsInsecureRequestIsForbidden");
}
}
}

// We can't call ShowPrompt() directly here since our logic for determining
Expand Down Expand Up @@ -538,22 +538,6 @@ NotificationPermissionRequest::Run() {
}
}

// Check this after checking the prompt prefs to make sure this pref overrides
// those. We rely on this for testing purposes.
if (!isSystem && !blocked &&
!StaticPrefs::dom_webnotifications_allowcrossoriginiframe() &&
!mPrincipal->Subsumes(mTopLevelPrincipal)) {
mPermission = NotificationPermission::Denied;
blocked = true;
nsCOMPtr<Document> doc = mWindow->GetExtantDoc();
if (doc) {
nsContentUtils::ReportToConsole(
nsIScriptError::errorFlag, "DOM"_ns, doc,
nsContentUtils::eDOM_PROPERTIES,
"NotificationsCrossOriginIframeRequestIsForbidden");
}
}

if (mPermission != NotificationPermission::Default) {
return DispatchResolvePromise();
}
Expand Down Expand Up @@ -605,7 +589,7 @@ nsresult NotificationPermissionRequest::ResolvePromise() {
}
}

mPermission = Notification::TestPermission(mPrincipal);
mPermission = GetRawNotificationPermission(mPrincipal);
}
if (mCallback) {
ErrorResult error;
Expand Down Expand Up @@ -717,6 +701,7 @@ Notification::Notification(nsIGlobalObject* aGlobal, const nsAString& aID,
if (!NS_IsMainThread()) {
mWorkerPrivate = GetCurrentThreadWorkerPrivate();
MOZ_ASSERT(mWorkerPrivate);
mWorkerUseRegularPrincipal = mWorkerPrivate->UseRegularPrincipal();
}
}

Expand Down Expand Up @@ -1353,9 +1338,16 @@ void Notification::ShowInternal() {
ErrorResult result;
NotificationPermission permission = NotificationPermission::Denied;
if (mWorkerPrivate) {
permission = GetPermissionInternal(mWorkerPrivate->GetPrincipal(), result);
nsIPrincipal* principal = mWorkerPrivate->GetPrincipal();
nsIPrincipal* effectiveStoragePrincipal =
mWorkerUseRegularPrincipal ? principal
: mWorkerPrivate->GetPartitionedPrincipal();
permission = GetNotificationPermission(
principal, effectiveStoragePrincipal, mWorkerPrivate->IsSecureContext(),
PermissionCheckPurpose::NotificationShow);
} else {
permission = GetPermissionInternal(GetOwnerWindow(), result);
permission = GetPermissionInternal(
GetOwnerWindow(), PermissionCheckPurpose::NotificationShow, result);
}
// We rely on GetPermissionInternal returning Denied on all failure codepaths.
MOZ_ASSERT_IF(result.Failed(), permission == NotificationPermission::Denied);
Expand Down Expand Up @@ -1495,7 +1487,9 @@ already_AddRefed<Promise> Notification::RequestPermission(
}

nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
if (!principal) {
nsCOMPtr<nsIPrincipal> effectiveStoragePrincipal =
sop->GetEffectiveStoragePrincipal();
if (!principal || !effectiveStoragePrincipal) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return nullptr;
}
Expand All @@ -1508,8 +1502,9 @@ already_AddRefed<Promise> Notification::RequestPermission(
if (aCallback.WasPassed()) {
permissionCallback = &aCallback.Value();
}
nsCOMPtr<nsIRunnable> request = new NotificationPermissionRequest(
principal, window, promise, permissionCallback);
nsCOMPtr<nsIRunnable> request =
new NotificationPermissionRequest(principal, effectiveStoragePrincipal,
window, promise, permissionCallback);

window->AsGlobal()->Dispatch(request.forget());

Expand All @@ -1520,30 +1515,34 @@ already_AddRefed<Promise> Notification::RequestPermission(
NotificationPermission Notification::GetPermission(const GlobalObject& aGlobal,
ErrorResult& aRv) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
return GetPermission(global, aRv);
return GetPermission(global, PermissionCheckPurpose::PermissionAttribute,
aRv);
}

// static
NotificationPermission Notification::GetPermission(nsIGlobalObject* aGlobal,
ErrorResult& aRv) {
NotificationPermission Notification::GetPermission(
nsIGlobalObject* aGlobal, PermissionCheckPurpose aPurpose,
ErrorResult& aRv) {
if (NS_IsMainThread()) {
return GetPermissionInternal(aGlobal->GetAsInnerWindow(), aRv);
} else {
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
MOZ_ASSERT(worker);
RefPtr<GetPermissionRunnable> r = new GetPermissionRunnable(worker);
r->Dispatch(worker, Canceling, aRv);
if (aRv.Failed()) {
return NotificationPermission::Denied;
}
return GetPermissionInternal(aGlobal->GetAsInnerWindow(), aPurpose, aRv);
}

return r->GetPermission();
WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
MOZ_ASSERT(worker);
RefPtr<GetPermissionRunnable> r = new GetPermissionRunnable(
worker, worker->UseRegularPrincipal(), aPurpose);
r->Dispatch(worker, Canceling, aRv);
if (aRv.Failed()) {
return NotificationPermission::Denied;
}

return r->GetPermission();
}

/* static */
NotificationPermission Notification::GetPermissionInternal(
nsPIDOMWindowInner* aWindow, ErrorResult& aRv) {
nsPIDOMWindowInner* aWindow, PermissionCheckPurpose aPurpose,
ErrorResult& aRv) {
// Get principal from global to check permission for notifications.
nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
if (!sop) {
Expand All @@ -1552,71 +1551,15 @@ NotificationPermission Notification::GetPermissionInternal(
}

nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
if (!principal) {
nsCOMPtr<nsIPrincipal> effectiveStoragePrincipal =
sop->GetEffectiveStoragePrincipal();
if (!principal || !effectiveStoragePrincipal) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return NotificationPermission::Denied;
}

if (principal->GetIsInPrivateBrowsing() &&
!StaticPrefs::dom_webnotifications_privateBrowsing_enabled()) {
return NotificationPermission::Denied;
}
// Disallow showing notification if our origin is not the same origin as the
// toplevel one, see https://github.com/whatwg/notifications/issues/177.
if (!StaticPrefs::dom_webnotifications_allowcrossoriginiframe()) {
nsCOMPtr<nsIScriptObjectPrincipal> topSop =
do_QueryInterface(aWindow->GetBrowsingContext()->Top()->GetDOMWindow());
nsIPrincipal* topPrincipal = topSop ? topSop->GetPrincipal() : nullptr;
if (!topPrincipal || !principal->Subsumes(topPrincipal)) {
return NotificationPermission::Denied;
}
}

return GetPermissionInternal(principal, aRv);
}

/* static */
NotificationPermission Notification::GetPermissionInternal(
nsIPrincipal* aPrincipal, ErrorResult& aRv) {
AssertIsOnMainThread();
MOZ_ASSERT(aPrincipal);

if (aPrincipal->IsSystemPrincipal()) {
return NotificationPermission::Granted;
} else {
// Allow files to show notifications by default.
if (aPrincipal->SchemeIs("file")) {
return NotificationPermission::Granted;
}
}

return TestPermission(aPrincipal);
}

/* static */
NotificationPermission Notification::TestPermission(nsIPrincipal* aPrincipal) {
AssertIsOnMainThread();

uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;

nsCOMPtr<nsIPermissionManager> permissionManager =
components::PermissionManager::Service();
if (!permissionManager) {
return NotificationPermission::Default;
}

permissionManager->TestExactPermissionFromPrincipal(
aPrincipal, "desktop-notification"_ns, &permission);

// Convert the result to one of the enum types.
switch (permission) {
case nsIPermissionManager::ALLOW_ACTION:
return NotificationPermission::Granted;
case nsIPermissionManager::DENY_ACTION:
return NotificationPermission::Denied;
default:
return NotificationPermission::Default;
}
return GetNotificationPermission(principal, effectiveStoragePrincipal,
aWindow->IsSecureContext(), aPurpose);
}

nsresult Notification::ResolveIconAndSoundURL(nsIGlobalObject* aGlobal,
Expand Down Expand Up @@ -2219,7 +2162,8 @@ already_AddRefed<Promise> Notification::ShowPersistentNotification(
// We check permission here rather than pass the Promise to NotificationTask
// which leads to uglier code.
// XXX: GetPermission is a synchronous blocking function on workers.
NotificationPermission permission = GetPermission(aGlobal, aRv);
NotificationPermission permission =
GetPermission(aGlobal, PermissionCheckPurpose::NotificationShow, aRv);

// Step 6.1: If the result of getting the notifications permission state is
// not "granted", then queue a global task on the DOM manipulation task source
Expand Down
Loading

0 comments on commit b710b85

Please sign in to comment.