/* -*- Mode: C++; tab-width: 2; 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 "mozilla/PermissionDelegateHandler.h" #include "nsGlobalWindowInner.h" #include "nsPIDOMWindow.h" #include "nsIPrincipal.h" #include "nsContentPermissionHelper.h" #include "mozilla/BasePrincipal.h" #include "mozilla/StaticPrefs_permissions.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/FeaturePolicyUtils.h" #include "mozilla/dom/WindowContext.h" #include "mozilla/PermissionManager.h" using namespace mozilla::dom; namespace mozilla { typedef PermissionDelegateHandler::PermissionDelegatePolicy DelegatePolicy; typedef PermissionDelegateHandler::PermissionDelegateInfo DelegateInfo; // Particular type of permissions to care about. We decide cases by case and // give various types of controls over each of these. static const DelegateInfo sPermissionsMap[] = { // Permissions API map. All permission names have to be in lowercase. {"geo", u"geolocation", DelegatePolicy::eDelegateUseFeaturePolicy}, // The same with geo, but we support both to save some conversions between // "geo" and "geolocation" {"geolocation", u"geolocation", DelegatePolicy::eDelegateUseFeaturePolicy}, {"desktop-notification", nullptr, DelegatePolicy::ePersistDeniedCrossOrigin}, {"persistent-storage", nullptr, DelegatePolicy::ePersistDeniedCrossOrigin}, {"vibration", nullptr, DelegatePolicy::ePersistDeniedCrossOrigin}, {"midi", nullptr, DelegatePolicy::eDelegateUseIframeOrigin}, // Like "midi" but with sysex support. {"midi-sysex", nullptr, DelegatePolicy::eDelegateUseIframeOrigin}, {"storage-access", nullptr, DelegatePolicy::eDelegateUseIframeOrigin}, {"camera", u"camera", DelegatePolicy::eDelegateUseFeaturePolicy}, {"microphone", u"microphone", DelegatePolicy::eDelegateUseFeaturePolicy}, {"screen", u"display-capture", DelegatePolicy::eDelegateUseFeaturePolicy}, {"xr", u"xr-spatial-tracking", DelegatePolicy::eDelegateUseFeaturePolicy}, }; static_assert(PermissionDelegateHandler::DELEGATED_PERMISSION_COUNT == (sizeof(sPermissionsMap) / sizeof(DelegateInfo)), "The PermissionDelegateHandler::DELEGATED_PERMISSION_COUNT must " "match to the " "length of sPermissionsMap. Please update it."); NS_IMPL_CYCLE_COLLECTION(PermissionDelegateHandler) NS_IMPL_CYCLE_COLLECTING_ADDREF(PermissionDelegateHandler) NS_IMPL_CYCLE_COLLECTING_RELEASE(PermissionDelegateHandler) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PermissionDelegateHandler) NS_INTERFACE_MAP_ENTRY(nsIPermissionDelegateHandler) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END PermissionDelegateHandler::PermissionDelegateHandler(dom::Document* aDocument) : mDocument(aDocument) { MOZ_ASSERT(aDocument); } /* static */ const DelegateInfo* PermissionDelegateHandler::GetPermissionDelegateInfo( const nsAString& aPermissionName) { nsAutoString lowerContent(aPermissionName); ToLowerCase(lowerContent); for (const auto& perm : sPermissionsMap) { if (lowerContent.EqualsASCII(perm.mPermissionName)) { return &perm; } } return nullptr; } NS_IMETHODIMP PermissionDelegateHandler::MaybeUnsafePermissionDelegate( const nsTArray& aTypes, bool* aMaybeUnsafe) { *aMaybeUnsafe = false; if (!StaticPrefs::permissions_delegation_enabled()) { return NS_OK; } for (auto& type : aTypes) { const DelegateInfo* info = GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(type)); if (!info) { continue; } nsAutoString featureName(info->mFeatureName); if (FeaturePolicyUtils::IsFeatureUnsafeAllowedAll(mDocument, featureName)) { *aMaybeUnsafe = true; return NS_OK; } } return NS_OK; } NS_IMETHODIMP PermissionDelegateHandler::GetPermissionDelegateFPEnabled(bool* aEnabled) { MOZ_ASSERT(NS_IsMainThread()); *aEnabled = StaticPrefs::permissions_delegation_enabled(); return NS_OK; } /* static */ nsresult PermissionDelegateHandler::GetDelegatePrincipal( const nsACString& aType, nsIContentPermissionRequest* aRequest, nsIPrincipal** aResult) { MOZ_ASSERT(aRequest); if (!StaticPrefs::permissions_delegation_enabled()) { return aRequest->GetPrincipal(aResult); } const DelegateInfo* info = GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType)); if (!info) { *aResult = nullptr; return NS_OK; } if (info->mPolicy == DelegatePolicy::eDelegateUseTopOrigin || info->mPolicy == DelegatePolicy::eDelegateUseFeaturePolicy) { return aRequest->GetTopLevelPrincipal(aResult); } return aRequest->GetPrincipal(aResult); } bool PermissionDelegateHandler::Initialize() { MOZ_ASSERT(mDocument); mPermissionManager = PermissionManager::GetInstance(); if (!mPermissionManager) { return false; } mPrincipal = mDocument->NodePrincipal(); return true; } static bool IsCrossOriginContentToTop(Document* aDocument) { MOZ_ASSERT(aDocument); RefPtr bc = aDocument->GetBrowsingContext(); if (!bc) { return true; } RefPtr topBC = bc->Top(); // In Fission, we can know if it is cross-origin by checking whether both // contexts in the same process. So, If they are not in the same process, we // can say that it's cross-origin. if (!topBC->IsInProcess()) { return true; } RefPtr topDoc = topBC->GetDocument(); if (!topDoc) { return true; } nsCOMPtr topLevelPrincipal = topDoc->NodePrincipal(); return !aDocument->NodePrincipal()->Subsumes(topLevelPrincipal); } bool PermissionDelegateHandler::HasFeaturePolicyAllowed( const DelegateInfo* info) const { if (info->mPolicy != DelegatePolicy::eDelegateUseFeaturePolicy || !info->mFeatureName) { return true; } nsAutoString featureName(info->mFeatureName); return FeaturePolicyUtils::IsFeatureAllowed(mDocument, featureName); } bool PermissionDelegateHandler::HasPermissionDelegated( const nsACString& aType) const { MOZ_ASSERT(mDocument); // System principal should have right to make permission request if (mPrincipal->IsSystemPrincipal()) { return true; } const DelegateInfo* info = GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType)); if (!info || !HasFeaturePolicyAllowed(info)) { return false; } if (!StaticPrefs::permissions_delegation_enabled()) { return true; } if (info->mPolicy == DelegatePolicy::ePersistDeniedCrossOrigin && !mDocument->IsTopLevelContentDocument() && IsCrossOriginContentToTop(mDocument)) { return false; } return true; } nsresult PermissionDelegateHandler::GetPermission(const nsACString& aType, uint32_t* aPermission, bool aExactHostMatch) { MOZ_ASSERT(mDocument); MOZ_ASSERT(mPrincipal); if (mPrincipal->IsSystemPrincipal()) { *aPermission = nsIPermissionManager::ALLOW_ACTION; return NS_OK; } const DelegateInfo* info = GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType)); if (!info || !HasFeaturePolicyAllowed(info)) { *aPermission = nsIPermissionManager::DENY_ACTION; return NS_OK; } nsresult (NS_STDCALL nsIPermissionManager::*testPermission)( nsIPrincipal*, const nsACString&, uint32_t*) = aExactHostMatch ? &nsIPermissionManager::TestExactPermissionFromPrincipal : &nsIPermissionManager::TestPermissionFromPrincipal; if (!StaticPrefs::permissions_delegation_enabled()) { return (mPermissionManager->*testPermission)(mPrincipal, aType, aPermission); } if (info->mPolicy == DelegatePolicy::ePersistDeniedCrossOrigin && !mDocument->IsTopLevelContentDocument() && IsCrossOriginContentToTop(mDocument)) { *aPermission = nsIPermissionManager::DENY_ACTION; return NS_OK; } nsIPrincipal* principal = mPrincipal; // If we cannot get the browsing context from the document, we fallback to use // the prinicpal of the document to test the permission. RefPtr bc = mDocument->GetBrowsingContext(); if ((info->mPolicy == DelegatePolicy::eDelegateUseTopOrigin || info->mPolicy == DelegatePolicy::eDelegateUseFeaturePolicy) && bc) { RefPtr topWC = bc->GetTopWindowContext(); if (topWC && topWC->IsInProcess()) { // If the top-level window context is in the same process, we directly get // the node principal from the top-level document to test the permission. // We cannot check the lists in the window context in this case since the // 'perm-changed' could be notified in the iframe before the top-level in // certain cases, for example, request permissions in first-party iframes. // In this case, the list in window context hasn't gotten updated, so it // would has an out-dated value until the top-level window get the // observer. So, we have to test permission manager directly if we can. RefPtr topDoc = topWC->GetBrowsingContext()->GetDocument(); if (topDoc) { principal = topDoc->NodePrincipal(); } } else if (topWC) { // Get the delegated permissions from the top-level window context. DelegatedPermissionList list = aExactHostMatch ? topWC->GetDelegatedExactHostMatchPermissions() : topWC->GetDelegatedPermissions(); size_t idx = std::distance(sPermissionsMap, info); *aPermission = list.mPermissions[idx]; return NS_OK; } } return (mPermissionManager->*testPermission)(principal, aType, aPermission); } nsresult PermissionDelegateHandler::GetPermissionForPermissionsAPI( const nsACString& aType, uint32_t* aPermission) { return GetPermission(aType, aPermission, false); } void PermissionDelegateHandler::PopulateAllDelegatedPermissions() { MOZ_ASSERT(mDocument); MOZ_ASSERT(mPermissionManager); // We only populate the delegated permissions for the top-level content. if (!mDocument->IsTopLevelContentDocument()) { return; } RefPtr wc = mDocument->GetWindowContext(); NS_ENSURE_TRUE_VOID(wc && !wc->IsDiscarded()); DelegatedPermissionList list; DelegatedPermissionList exactHostMatchList; for (const auto& perm : sPermissionsMap) { size_t idx = std::distance(sPermissionsMap, &perm); nsDependentCString type(perm.mPermissionName); // Populate the permission. uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION; Unused << mPermissionManager->TestPermissionFromPrincipal(mPrincipal, type, &permission); list.mPermissions[idx] = permission; // Populate the exact-host-match permission. permission = nsIPermissionManager::UNKNOWN_ACTION; Unused << mPermissionManager->TestExactPermissionFromPrincipal( mPrincipal, type, &permission); exactHostMatchList.mPermissions[idx] = permission; } WindowContext::Transaction txn; txn.SetDelegatedPermissions(list); txn.SetDelegatedExactHostMatchPermissions(exactHostMatchList); MOZ_ALWAYS_SUCCEEDS(txn.Commit(wc)); } void PermissionDelegateHandler::UpdateDelegatedPermission( const nsACString& aType) { MOZ_ASSERT(mDocument); MOZ_ASSERT(mPermissionManager); // We only update the delegated permission for the top-level content. if (!mDocument->IsTopLevelContentDocument()) { return; } RefPtr wc = mDocument->GetWindowContext(); NS_ENSURE_TRUE_VOID(wc); const DelegateInfo* info = GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType)); if (!info) { return; } size_t idx = std::distance(sPermissionsMap, info); WindowContext::Transaction txn; bool changed = false; DelegatedPermissionList list = wc->GetDelegatedPermissions(); if (UpdateDelegatePermissionInternal( list, aType, idx, &nsIPermissionManager::TestPermissionFromPrincipal)) { txn.SetDelegatedPermissions(list); changed = true; } DelegatedPermissionList exactHostMatchList = wc->GetDelegatedExactHostMatchPermissions(); if (UpdateDelegatePermissionInternal( exactHostMatchList, aType, idx, &nsIPermissionManager::TestExactPermissionFromPrincipal)) { txn.SetDelegatedExactHostMatchPermissions(exactHostMatchList); changed = true; } // We only commit if there is any change of permissions. if (changed) { MOZ_ALWAYS_SUCCEEDS(txn.Commit(wc)); } } bool PermissionDelegateHandler::UpdateDelegatePermissionInternal( PermissionDelegateHandler::DelegatedPermissionList& aList, const nsACString& aType, size_t aIdx, nsresult (NS_STDCALL nsIPermissionManager::*aTestFunc)(nsIPrincipal*, const nsACString&, uint32_t*)) { MOZ_ASSERT(aTestFunc); MOZ_ASSERT(mPermissionManager); MOZ_ASSERT(mPrincipal); uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION; Unused << (mPermissionManager->*aTestFunc)(mPrincipal, aType, &permission); if (aList.mPermissions[aIdx] != permission) { aList.mPermissions[aIdx] = permission; return true; } return false; } } // namespace mozilla