Bug 1483631 - Restrict nested permission requests in ContentPermissionPrompt with permission delegate r=baku,mccr8

Differential Revision: https://phabricator.services.mozilla.com/D47416

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Thomas Nguyen 2019-12-04 15:39:03 +00:00
parent b727e2abc1
commit fd744cea01
14 changed files with 345 additions and 71 deletions

View File

@ -1727,6 +1727,12 @@ pref("view_source.tab", true);
pref("dom.serviceWorkers.enabled", true);
#ifdef NIGHTLY_BUILD
pref("dom.security.featurePolicy.enabled", true);
#else
pref("dom.security.featurePolicy.enabled", false);
#endif
// Enable Push API.
pref("dom.push.enabled", true);

View File

@ -9,6 +9,8 @@ skip-if = debug || os == "linux" && asan # Bug 1522069
[browser_permissions_delegate_vibrate.js]
support-files=
empty.html
[browser_permission_delegate_geo.js]
skip-if = fission # Bug 1587743
[browser_permissions_event_telemetry.js]
[browser_permissions_postPrompt.js]
support-files=

View File

@ -0,0 +1,155 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const ORIGIN = "https://example.com";
const CROSS_SUBFRAME_PAGE =
getRootDirectory(gTestPath).replace("chrome://mochitests/content", ORIGIN) +
"temporary_permissions_subframe.html";
const PromptResult = {
ALLOW: "allow",
DENY: "deny",
PROMPT: "prompt",
};
var Perms = Services.perms;
var uri = NetUtil.newURI(ORIGIN);
var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {});
add_task(async function setup() {
await new Promise(r => {
SpecialPowers.pushPrefEnv(
{
set: [
["dom.security.featurePolicy.enabled", true],
["dom.security.featurePolicy.header.enabled", true],
["dom.security.featurePolicy.webidl.enabled", true],
["permissions.delegation.enabled", true],
],
},
r
);
});
});
// Test that temp blocked permissions in first party affect the third party
// iframe.
add_task(async function testUseTempPermissionsFirstParty() {
await BrowserTestUtils.withNewTab(CROSS_SUBFRAME_PAGE, async function(
browser
) {
SitePermissions.setForPrincipal(
principal,
"geo",
SitePermissions.BLOCK,
SitePermissions.SCOPE_TEMPORARY,
browser
);
// Request a permission.
await ContentTask.spawn(browser, uri.host, async function(host0) {
let frame = content.document.getElementById("frame");
function onMessage(event) {
// Check the result right here because there's no notification
is(event.data, "deny", "Expected deny for third party");
content.window.removeEventListener("message", onMessage);
}
content.window.addEventListener("message", onMessage);
await content.SpecialPowers.spawn(frame, [host0], async function(host) {
const { E10SUtils } = ChromeUtils.import(
"resource://gre/modules/E10SUtils.jsm"
);
E10SUtils.wrapHandlingUserInput(this.content, true, function() {
let frameDoc = this.content.document;
frameDoc.getElementById("geo").click();
});
});
});
SitePermissions.removeFromPrincipal(principal, "geo", browser);
});
});
// Test that persistent permissions in first party affect the third party
// iframe.
add_task(async function testUsePersistentPermissionsFirstParty() {
await BrowserTestUtils.withNewTab(CROSS_SUBFRAME_PAGE, async function(
browser
) {
async function checkPermission(aPermission, aExpect) {
PermissionTestUtils.add(uri, "geo", aPermission);
let waitForPrompt = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popupshown"
);
// Request a permission.
await ContentTask.spawn(
browser,
{ host: uri.host, expect: aExpect },
async function(args) {
let frame = content.document.getElementById("frame");
if (args.expect != "prompt") {
function onMessage(event) {
// Check the result right here because there's no notification
is(
event.data,
args.expect,
"Expected correct permission for third party"
);
content.window.removeEventListener("message", onMessage);
}
content.window.addEventListener("message", onMessage);
}
await content.SpecialPowers.spawn(frame, [args.host], async function(
host
) {
const { E10SUtils } = ChromeUtils.import(
"resource://gre/modules/E10SUtils.jsm"
);
E10SUtils.wrapHandlingUserInput(this.content, true, function() {
let frameDoc = this.content.document;
frameDoc.getElementById("geo").click();
});
});
}
);
if (aExpect == PromptResult.PROMPT) {
await waitForPrompt;
// Notification is shown, check label and deny to clean
let popuphidden = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuphidden"
);
let notification = PopupNotifications.panel.firstElementChild;
// Check the label of the notificaiton should be the first party
is(
PopupNotifications.getNotification("geolocation").options.name,
uri.host,
"Use first party's origin"
);
EventUtils.synthesizeMouseAtCenter(notification.secondaryButton, {});
await popuphidden;
SitePermissions.removeFromPrincipal(null, "geo", browser);
}
PermissionTestUtils.remove(uri, "geo");
}
await checkPermission(Perms.PROMPT_ACTION, PromptResult.PROMPT);
await checkPermission(Perms.DENY_ACTION, PromptResult.DENY);
await checkPermission(Perms.ALLOW_ACTION, PromptResult.ALLOW);
});
});

