Bug 1560570- FeaturePolicy should be considered when permissions.query() is called r=baku,johannh

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Thomas Nguyen 2019-09-21 08:38:26 +00:00
Родитель ced80473dd
Коммит cf867199bc
13 изменённых файлов: 598 добавлений и 7 удалений

Просмотреть файл

@ -83,6 +83,7 @@
#include "nsINSSErrorsService.h"
#include "nsISocketProvider.h"
#include "nsISiteSecurityService.h"
#include "PermissionDelegateHandler.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/BasicEvents.h"
@ -1906,6 +1907,10 @@ Document::~Document() {
mXULPersist->DropDocumentReference();
}
if (mPermissionDelegateHandler) {
mPermissionDelegateHandler->DropDocumentReference();
}
delete mHeaderData;
mPendingTitleChangeEvent.Revoke();
@ -15366,6 +15371,14 @@ void Document::AddResizeObserver(ResizeObserver* aResizeObserver) {
mResizeObserverController->AddResizeObserver(aResizeObserver);
}
PermissionDelegateHandler* Document::GetPermissionDelegateHandler() {
if (!mPermissionDelegateHandler) {
mPermissionDelegateHandler =
mozilla::MakeAndAddRef<PermissionDelegateHandler>(this);
}
return mPermissionDelegateHandler;
}
void Document::ScheduleResizeObserversNotification() const {
if (!mResizeObserverController) {
return;

Просмотреть файл

@ -134,6 +134,7 @@ class nsIGlobalObject;
class nsIXULWindow;
class nsXULPrototypeDocument;
class nsXULPrototypeElement;
class PermissionDelegateHandler;
struct nsFont;
namespace mozilla {
@ -3970,6 +3971,9 @@ class Document : public nsINode,
void AddResizeObserver(ResizeObserver* aResizeObserver);
void ScheduleResizeObserversNotification() const;
// Getter for PermissionDelegateHandler. Performs lazy initialization.
PermissionDelegateHandler* GetPermissionDelegateHandler();
/**
* Localization
*
@ -4596,6 +4600,10 @@ class Document : public nsINode,
UniquePtr<ResizeObserverController> mResizeObserverController;
// Permission Delegate Handler, lazily-initialized in
// PermissionDelegateHandler
RefPtr<PermissionDelegateHandler> mPermissionDelegateHandler;
// True if BIDI is enabled.
bool mBidiEnabled : 1;
// True if we may need to recompute the language prefs for this document.

Просмотреть файл

@ -12,6 +12,7 @@
#include "PermissionObserver.h"
#include "PermissionUtils.h"
#include "nsPermission.h"
#include "PermissionDelegateHandler.h"
namespace mozilla {
namespace dom {
@ -64,19 +65,22 @@ JSObject* PermissionStatus::WrapObject(JSContext* aCx,
}
nsresult PermissionStatus::UpdateState() {
nsCOMPtr<nsIPermissionManager> permMgr = services::GetPermissionManager();
if (NS_WARN_IF(!permMgr)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
if (NS_WARN_IF(!window)) {
return NS_ERROR_FAILURE;
}
RefPtr<Document> document = window->GetExtantDoc();
if (NS_WARN_IF(!document)) {
return NS_ERROR_FAILURE;
}
uint32_t action = nsIPermissionManager::DENY_ACTION;
nsresult rv = permMgr->TestPermissionFromWindow(
window, PermissionNameToType(mName), &action);
PermissionDelegateHandler* permissionHandler =
document->GetPermissionDelegateHandler();
nsresult rv = permissionHandler->GetPermissionForPermissionsAPI(
PermissionNameToType(mName), &action);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}

Просмотреть файл

@ -1,5 +1,10 @@
[DEFAULT]
support-files =
file_empty.html
prefs =
dom.security.featurePolicy.enabled=true
dom.security.featurePolicy.header.enabled=true
dom.security.featurePolicy.webidl.enabled=true
[test_cross_origin_iframe.html]
[test_permissions_api.html]

Просмотреть файл

@ -0,0 +1,239 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test for Permissions API</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test"></pre>
<script type="application/javascript">
/*globals SpecialPowers, SimpleTest, is, ok, */
'use strict';
function setPermission(type, allow) {
return new Promise(resolve => {
SpecialPowers.popPermissions(() => {
SpecialPowers.pushPermissions(
[{ type, allow, context: document }],
resolve
);
});
});
}
function checkPermission(aWindow, aExpectedState, aName) {
return SpecialPowers.wrap(aWindow).navigator.permissions
.query({ name: aName })
.then(
result => is(SpecialPowers.wrap(result).state, aExpectedState, `correct state for '${aName}'`),
() => ok(false, `query should not have rejected for '${aName}'`)
);
}
function createIframe(aId, aAllow) {
return new Promise((resolve) => {
const iframe = document.createElement('iframe');
iframe.id = aId;
iframe.src = 'https://example.org/tests/dom/permission/tests/file_empty.html';
if (aAllow) {
iframe.allow = aAllow;
}
iframe.onload = () => resolve(iframe.contentWindow);
document.body.appendChild(iframe);
});
}
function removeIframe(aId) {
return new Promise((resolve) => {
document.body.removeChild(document.getElementById(aId));
resolve();
});
}
const {
UNKNOWN_ACTION,
PROMPT_ACTION,
ALLOW_ACTION,
DENY_ACTION
} = SpecialPowers.Ci.nsIPermissionManager;
const tests = [
{
id: 'query navigation top unknown',
top: UNKNOWN_ACTION,
name: 'geolocation',
type: 'geo',
expected: 'denied',
},
{
id: 'query notifications top unknown',
top: UNKNOWN_ACTION,
name: 'notifications',
type: 'desktop-notification',
expected: 'denied',
},
{
id: 'query push top unknown',
top: UNKNOWN_ACTION,
name: 'push',
type: 'desktop-notification',
expected: 'denied',
},
{
id: 'query persistent-storage unknown',
top: UNKNOWN_ACTION,
name: 'persistent-storage',
type: 'persistent-storage',
expected: 'prompt',
},
{
id: 'query navigation top prompt',
top: PROMPT_ACTION,
name: 'geolocation',
type: 'geo',
expected: 'denied',
},
{
id: 'query notifications top prompt',
top: PROMPT_ACTION,
name: 'notifications',
type: 'desktop-notification',
expected: 'denied',
},
{
id: 'query push top prompt',
top: PROMPT_ACTION,
name: 'push',
type: 'desktop-notification',
expected: 'denied',
},
{
id: 'query persistent-storage top prompt',
top: PROMPT_ACTION,
name: 'persistent-storage',
type: 'persistent-storage',
expected: 'prompt',
},
{
id: 'query navigation top denied',
top: DENY_ACTION,
name: 'geolocation',
type: 'geo',
expected: 'denied',
},
{
id: 'query notifications top denied',
top: DENY_ACTION,
name: 'notifications',
type: 'desktop-notification',
expected: 'denied',
},
{
id: 'query push top denied',
top: DENY_ACTION,
name: 'push',
type: 'desktop-notification',
expected: 'denied',
},
{
id: 'query persistent-storage top denied',
top: DENY_ACTION,
name: 'persistent-storage',
type: 'persistent-storage',
expected: 'prompt',
},
{
id: 'query navigation top granted',
top: ALLOW_ACTION,
name: 'geolocation',
type: 'geo',
expected: 'denied',
},
{
id: 'query notifications top granted',
top: ALLOW_ACTION,
name: 'notifications',
type: 'desktop-notification',
expected: 'denied',
},
{
id: 'query push top granted',
top: ALLOW_ACTION,
name: 'push',
type: 'desktop-notification',
expected: 'denied',
},
{
id: 'query persistent-storage top granted',
top: ALLOW_ACTION,
name: 'persistent-storage',
type: 'persistent-storage',
expected: 'prompt',
},
{
id: 'query navigation top denied, iframe has allow attribute',
top: DENY_ACTION,
allow: 'geolocation',
name: 'geolocation',
type: 'geo',
expected: 'denied',
},
{
id: 'query navigation top granted, iframe has allow attribute',
top: ALLOW_ACTION,
allow: 'geolocation',
name: 'geolocation',
type: 'geo',
expected: 'granted',
},
{
id: 'query navigation top prompt, iframe has allow attribute',
top: PROMPT_ACTION,
allow: 'geolocation',
name: 'geolocation',
type: 'geo',
expected: 'prompt',
},
{
id: 'query navigation top unknown, iframe has allow attribute',
top: UNKNOWN_ACTION,
allow: 'geolocation',
name: 'geolocation',
type: 'geo',
expected: 'prompt',
},
];
SimpleTest.waitForExplicitFinish();
async function nextTest() {
if (tests.length == 0) {
SimpleTest.finish();
return;
}
let test = tests.shift();
await setPermission(test.type, test.top)
.then(() => createIframe(test.id, test.allow))
.then(contentWindow => checkPermission(contentWindow, test.expected, test.name))
.then(() => removeIframe(test.id));
SimpleTest.executeSoon(nextTest);
}
SpecialPowers.pushPrefEnv({"set": [
["permissions.delegation.enable", true],
]}).then(nextTest);
</script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,117 @@
/* -*- 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 "nsGlobalWindowInner.h"
#include "PermissionDelegateHandler.h"
#include "nsPIDOMWindow.h"
#include "nsIPermissionManager.h"
#include "nsIPrincipal.h"
#include "mozilla/Preferences.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/FeaturePolicyUtils.h"
using namespace mozilla;
using namespace mozilla::dom;
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
{"geo", u"geolocation", DelegatePolicy::eDelegateUseFeaturePolicy},
{"desktop-notification", nullptr,
DelegatePolicy::ePersistDeniedCrossOrigin},
{"persistent-storage", nullptr, DelegatePolicy::eDelegateUseIframeOrigin},
};
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(nsISupports)
NS_INTERFACE_MAP_END
PermissionDelegateHandler::PermissionDelegateHandler(dom::Document* aDocument)
: mDocument(aDocument) {
MOZ_ASSERT(aDocument);
}
const DelegateInfo* PermissionDelegateHandler::GetPermissionDelegateInfo(
const nsAString& aPermissionName) const {
nsAutoString lowerContent(aPermissionName);
ToLowerCase(lowerContent);
for (const auto& perm : sPermissionsMap) {
if (lowerContent.EqualsASCII(perm.mPermissionName)) {
return &perm;
}
}
return nullptr;
}
nsresult PermissionDelegateHandler::GetPermissionForPermissionsAPI(
const nsACString& aType, uint32_t* aPermission) {
MOZ_ASSERT(mDocument);
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;
}
nsresult rv;
nsCOMPtr<nsIPermissionManager> permMgr =
do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
if (NS_WARN_IF(NS_FAILED(rv))) {
*aPermission = nsIPermissionManager::DENY_ACTION;
return rv;
}
nsCOMPtr<nsIPrincipal> principal = mDocument->NodePrincipal();
if (!Preferences::GetBool("permissions.delegation.enable", false)) {
return permMgr->TestPermissionFromPrincipal(principal, aType, aPermission);
}
if (info->mPolicy == DelegatePolicy::eDelegateUseIframeOrigin) {
return permMgr->TestPermissionFromPrincipal(principal, aType, aPermission);
}
nsPIDOMWindowInner* window = mDocument->GetInnerWindow();
nsGlobalWindowInner* innerWindow = nsGlobalWindowInner::Cast(window);
nsIPrincipal* topPrincipal = innerWindow->GetTopLevelPrincipal();
// Permission is delegated in same origin
if (principal->Subsumes(topPrincipal)) {
return permMgr->TestPermissionFromPrincipal(topPrincipal, aType,
aPermission);
}
if (info->mPolicy == DelegatePolicy::ePersistDeniedCrossOrigin) {
*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;
}
}
return permMgr->TestPermissionFromPrincipal(topPrincipal, aType, aPermission);
}

Просмотреть файл

@ -0,0 +1,103 @@
/* -*- 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/. */
/*
* Permission delegate handler provides a policy of how top-level can
* delegate permission to embedded iframes.
*
* This class includes a mechanism to delegate permission using feature
* policy. Feature policy will assure that only cross-origin iframes which
* have been explicitly granted access will have the opportunity to request
* permission.
*
* For example if an iframe has not been granted access to geolocation by
* Feature Policy, geolocation request from the iframe will be automatically
* denied. if the top-level origin already has access to geolocation and the
* iframe has been granted access to geolocation by Feature Policy, the iframe
* will also have access to geolocation. If the top-level frame did not have
* access to geolocation, and the iframe has been granted access to geolocation
* by Feature Policy, a request from the cross-origin iframe would trigger a
* prompt using of the top-level origin.
*/
#ifndef PermissionDelegateHandler_h__
#define PermissionDelegateHandler_h__
#include "nsISupports.h"
namespace mozilla {
namespace dom {
class Document;
}
} // namespace mozilla
class PermissionDelegateHandler final : nsISupports {
public:
explicit PermissionDelegateHandler(mozilla::dom::Document* aDocument);
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_CLASS(PermissionDelegateHandler)
/*
* Get permission state for permission api with aType, which applied
* permission delegate policy.
*/
nsresult GetPermissionForPermissionsAPI(const nsACString& aType,
uint32_t* aPermission);
enum PermissionDelegatePolicy {
/* Always delegate permission from top level to iframe and the iframe
* should use top level origin to get/set permission.*/
eDelegateUseTopOrigin,
/* Permission is delegated using Feature Policy. Permission is denied by
* default in cross origin iframe and the iframe only could get/set
* permission if there's allow attribute set in iframe. e.g allow =
* "geolocation" */
eDelegateUseFeaturePolicy,
/* Persistent denied permissions in cross origin iframe */
ePersistDeniedCrossOrigin,
/* This is the old behavior of cross origin iframe permission. The
* permission delegation should not have an effect on iframe. The cross
* origin iframe get/set permissions by its origin */
eDelegateUseIframeOrigin,
};
/*
* Indicates matching between Feature Policy and Permissions name defined in
* Permissions Manager, not DOM Permissions API. Permissions API exposed in
* DOM only supports "geo" at the moment but Permissions Manager also supports
* "camera", "microphone".
*/
typedef struct {
const char* mPermissionName;
const char16_t* mFeatureName;
PermissionDelegatePolicy mPolicy;
} PermissionDelegateInfo;
/**
* The loader maintains a weak reference to the document with
* which it is initialized. This call forces the reference to
* be dropped.
*/
void DropDocumentReference() { mDocument = nullptr; }
private:
virtual ~PermissionDelegateHandler() = default;
/*
* Helper function to return the delegate info value for aPermissionName.
*/
const PermissionDelegateInfo* GetPermissionDelegateInfo(
const nsAString& aPermissionName) const;
// A weak pointer to our document. Nulled out by DropDocumentReference.
mozilla::dom::Document* mDocument;
};
#endif // PermissionDelegateHandler_h__

Просмотреть файл

@ -10,10 +10,15 @@ TESTING_JS_MODULES += [
'test/PermissionTestUtils.jsm',
]
EXPORTS += [
'PermissionDelegateHandler.h',
]
UNIFIED_SOURCES += [
'nsContentBlocker.cpp',
'nsPermission.cpp',
'nsPermissionManager.cpp',
'PermissionDelegateHandler.cpp',
]
XPCOM_MANIFESTS += [

Просмотреть файл

@ -6499,6 +6499,15 @@
value: 5000
mirror: always
#---------------------------------------------------------------------------
# Prefs starting with "permissions."
#---------------------------------------------------------------------------
- name: permissions.delegation.enable
type: bool
value: @IS_NIGHTLY_BUILD@
mirror: always
#---------------------------------------------------------------------------
# Prefs starting with "plain_text."
#---------------------------------------------------------------------------

Просмотреть файл

@ -61,6 +61,7 @@ pref_groups = [
'network',
'nglayout',
'page_load',
'permissions',
'plain_text',
'plugins',
'preferences',

Просмотреть файл

@ -0,0 +1 @@
prefs: [permissions.delegation.enable:true, dom.security.featurePolicy.enabled:true, dom.security.featurePolicy.header.enabled:true, dom.security.featurePolicy.webidl.enabled:true]

Просмотреть файл

@ -0,0 +1,11 @@
<script>
'use strict';
Promise.resolve().then(() => navigator.permissions.query({name:'geolocation'}))
.then(permissionStatus => {
window.parent.postMessage({ state: permissionStatus.state }, '*');
}, error => {
window.parent.postMessage({ state: null }, '*');
});
</script>

Просмотреть файл

@ -0,0 +1,75 @@
<!doctype html>
<meta charset=utf-8>
<title>Test permissions query againts feature policy allow attribute</title>
<script src=/resources/testharness.js></script>
<script src=/resources/testharnessreport.js></script>
<div id="log"></div>
<script>
"use strict";
function test_permissions_query(
feature_description, test, src, expect_state, allow_attribute) {
let frame = document.createElement('iframe');
frame.src = src;
if (typeof allow_attribute !== 'undefined') {
frame.allow = allow_attribute;
}
window.addEventListener('message', test.step_func(function handler(evt) {
if (evt.source === frame.contentWindow) {
assert_equals(evt.data.state, expect_state, feature_description);
document.body.removeChild(frame);
window.removeEventListener('message', handler);
test.done();
}
}));
document.body.appendChild(frame);
}
const same_origin_src =
"/permissions/feature-policy-permissions-query.html";
const cross_origin_src =
"https://{{domains[www]}}:{{ports[https][0]}}" + same_origin_src;
async_test(t => {
test_permissions_query(
'navigator.permissions.query("geolocation")',
t,
same_origin_src,
"prompt",
"geolocation"
);
}, 'Permissions.state is "prompt" with allow="geolocation" in same-origin iframes.');
async_test(t => {
test_permissions_query(
'navigator.permissions.query("geolocation")',
t,
cross_origin_src,
"prompt",
"geolocation"
);
}, 'Permissions.state is "prompt" with allow="geolocation" in cross-origin iframes.');
async_test(t => {
test_permissions_query(
'navigator.permissions.query("geolocation")',
t,
same_origin_src,
"prompt"
);
}, 'Permission.state is "prompt" in same-origin iframes.');
async_test(t => {
test_permissions_query(
'navigator.permissions.query("geolocation")',
t,
cross_origin_src,
"denied"
);
}, 'Permission.state is "denied" in cross-origin iframes.');
</script>