View File

@ -18,6 +18,15 @@ function requestPush() {
});
}
function requestGeo() {
return navigator.geolocation.getCurrentPosition(() => {
parent.postMessage("allow", "*");
}, () => {
parent.postMessage("deny", "*");
});
}
window.onmessage = function(event) {
switch (event.data) {
case "push":
@ -30,7 +39,7 @@ window.onmessage = function(event) {
<body onkeydown="gKeyDowns++;" onkeypress="gKeyPresses++">
<!-- This page could eventually request permissions from content
and make sure that chrome responds appropriately -->
<button id="geo" onclick="navigator.geolocation.getCurrentPosition(() => {})">Geolocation</button>
<button id="geo" onclick="requestGeo()">Geolocation</button>
<button id="desktop-notification" onclick="Notification.requestPermission()">Notifications</button>
<button id="push" onclick="requestPush()">Push Notifications</button>
<button id="camera" onclick="navigator.mediaDevices.getUserMedia({video: true, fake: true})">Camera</button>

View File

@ -162,6 +162,14 @@ var PermissionPromptPrototype = {
return this.principal.URI.hostPort;
},
/**
* Indicates the type of the permission request from content. This type might
* be different from the permission key used in the permissions database.
*/
get type() {
return undefined;
},
/**
* If the nsIPermissionManager is being queried and written
* to for this permission request, set this to the key to be
@ -711,6 +719,10 @@ var PermissionPromptForRequestPrototype = {
},
get principal() {
if (Services.prefs.getBoolPref("permissions.delegate.enable", false)) {
let request = this.request.QueryInterface(Ci.nsIContentPermissionRequest);
return request.getDelegatePrincipal(this.type);
}
return this.request.principal;
},
@ -739,6 +751,10 @@ function GeolocationPermissionPrompt(request) {
GeolocationPermissionPrompt.prototype = {
__proto__: PermissionPromptForRequestPrototype,
get type() {
return "geo";
},
get permissionKey() {
return "geo";
},
@ -888,6 +904,10 @@ function DesktopNotificationPermissionPrompt(request) {
DesktopNotificationPermissionPrompt.prototype = {
__proto__: PermissionPromptForRequestPrototype,
get type() {
return "desktop-notification";
},
get permissionKey() {
return "desktop-notification";
},
@ -991,6 +1011,10 @@ function PersistentStoragePermissionPrompt(request) {
PersistentStoragePermissionPrompt.prototype = {
__proto__: PermissionPromptForRequestPrototype,
get type() {
return "persistent-storage";
},
get permissionKey() {
return "persistent-storage";
},
@ -1079,6 +1103,10 @@ function MIDIPermissionPrompt(request) {
MIDIPermissionPrompt.prototype = {
__proto__: PermissionPromptForRequestPrototype,
get type() {
return "midi";
},
get permissionKey() {
return this.permName;
},
@ -1171,6 +1199,10 @@ StorageAccessPermissionPrompt.prototype = {
return false;
},
get type() {
return "storage-access";
},
get permissionKey() {
// Make sure this name is unique per each third-party tracker
return "storage-access-" + this.principal.origin;

View File

@ -85,13 +85,14 @@ function makeMockPermissionRequest(browser) {
};
let types = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
types.appendElement(type);
let principal = browser.contentPrincipal;
let result = {
types,
documentDOMContentLoadedTimestamp: 0,
isHandlingUserInput: false,
userHadInteractedWithDocument: false,
principal: browser.contentPrincipal,
topLevelPrincipal: browser.contentPrincipal,
principal,
topLevelPrincipal: principal,
requester: null,
_cancelled: false,
cancel() {
@ -101,6 +102,9 @@ function makeMockPermissionRequest(browser) {
allow() {
this._allowed = true;
},
getDelegatePrincipal(aType) {
return principal;
},
QueryInterface: ChromeUtils.generateQI([Ci.nsIContentPermissionRequest]),
};

View File

@ -35,7 +35,7 @@
using mozilla::Unused; // <snicker>
using namespace mozilla::dom;
using namespace mozilla;
using DelegateInfo = PermissionDelegateHandler::PermissionDelegateInfo;
#define kVisibilityChange "visibilitychange"
class VisibilityChangeListener final : public nsIDOMEventListener {
@ -581,9 +581,21 @@ ContentPermissionRequestBase::GetPrincipal(
return NS_OK;
}
NS_IMETHODIMP
ContentPermissionRequestBase::GetDelegatePrincipal(
const nsACString& aType, nsIPrincipal** aRequestingPrincipal) {
return PermissionDelegateHandler::GetDelegatePrincipal(aType, this,
aRequestingPrincipal);
}
NS_IMETHODIMP
ContentPermissionRequestBase::GetTopLevelPrincipal(
nsIPrincipal** aRequestingPrincipal) {
if (!mTopLevelPrincipal) {
*aRequestingPrincipal = nullptr;
return NS_OK;
}
NS_IF_ADDREF(*aRequestingPrincipal = mTopLevelPrincipal);
return NS_OK;
}
@ -911,10 +923,27 @@ nsContentPermissionRequestProxy::GetTopLevelPrincipal(
return NS_ERROR_FAILURE;
}
if (!mParent->mTopLevelPrincipal) {
*aRequestingPrincipal = nullptr;
return NS_OK;
}
NS_ADDREF(*aRequestingPrincipal = mParent->mTopLevelPrincipal);
return NS_OK;
}
NS_IMETHODIMP
nsContentPermissionRequestProxy::GetDelegatePrincipal(
const nsACString& aType, nsIPrincipal** aRequestingPrincipal) {
NS_ENSURE_ARG_POINTER(aRequestingPrincipal);
if (mParent == nullptr) {
return NS_ERROR_FAILURE;
}
return PermissionDelegateHandler::GetDelegatePrincipal(aType, this,
aRequestingPrincipal);
}
NS_IMETHODIMP
nsContentPermissionRequestProxy::GetElement(Element** aRequestingElement) {
NS_ENSURE_ARG_POINTER(aRequestingElement);

View File

@ -120,6 +120,8 @@ class ContentPermissionRequestBase : public nsIContentPermissionRequest {
NS_IMETHOD GetTypes(nsIArray** aTypes) override;
NS_IMETHOD GetPrincipal(nsIPrincipal** aPrincipal) override;
NS_IMETHOD GetDelegatePrincipal(const nsACString& aType,
nsIPrincipal** aPrincipal) override;
NS_IMETHOD GetTopLevelPrincipal(nsIPrincipal** aTopLevelPrincipal) override;
NS_IMETHOD GetWindow(mozIDOMWindow** aWindow) override;
NS_IMETHOD GetElement(mozilla::dom::Element** aElement) override;

View File

@ -9,7 +9,6 @@
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/CycleCollectedJSContext.h" // for nsAutoMicroTask
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/FeaturePolicyUtils.h"
#include "mozilla/dom/PermissionMessageUtils.h"
#include "mozilla/dom/GeolocationPositionError.h"
#include "mozilla/dom/GeolocationPositionErrorBinding.h"
@ -984,21 +983,6 @@ bool Geolocation::ShouldBlockInsecureRequests() const {
return false;
}
bool Geolocation::FeaturePolicyBlocked() const {
nsCOMPtr<nsPIDOMWindowInner> win = do_QueryReferent(mOwner);
if (!win) {
return true;
}
nsCOMPtr<Document> doc = win->GetExtantDoc();
if (!doc) {
return false;
}
return FeaturePolicyUtils::IsFeatureAllowed(doc,
NS_LITERAL_STRING("geolocation"));
}
bool Geolocation::ClearPendingRequest(nsGeolocationRequest* aRequest) {
if (aRequest->IsWatch() && this->IsAlreadyCleared(aRequest)) {
this->NotifyAllowedRequest(aRequest);
@ -1051,7 +1035,7 @@ nsresult Geolocation::GetCurrentPosition(GeoPositionCallback callback,
static_cast<uint8_t>(mProtocolType), target);
if (!StaticPrefs::geo_enabled() || ShouldBlockInsecureRequests() ||
!FeaturePolicyBlocked()) {
!request->CheckPermissionDelegate()) {
request->RequestDelayedTask(target,
nsGeolocationRequest::DelayedTaskType::Deny);
return NS_OK;
@ -1124,7 +1108,7 @@ int32_t Geolocation::WatchPosition(GeoPositionCallback aCallback,
watchId);
if (!StaticPrefs::geo_enabled() || ShouldBlockInsecureRequests() ||
!FeaturePolicyBlocked()) {
!request->CheckPermissionDelegate()) {
request->RequestDelayedTask(target,
nsGeolocationRequest::DelayedTaskType::Deny);
return watchId;

View File

@ -205,10 +205,6 @@ class Geolocation final : public nsIGeolocationUpdate, public nsWrapperCache {
// within a context that is not secure.
bool ShouldBlockInsecureRequests() const;
// Return whather the Feature 'geolocation' is blocked by FeaturePolicy
// directive.
bool FeaturePolicyBlocked() const;
// Two callback arrays. The first |mPendingCallbacks| holds objects for only
// one callback and then they are released/removed from the array. The second
// |mWatchingCallbacks| holds objects until the object is explictly removed or

View File

@ -98,6 +98,15 @@ interface nsIContentPermissionRequest : nsISupports {
*/
readonly attribute nsIContentPermissionRequester requester;
/*
* Get delegate principal of the permission request. This will return nullptr,
* or request's principal or top level principal based on the delegate policy
* will be applied for a given type.
*
* @param aType the permission type to get
*/
nsIPrincipal getDelegatePrincipal(in ACString aType);
/**
* allow or cancel the request
*/

View File

@ -4622,8 +4622,13 @@ ContentParent::AllocPContentPermissionRequestParent(
return nullptr;
}
nsIPrincipal* topPrincipal = aTopLevelPrincipal;
if (!topPrincipal) {
nsCOMPtr<nsIPrincipal> principal = tp->GetContentPrincipal();
topPrincipal = principal;
}
return nsContentPermissionUtils::CreateContentPermissionRequestParent(
aRequests, tp->GetOwnerElement(), aPrincipal, aTopLevelPrincipal,
aRequests, tp->GetOwnerElement(), aPrincipal, topPrincipal,
aIsHandlingUserInput, aDocumentHasUserInput, aPageLoadTimestamp, aTabId);
}

View File

@ -32,6 +32,11 @@ static const DelegateInfo sPermissionsMap[] = {
DelegatePolicy::ePersistDeniedCrossOrigin},
{"persistent-storage", nullptr, DelegatePolicy::ePersistDeniedCrossOrigin},
{"vibration", nullptr, DelegatePolicy::ePersistDeniedCrossOrigin},
{"midi", nullptr, DelegatePolicy::eDelegateUseIframeOrigin},
{"storage-access", nullptr, DelegatePolicy::eDelegateUseIframeOrigin},
{"camera", u"camera", DelegatePolicy::eDelegateUseFeaturePolicy},
{"microphone", u"microphone", DelegatePolicy::eDelegateUseFeaturePolicy},
{"screen", u"display-capture", DelegatePolicy::eDelegateUseFeaturePolicy},
};
NS_IMPL_CYCLE_COLLECTION(PermissionDelegateHandler)
@ -47,8 +52,9 @@ PermissionDelegateHandler::PermissionDelegateHandler(dom::Document* aDocument)
MOZ_ASSERT(aDocument);
}
/* static */
const DelegateInfo* PermissionDelegateHandler::GetPermissionDelegateInfo(
const nsAString& aPermissionName) const {
const nsAString& aPermissionName) {
nsAutoString lowerContent(aPermissionName);
ToLowerCase(lowerContent);
@ -61,6 +67,32 @@ const DelegateInfo* PermissionDelegateHandler::GetPermissionDelegateInfo(
return nullptr;
}
/* 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 &&
StaticPrefs::dom_security_featurePolicy_enabled())) {
return aRequest->GetTopLevelPrincipal(aResult);
}
return aRequest->GetPrincipal(aResult);
}
bool PermissionDelegateHandler::Initialize() {
MOZ_ASSERT(mDocument);
@ -86,14 +118,21 @@ static bool IsTopWindowContent(Document* aDocument) {
return browsingContext && browsingContext->IsTopContent();
}
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) {
MOZ_ASSERT(mDocument);
if (!StaticPrefs::permissions_delegation_enable()) {
return true;
}
// System principal should have right to make permission request
if (mPrincipal->IsSystemPrincipal()) {
return true;
@ -101,21 +140,12 @@ bool PermissionDelegateHandler::HasPermissionDelegated(
const DelegateInfo* info =
GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType));
// If the type is not in the supported list, auto denied
if (!info) {
if (!info || !HasFeaturePolicyAllowed(info)) {
return false;
}
if (info->mPolicy == DelegatePolicy::eDelegateUseFeaturePolicy &&
info->mFeatureName) {
nsAutoString featureName(info->mFeatureName);
// Default allowlist for a feature used in permissions delegate should be
// set to eSelf, to ensure that permission is denied by default and only
// have the opportunity to request permission with allow attribute.
if (!FeaturePolicyUtils::IsFeatureAllowed(mDocument, featureName)) {
return false;
}
if (!StaticPrefs::permissions_delegation_enabled()) {
return true;
}
if (info->mPolicy == DelegatePolicy::ePersistDeniedCrossOrigin &&
@ -137,37 +167,23 @@ nsresult PermissionDelegateHandler::GetPermission(const nsACString& aType,
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_enable()) {
if (!StaticPrefs::permissions_delegation_enabled()) {
return (mPermissionManager->*testPermission)(mPrincipal, aType,
aPermission);
}
const DelegateInfo* info =
GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType));
// If the type is not in the supported list, auto denied
if (!info) {
*aPermission = nsIPermissionManager::DENY_ACTION;
return NS_OK;
}
if (info->mPolicy == DelegatePolicy::eDelegateUseFeaturePolicy &&
info->mFeatureName) {
nsAutoString featureName(info->mFeatureName);
// Default allowlist for a feature used in permissions delegate should be
// set to eSelf, to ensure that permission is denied by default and only
// have the opportunity to request permission with allow attribute.
if (!FeaturePolicyUtils::IsFeatureAllowed(mDocument, featureName)) {
*aPermission = nsIPermissionManager::DENY_ACTION;
return NS_OK;
}
}
if (info->mPolicy == DelegatePolicy::ePersistDeniedCrossOrigin &&
!IsTopWindowContent(mDocument) &&
!mPrincipal->Subsumes(mTopLevelPrincipal)) {

View File

@ -115,15 +115,40 @@ class PermissionDelegateHandler final : nsISupports {
*/
void DropDocumentReference() { mDocument = nullptr; }
private:
virtual ~PermissionDelegateHandler() = default;
/*
* Helper function to return the delegate info value for aPermissionName.
* @param aPermissionName the permission name to get
*/
const PermissionDelegateInfo* GetPermissionDelegateInfo(
const nsAString& aPermissionName) const;
static const PermissionDelegateInfo* GetPermissionDelegateInfo(
const nsAString& aPermissionName);
/*
* Helper function to return the delegate principal. This will return nullptr,
* or request's principal or top level principal based on the delegate policy
* will be applied for a given type.
* We use this function when prompting, no need to perform permission check
* (deny/allow).
*
* @param aType the permission type to get
* @param aRequest The request which the principal is get from.
* @param aResult out argument which will be a principal that we
* will return from this function.
*/
static nsresult GetDelegatePrincipal(const nsACString& aType,
nsIContentPermissionRequest* aRequest,
nsIPrincipal** aResult);
private:
virtual ~PermissionDelegateHandler() = default;
/*
* Check whether the permission is blocked by FeaturePolicy directive.
* Default allowlist for a featureName of permission used in permissions
* delegate should be set to eSelf, to ensure that permission is denied by
* default and only have the opportunity to request permission with allow
* attribute.
*/
bool HasFeaturePolicyAllowed(const PermissionDelegateInfo* info) const;
// A weak pointer to our document. Nulled out by DropDocumentReference.
mozilla::dom::Document* mDocument;