Bug 1483631 - Restrict nested permission requests in webrtc with permission delegate r=jib

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Thomas Nguyen 2019-12-04 15:39:14 +00:00
parent fd744cea01
commit a0b817ac65
20 changed files with 935 additions and 121 deletions

View File

@ -284,14 +284,40 @@ function prompt(
// So, what you are looking at here is not a real nsIContentPermissionRequest, but
// something that looks really similar and will be transmitted to webrtcUI.jsm
// for showing the prompt.
// Note that we basically do the permission delegate check in
// nsIContentPermissionRequest, but because webrtc uses their own prompting
// system, we should manually apply the delegate policy here. Permission
// should be delegated using Feature Policy and top principal
const shouldDelegatePermission =
Services.prefs.getBoolPref("permissions.delegation.enabled", false) &&
Services.prefs.getBoolPref("dom.security.featurePolicy.enabled", false);
const origin = shouldDelegatePermission
? aContentWindow.top.document.nodePrincipal.origin
: aContentWindow.document.nodePrincipal.origin;
let secondOrigin = undefined;
if (shouldDelegatePermission) {
const permDelegateHandler = aContentWindow.document.permDelegateHandler.QueryInterface(
Ci.nsIPermissionDelegateHandler
);
if (permDelegateHandler.maybeUnsafePermissionDelegate(requestTypes)) {
// We are going to prompt both first party and third party origin.
// SecondOrigin should be third party
secondOrigin = aContentWindow.document.nodePrincipal.origin;
}
}
let request = {
callID: aCallID,
windowID: aWindowID,
origin: aContentWindow.document.nodePrincipal.origin,
origin,
secondOrigin,
documentURI: aContentWindow.document.documentURI,
secure: aSecure,
isHandlingUserInput: aIsHandlingUserInput,
isThirdPartyOrigin,
shouldDelegatePermission,
requestTypes,
sharingScreen,
sharingAudio,

View File

@ -427,9 +427,9 @@ pref("permissions.postPrompt.animate", true);
#endif
#ifdef NIGHTLY_BUILD
pref("permissions.delegation.enable", true);
pref("permissions.delegation.enabled", true);
#else
pref("permissions.delegation.enable", false);
pref("permissions.delegation.enabled", false);
#endif
// handle links targeting new windows

View File

@ -14,7 +14,7 @@ add_task(async function testNoPermissionPrompt() {
{
set: [
["dom.security.featurePolicy.enabled", true],
["permissions.delegation.enable", true],
["permissions.delegation.enabled", true],
["dom.vibrator.enabled", true],
["dom.security.featurePolicy.header.enabled", true],
["dom.security.featurePolicy.webidl.enabled", true],

View File

@ -3,6 +3,7 @@ support-files =
get_user_media.html
get_user_media_in_frame.html
get_user_media_in_xorigin_frame.html
get_user_media_in_xorigin_frame_ancestor.html
head.js
[browser_devices_get_user_media.js]
@ -13,6 +14,7 @@ skip-if = (os == "linux" && debug) # linux: bug 976544
skip-if = debug # bug 1369731
[browser_devices_get_user_media_in_xorigin_frame.js]
skip-if = debug # bug 1369731
[browser_devices_get_user_media_in_xorigin_frame_chain.js]
[browser_devices_get_user_media_multi_process.js]
skip-if = (debug && os == "win") # bug 1393761
[browser_devices_get_user_media_paused.js]

View File

@ -1,41 +1,175 @@
/* 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/. */
const permissionError =
"error: NotAllowedError: The request is not allowed " +
"by the user agent or the platform in the current context.";
const PromptResult = {
ALLOW: "allow",
DENY: "deny",
PROMPT: "prompt",
};
const Perms = Services.perms;
async function promptNoDelegate(aThirdPartyOrgin) {
// Persistent allowed first party origin
const uri = gBrowser.selectedBrowser.documentURI;
PermissionTestUtils.add(uri, "microphone", Services.perms.ALLOW_ACTION);
PermissionTestUtils.add(uri, "camera", Services.perms.ALLOW_ACTION);
// Check that we get a prompt.
const observerPromise = expectObserverCalled("getUserMedia:request");
const promise = promisePopupNotificationShown("webRTC-shareDevices");
await promiseRequestDevice(true, true, "frame4");
await promise;
await observerPromise;
// The 'Remember this decision' checkbox is hidden.
const notification = PopupNotifications.panel.firstElementChild;
const checkbox = notification.checkbox;
ok(!!checkbox, "checkbox is present");
ok(checkbox.hidden, "checkbox is not visible");
ok(!checkbox.checked, "checkbox not checked");
// Check the label of the notification should be the first party
is(
PopupNotifications.getNotification("webRTC-shareDevices").options.name,
uri.host,
"Use first party's origin"
);
// Check the secondName of the notification should be the third party
is(
PopupNotifications.getNotification("webRTC-shareDevices").options
.secondName,
aThirdPartyOrgin,
"Use third party's origin as secondName"
);
let indicator = promiseIndicatorWindow();
let observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
let observerPromise2 = expectObserverCalled("recording-device-events");
await promiseMessage("ok", () =>
EventUtils.synthesizeMouseAtCenter(notification.button, {})
);
await observerPromise1;
await observerPromise2;
Assert.deepEqual(
await getMediaCaptureState(),
{ audio: true, video: true },
"expected camera and microphone to be shared"
);
await indicator;
await checkSharingUI({ audio: true, video: true });
// Cleanup.
await closeStream(false, "frame4");
PermissionTestUtils.remove(uri, "camera");
PermissionTestUtils.remove(uri, "microphone");
}
async function promptNoDelegateScreenSharing(aThirdPartyOrgin) {
// Persistent allow screen sharing
const uri = gBrowser.selectedBrowser.documentURI;
PermissionTestUtils.add(uri, "screen", Services.perms.ALLOW_ACTION);
const observerPromise = expectObserverCalled("getUserMedia:request");
const promise = promisePopupNotificationShown("webRTC-shareDevices");
await promiseRequestDevice(false, true, "frame4", "screen");
await promise;
await observerPromise;
checkDeviceSelectors(false, false, true);
const notification = PopupNotifications.panel.firstElementChild;
const iconclass = notification.getAttribute("iconclass");
ok(iconclass.includes("screen-icon"), "panel using screen icon");
// The 'Remember this decision' checkbox is hidden.
const checkbox = notification.checkbox;
ok(!!checkbox, "checkbox is present");
ok(checkbox.hidden, "checkbox is not visible");
ok(!checkbox.checked, "checkbox not checked");
// Check the label of the notification should be the first party
is(
PopupNotifications.getNotification("webRTC-shareDevices").options.name,
uri.host,
"Use first party's origin"
);
// Check the secondName of the notification should be the third party
is(
PopupNotifications.getNotification("webRTC-shareDevices").options
.secondName,
aThirdPartyOrgin,
"Use third party's origin as secondName"
);
const menulist = document.getElementById("webRTC-selectWindow-menulist");
const count = menulist.itemCount;
menulist.getItemAtIndex(count - 1).doCommand();
ok(!notification.button.disabled, "Allow button is enabled");
const indicator = promiseIndicatorWindow();
const observerPromise1 = expectObserverCalled("getUserMedia:response:allow");
const observerPromise2 = expectObserverCalled("recording-device-events");
await promiseMessage("ok", () =>
EventUtils.synthesizeMouseAtCenter(notification.button, {})
);
await observerPromise1;
await observerPromise2;
Assert.deepEqual(
await getMediaCaptureState(),
{ screen: "Screen" },
"expected screen to be shared"
);
await indicator;
await checkSharingUI({ screen: "Screen" });
await closeStream(false, "frame4");
PermissionTestUtils.remove(uri, "screen");
}
var gTests = [
{
desc: "'Always Allow' disabled on third party pages",
desc:
"'Always Allow' enabled on third party pages, when origin is explicitly allowed",
run: async function checkNoAlwaysOnThirdParty() {
// Initially set both permissions to 'allow'.
let origin = "https://test1.example.com/";
PermissionTestUtils.add(
origin,
"microphone",
Services.perms.ALLOW_ACTION
);
PermissionTestUtils.add(origin, "camera", Services.perms.ALLOW_ACTION);
// Initially set both permissions to 'prompt'.
const uri = gBrowser.selectedBrowser.documentURI;
PermissionTestUtils.add(uri, "microphone", Services.perms.PROMPT_ACTION);
PermissionTestUtils.add(uri, "camera", Services.perms.PROMPT_ACTION);
// Request devices and expect a prompt despite the saved 'Allow' permission,
// because we're a third party.
let observerPromise = expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
const observerPromise = expectObserverCalled("getUserMedia:request");
const promise = promisePopupNotificationShown("webRTC-shareDevices");
await promiseRequestDevice(true, true, "frame1");
await promise;
await observerPromise;
checkDeviceSelectors(true, true);
// Ensure that the 'Remember this decision' checkbox is absent.
let notification = PopupNotifications.panel.firstElementChild;
let checkbox = notification.checkbox;
// The 'Remember this decision' checkbox is visible.
const notification = PopupNotifications.panel.firstElementChild;
const checkbox = notification.checkbox;
ok(!!checkbox, "checkbox is present");
ok(checkbox.hidden, "checkbox is not visible");
ok(!checkbox.hidden, "checkbox is visible");
ok(!checkbox.checked, "checkbox not checked");
let indicator = promiseIndicatorWindow();
let observerPromise1 = expectObserverCalled(
// Check the label of the notification should be the first party
is(
PopupNotifications.getNotification("webRTC-shareDevices").options.name,
uri.host,
"Use first party's origin"
);
const indicator = promiseIndicatorWindow();
const observerPromise1 = expectObserverCalled(
"getUserMedia:response:allow"
);
let observerPromise2 = expectObserverCalled("recording-device-events");
const observerPromise2 = expectObserverCalled("recording-device-events");
await promiseMessage("ok", () =>
EventUtils.synthesizeMouseAtCenter(notification.button, {})
);
@ -51,38 +185,39 @@ var gTests = [
// Cleanup.
await closeStream(false, "frame1");
PermissionTestUtils.remove(origin, "camera");
PermissionTestUtils.remove(origin, "microphone");
PermissionTestUtils.remove(uri, "camera");
PermissionTestUtils.remove(uri, "microphone");
},
},
{
desc: "'Always Allow' disabled when sharing screen in third party iframes",
desc:
"'Always Allow' disabled when sharing screen in third party iframes, when origin is explicitly allowed",
run: async function checkScreenSharing() {
let observerPromise = expectObserverCalled("getUserMedia:request");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
const observerPromise = expectObserverCalled("getUserMedia:request");
const promise = promisePopupNotificationShown("webRTC-shareDevices");
await promiseRequestDevice(false, true, "frame1", "screen");
await promise;
await observerPromise;
checkDeviceSelectors(false, false, true);
let notification = PopupNotifications.panel.firstElementChild;
let iconclass = notification.getAttribute("iconclass");
const notification = PopupNotifications.panel.firstElementChild;
const iconclass = notification.getAttribute("iconclass");
ok(iconclass.includes("screen-icon"), "panel using screen icon");
// Ensure that the 'Remember this decision' checkbox is absent.
let checkbox = notification.checkbox;
// The 'Remember this decision' checkbox is visible.
const checkbox = notification.checkbox;
ok(!!checkbox, "checkbox is present");
ok(checkbox.hidden, "checkbox is not visible");
ok(!checkbox.hidden, "checkbox is visible");
ok(!checkbox.checked, "checkbox not checked");
let menulist = document.getElementById("webRTC-selectWindow-menulist");
let count = menulist.itemCount;
const menulist = document.getElementById("webRTC-selectWindow-menulist");
const count = menulist.itemCount;
ok(
count >= 4,
"There should be the 'Select Window or Screen' item, a separator and at least one window and one screen"
);
let noWindowOrScreenItem = menulist.getItemAtIndex(0);
const noWindowOrScreenItem = menulist.getItemAtIndex(0);
ok(
noWindowOrScreenItem.hasAttribute("selected"),
"the 'Select Window or Screen' item is selected"
@ -106,11 +241,11 @@ var gTests = [
menulist.getItemAtIndex(count - 1).doCommand();
ok(!notification.button.disabled, "Allow button is enabled");
let indicator = promiseIndicatorWindow();
let observerPromise1 = expectObserverCalled(
const indicator = promiseIndicatorWindow();
const observerPromise1 = expectObserverCalled(
"getUserMedia:response:allow"
);
let observerPromise2 = expectObserverCalled("recording-device-events");
const observerPromise2 = expectObserverCalled("recording-device-events");
await promiseMessage("ok", () =>
EventUtils.synthesizeMouseAtCenter(notification.button, {})
);
@ -127,9 +262,325 @@ var gTests = [
await closeStream(false, "frame1");
},
},
{
desc: "getUserMedia use persistent permissions from first party",
run: async function checkUsePersistentPermissionsFirstParty() {
async function checkPersistentPermission(
aPermission,
aRequestType,
aIframeId,
aExpect
) {
info(
`Test persistent permission ${aPermission} type ${aRequestType} expect ${aExpect}`
);
const uri = gBrowser.selectedBrowser.documentURI;
// Persistent allow/deny for first party uri
PermissionTestUtils.add(uri, aRequestType, aPermission);
let audio = aRequestType == "microphone";
let video = aRequestType == "camera";
const screen = aRequestType == "screen" ? "screen" : undefined;
if (screen) {
audio = false;
video = true;
}
if (aExpect == PromptResult.PROMPT) {
// Check that we get a prompt.
const observerPromise = expectObserverCalled("getUserMedia:request");
const observerPromise1 = expectObserverCalled(
"getUserMedia:response:deny"
);
const observerPromise2 = expectObserverCalled(
"recording-window-ended"
);
const promise = promisePopupNotificationShown("webRTC-shareDevices");
await promiseRequestDevice(audio, video, aIframeId, screen);
await promise;
await observerPromise;
// Check the label of the notification should be the first party
is(
PopupNotifications.getNotification("webRTC-shareDevices").options
.name,
uri.host,
"Use first party's origin"
);
// Deny the request to cleanup...
await promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
await observerPromise1;
await observerPromise2;
let browser = gBrowser.selectedBrowser;
SitePermissions.removeFromPrincipal(null, aRequestType, browser);
} else if (aExpect == PromptResult.ALLOW) {
const observerPromise = expectObserverCalled("getUserMedia:request");
const observerPromise1 = expectObserverCalled(
"getUserMedia:response:allow"
);
const observerPromise2 = expectObserverCalled(
"recording-device-events"
);
const promise = promiseMessage("ok");
await promiseRequestDevice(audio, video, aIframeId, screen);
await promise;
await observerPromise;
await promiseNoPopupNotification("webRTC-shareDevices");
await observerPromise1;
await observerPromise2;
let expected = {};
if (audio) {
expected.audio = audio;
}
if (video) {
expected.video = video;
}
Assert.deepEqual(
await getMediaCaptureState(),
expected,
"expected " + Object.keys(expected).join(" and ") + " to be shared"
);
await closeStream(false, "frame1");
} else if (aExpect == PromptResult.DENY) {
const observerPromise = expectObserverCalled(
"recording-window-ended"
);
const promise = promiseMessage(permissionError);
await promiseRequestDevice(audio, video, aIframeId, screen);
await promise;
await observerPromise;
}
PermissionTestUtils.remove(uri, aRequestType);
}
await checkPersistentPermission(
Perms.PROMPT_ACTION,
"camera",
"frame1",
PromptResult.PROMPT
);
await checkPersistentPermission(
Perms.DENY_ACTION,
"camera",
"frame1",
PromptResult.DENY
);
await checkPersistentPermission(
Perms.ALLOW_ACTION,
"camera",
"frame1",
PromptResult.ALLOW
);
await checkPersistentPermission(
Perms.PROMPT_ACTION,
"microphone",
"frame1",
PromptResult.PROMPT
);
await checkPersistentPermission(
Perms.DENY_ACTION,
"microphone",
"frame1",
PromptResult.DENY
);
await checkPersistentPermission(
Perms.ALLOW_ACTION,
"microphone",
"frame1",
PromptResult.ALLOW
);
await checkPersistentPermission(
Perms.PROMPT_ACTION,
"screen",
"frame1",
PromptResult.PROMPT
);
await checkPersistentPermission(
Perms.DENY_ACTION,
"screen",
"frame1",
PromptResult.DENY
);
// Always prompt screen sharing
await checkPersistentPermission(
Perms.ALLOW_ACTION,
"screen",
"frame1",
PromptResult.PROMPT
);
// Denied by default if allow is not defined
await checkPersistentPermission(
Perms.PROMPT_ACTION,
"camera",
"frame3",
PromptResult.DENY
);
await checkPersistentPermission(
Perms.DENY_ACTION,
"camera",
"frame3",
PromptResult.DENY
);
await checkPersistentPermission(
Perms.ALLOW_ACTION,
"camera",
"frame3",
PromptResult.DENY
);
await checkPersistentPermission(
Perms.PROMPT_ACTION,
"microphone",
"frame3",
PromptResult.DENY
);
await checkPersistentPermission(
Perms.DENY_ACTION,
"microphone",
"frame3",
PromptResult.DENY
);
await checkPersistentPermission(
Perms.ALLOW_ACTION,
"microphone",
"frame3",
PromptResult.DENY
);
await checkPersistentPermission(
Perms.PROMPT_ACTION,
"screen",
"frame3",
PromptResult.DENY
);
await checkPersistentPermission(
Perms.DENY_ACTION,
"screen",
"frame3",
PromptResult.DENY
);
await checkPersistentPermission(
Perms.ALLOW_ACTION,
"screen",
"frame3",
PromptResult.DENY
);
},
},
{
desc: "getUserMedia use temporary blocked permissions from first party",
run: async function checkUseTempPermissionsBlockFirstParty() {
async function checkTempPermission(aRequestType) {
let browser = gBrowser.selectedBrowser;
let observerPromise = expectObserverCalled("getUserMedia:request");
let observerPromise1 = expectObserverCalled(
"getUserMedia:response:deny"
);
let observerPromise2 = expectObserverCalled("recording-window-ended");
let promise = promisePopupNotificationShown("webRTC-shareDevices");
let audio = aRequestType == "microphone";
let video = aRequestType == "camera";
const screen = aRequestType == "screen" ? "screen" : undefined;
if (screen) {
audio = false;
video = true;
}
await promiseRequestDevice(audio, video, null, screen);
await promise;
await observerPromise;
// Temporarily grant/deny from top level
// Only need to check allow and deny temporary permissions
await promiseMessage(permissionError, () => {
activateSecondaryAction(kActionDeny);
});
await observerPromise1;
await observerPromise2;
await checkNotSharing();
observerPromise = expectObserverCalled("getUserMedia:request");
observerPromise1 = expectObserverCalled("getUserMedia:response:deny");
observerPromise2 = expectObserverCalled("recording-window-ended");
promise = promiseMessage(permissionError);
await promiseRequestDevice(audio, video, "frame1", screen);
await promise;
await observerPromise;
await observerPromise1;
await observerPromise2;
SitePermissions.removeFromPrincipal(null, aRequestType, browser);
}
// At the moment we only save temporary deny
await checkTempPermission("camera");
await checkTempPermission("microphone");
await checkTempPermission("screen");
},
},
{
desc:
"Prompt and display both first party and third party origin in maybe unsafe permission delegation",
run: async function checkPromptNoDelegate() {
await promptNoDelegate("test1.example.com");
},
},
{
desc:
"Prompt and display both first party and third party origin when sharing screen in unsafe permission delegation",
run: async function checkPromptNoDelegateScreenSharing() {
await promptNoDelegateScreenSharing("test1.example.com");
},
},
{
desc:
"Change location, prompt and display both first party and third party origin in maybe unsafe permission delegation",
run: async function checkPromptNoDelegateChangeLoxation() {
await promiseChangeLocationFrame(
"frame4",
"https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"
);
await promptNoDelegate("test2.example.com");
},
},
{
desc:
"Change location, prompt and display both first party and third party origin when sharing screen in unsafe permission delegation",
run: async function checkPromptNoDelegateScreenSharingChangeLocation() {
await promiseChangeLocationFrame(
"frame4",
"https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"
);
await promptNoDelegateScreenSharing("test2.example.com");
},
},
];
add_task(async function test() {
await SpecialPowers.pushPrefEnv({
set: [
["permissions.delegation.enabled", true],
["dom.security.featurePolicy.enabled", true],
["dom.security.featurePolicy.header.enabled", true],
["dom.security.featurePolicy.webidl.enabled", true],
],
});
await runTests(gTests, {
relativeURI: "get_user_media_in_xorigin_frame.html",
});

View File

@ -0,0 +1,253 @@
/* 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/. */
const permissionError =
"error: NotAllowedError: The request is not allowed " +
"by the user agent or the platform in the current context.";
const PromptResult = {
ALLOW: "allow",
DENY: "deny",
PROMPT: "prompt",
};
const Perms = Services.perms;
function expectObserverCalledAncestor(aTopic, browsingContext) {
if (!gMultiProcessBrowser) {
return expectObserverCalledInProcess(aTopic);
}
return BrowserTestUtils.contentTopicObserved(browsingContext, aTopic);
}
function enableObserverVerificationAncestor(browsingContext) {
// Skip these checks in single process mode as it isn't worth implementing it.
if (!gMultiProcessBrowser) {
return Promise.resolve();
}
return BrowserTestUtils.startObservingTopics(browsingContext, observerTopics);
}
function disableObserverVerificationAncestor(browsingContextt) {
if (!gMultiProcessBrowser) {
return Promise.resolve();
}
return BrowserTestUtils.stopObservingTopics(
browsingContextt,
observerTopics
).catch(reason => {
ok(false, "Failed " + reason);
});
}
function promiseRequestDeviceAncestor(
aRequestAudio,
aRequestVideo,
aType,
aBrowser,
aBadDevice = false
) {
info("requesting devices");
return SpecialPowers.spawn(
aBrowser,
[{ aRequestAudio, aRequestVideo, aType, aBadDevice }],
async function(args) {
let global = content.wrappedJSObject.document.getElementById("frame4")
.contentWindow;
global.requestDevice(
args.aRequestAudio,
args.aRequestVideo,
args.aType,
args.aBadDevice
);
}
);
}
async function closeStreamAncestor(browser) {
let observerPromises = [];
observerPromises.push(
expectObserverCalledAncestor("recording-device-events", browser)
);
observerPromises.push(
expectObserverCalledAncestor("recording-window-ended", browser)
);
info("closing the stream");
await SpecialPowers.spawn(browser, [], async () => {
let global = content.wrappedJSObject.document.getElementById("frame4")
.contentWindow;
global.closeStream();
});
await Promise.all(observerPromises);
await assertWebRTCIndicatorStatus(null);
}
var gTests = [
{
desc:
"getUserMedia use persistent permissions from first party if third party is explicitly trusted",
skipObserverVerification: true,
run: async function checkPermissionsAncestorChain() {
async function checkPermission(aPermission, aRequestType, aExpect) {
info(
`Test persistent permission ${aPermission} type ${aRequestType} expect ${aExpect}`
);
const uri = gBrowser.selectedBrowser.documentURI;
// Persistent allow/deny for first party uri
PermissionTestUtils.add(uri, aRequestType, aPermission);
let audio = aRequestType == "microphone";
let video = aRequestType == "camera";
const screen = aRequestType == "screen" ? "screen" : undefined;
if (screen) {
audio = false;
video = true;
}
const iframeAncestor = await SpecialPowers.spawn(
gBrowser.selectedBrowser,
[],
() => {
return content.document.getElementById("frameAncestor")
.browsingContext;
}
);
if (aExpect == PromptResult.PROMPT) {
// Check that we get a prompt.
const observerPromise = expectObserverCalledAncestor(
"getUserMedia:request",
iframeAncestor
);
const promise = promisePopupNotificationShown("webRTC-shareDevices");
await promiseRequestDeviceAncestor(
audio,
video,
screen,
iframeAncestor
);
await promise;
await observerPromise;
// Check the label of the notification should be the first party
is(
PopupNotifications.getNotification("webRTC-shareDevices").options
.name,
uri.host,
"Use first party's origin"
);
const observerPromise1 = expectObserverCalledAncestor(
"getUserMedia:response:deny",
iframeAncestor
);
const observerPromise2 = expectObserverCalledAncestor(
"recording-window-ended",
iframeAncestor
);
// Deny the request to cleanup...
activateSecondaryAction(kActionDeny);
await observerPromise1;
await observerPromise2;
let browser = gBrowser.selectedBrowser;
SitePermissions.removeFromPrincipal(null, aRequestType, browser);
} else if (aExpect == PromptResult.ALLOW) {
const observerPromise = expectObserverCalledAncestor(
"getUserMedia:request",
iframeAncestor
);
const observerPromise1 = expectObserverCalledAncestor(
"getUserMedia:response:allow",
iframeAncestor
);
const observerPromise2 = expectObserverCalledAncestor(
"recording-device-events",
iframeAncestor
);
await promiseRequestDeviceAncestor(
audio,
video,
screen,
iframeAncestor
);
await observerPromise;
await promiseNoPopupNotification("webRTC-shareDevices");
await observerPromise1;
await observerPromise2;
let expected = {};
if (audio) {
expected.audio = audio;
}
if (video) {
expected.video = video;
}
Assert.deepEqual(
await getMediaCaptureState(),
expected,
"expected " + Object.keys(expected).join(" and ") + " to be shared"
);
await closeStreamAncestor(iframeAncestor);
} else if (aExpect == PromptResult.DENY) {
const observerPromise = expectObserverCalledAncestor(
"recording-window-ended",
iframeAncestor
);
await promiseRequestDeviceAncestor(
audio,
video,
screen,
iframeAncestor
);
await observerPromise;
}
PermissionTestUtils.remove(uri, aRequestType);
}
await checkPermission(Perms.PROMPT_ACTION, "camera", PromptResult.PROMPT);
await checkPermission(Perms.DENY_ACTION, "camera", PromptResult.DENY);
await checkPermission(Perms.ALLOW_ACTION, "camera", PromptResult.ALLOW);
await checkPermission(
Perms.PROMPT_ACTION,
"microphone",
PromptResult.PROMPT
);
await checkPermission(Perms.DENY_ACTION, "microphone", PromptResult.DENY);
await checkPermission(
Perms.ALLOW_ACTION,
"microphone",
PromptResult.ALLOW
);
await checkPermission(Perms.PROMPT_ACTION, "screen", PromptResult.PROMPT);
await checkPermission(Perms.DENY_ACTION, "screen", PromptResult.DENY);
// Always prompt screen sharing
await checkPermission(Perms.ALLOW_ACTION, "screen", PromptResult.PROMPT);
},
},
];
add_task(async function test() {
await SpecialPowers.pushPrefEnv({
set: [
["permissions.delegation.enabled", true],
["dom.security.featurePolicy.enabled", true],
["dom.security.featurePolicy.header.enabled", true],
["dom.security.featurePolicy.webidl.enabled", true],
],
});
await runTests(gTests, {
relativeURI: "get_user_media_in_xorigin_frame_ancestor.html",
});
});

View File

@ -20,7 +20,7 @@ try {
function message(m) {
// eslint-disable-next-line no-unsanitized/property
document.getElementById("message").innerHTML = m;
window.parent.postMessage(m, "*");
parent.postMessage(m, "*");
}
var gStreams = [];
@ -51,7 +51,7 @@ function requestDevice(aAudio, aVideo, aShare, aBadDevice = false) {
opts.fake = true;
}
window.navigator.mediaDevices.getUserMedia(opts)
navigator.mediaDevices.getUserMedia(opts)
.then(stream => {
gStreams.push(stream);
message("ok");

View File

@ -57,5 +57,7 @@ function closeStream() {
</script>
<iframe id="frame1" allow="camera;microphone;display-capture" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
<iframe id="frame2" allow="camera;microphone" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
<iframe id="frame3" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
<iframe id="frame4" allow="camera *;microphone *;display-capture *" src="https://test1.example.com/browser/browser/base/content/test/webrtc/get_user_media.html"></iframe>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Permissions Test</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
</head>
<body>
<iframe id="frameAncestor"
src="https://test2.example.com/browser/browser/base/content/test/webrtc/get_user_media_in_xorigin_frame.html"
allow="camera 'src' https://test1.example.com;microphone 'src' https://test1.example.com;display-capture 'src' https://test1.example.com"></iframe>
</body>
</html>

View File

@ -665,6 +665,29 @@ function promiseReloadFrame(aFrameId) {
);
}
function promiseChangeLocationFrame(aFrameId, aNewLocation) {
return SpecialPowers.spawn(
gBrowser.selectedBrowser.browsingContext,
[{ aFrameId, aNewLocation }],
async function(args) {
let frame = content.wrappedJSObject.document.getElementById(
args.aFrameId
);
return new Promise(resolve => {
function listener() {
frame.removeEventListener("load", listener, true);
resolve();
}
frame.addEventListener("load", listener, true);
content.wrappedJSObject.document.getElementById(
args.aFrameId
).contentWindow.location = args.aNewLocation;
});
}
);
}
async function openNewTestTab(leaf = "get_user_media.html") {
let rootDir = getRootDirectory(gTestPath);
rootDir = rootDir.replace(

View File

@ -726,6 +726,24 @@ getUserMedia.shareCameraAndAudioCapture2.message = Will you allow %S to use your
getUserMedia.shareScreenAndMicrophone3.message = Will you allow %S to use your microphone and see your screen?
getUserMedia.shareScreenAndAudioCapture3.message = Will you allow %S to listen to this tabs audio and see your screen?
getUserMedia.shareAudioCapture2.message = Will you allow %S to listen to this tabs audio?
# LOCALIZATION NOTE (getUserMedia.shareCameraUnsafeDelegation.message,
# getUserMedia.shareMicrophoneUnsafeDelegation.message,
# getUserMedia.shareScreenUnsafeDelegation.message,
# getUserMedia.shareCameraAndMicrophoneUnsafeDelegation.message,
# getUserMedia.shareCameraAndAudioCaptureUnsafeDelegation.message,
# getUserMedia.shareScreenAndMicrophoneUnsafeDelegation.message,
# getUserMedia.shareScreenAndAudioCaptureUnsafeDelegation.message,
# %1$S is the first party origin.
# %2$S is the third party origin.
getUserMedia.shareCameraUnsafeDelegation.message = Will you allow %1$S to give %2$S access to your camera?
getUserMedia.shareMicrophoneUnsafeDelegations.message = Will you allow %1$S to give %2$S access to your microphone?
getUserMedia.shareScreenUnsafeDelegation.message = Will you allow %1$S to give %2$S permission to see your screen?
getUserMedia.shareCameraAndMicrophoneUnsafeDelegation.message = Will you allow %1$S to give %2$S access to your camera and microphone?
getUserMedia.shareCameraAndAudioCaptureUnsafeDelegation.message = Will you allow %1$S to give %2$S access to your camera and listen to this tabs audio?
getUserMedia.shareScreenAndMicrophoneUnsafeDelegation.message = Will you allow %1$S to give %2$S access to your microphone and see your screen?
getUserMedia.shareScreenAndAudioCaptureUnsafeDelegation.message = Will you allow %1$S to give %2$S permission to listen to this tabs audio and see your screen?
# LOCALIZATION NOTE (getUserMedia.shareScreenWarning.message): NB: inserted via innerHTML, so please don't use <, > or & in this string.
# %S will be the 'learn more' link
getUserMedia.shareScreenWarning.message = Only share screens with sites you trust. Sharing can allow deceptive sites to browse as you and steal your private data. %S

View File

@ -529,7 +529,7 @@ function checkRequestAllowed(aRequest, aPrincipal, aBrowser) {
if (videoDevices.length && sharingScreen) {
camAllowed = false;
}
if (aRequest.isThirdPartyOrigin) {
if (aRequest.isThirdPartyOrigin && !aRequest.shouldDelegatePermission) {
camAllowed = false;
micAllowed = false;
}
@ -643,7 +643,10 @@ function prompt(aBrowser, aRequest) {
// If the request comes from a popup, we don't want to show the prompt,
// but we do want to allow the request if the user previously gave permission.
if (isPopup) {
if (!checkRequestAllowed(aRequest, principal, aBrowser)) {
if (
aRequest.secondOrigin ||
!checkRequestAllowed(aRequest, principal, aBrowser)
) {
denyRequest(aBrowser, aRequest);
}
return;
@ -680,20 +683,39 @@ function prompt(aBrowser, aRequest) {
// "includes()". This allows the rotation of string identifiers. We list the
// full identifiers here so they can be cross-referenced more easily.
let joinedRequestTypes = requestTypes.join("And");
let stringId = [
// Individual request types first.
"getUserMedia.shareCamera2.message",
"getUserMedia.shareMicrophone2.message",
"getUserMedia.shareScreen3.message",
"getUserMedia.shareAudioCapture2.message",
// Combinations of the above request types last.
"getUserMedia.shareCameraAndMicrophone2.message",
"getUserMedia.shareCameraAndAudioCapture2.message",
"getUserMedia.shareScreenAndMicrophone3.message",
"getUserMedia.shareScreenAndAudioCapture3.message",
].find(id => id.includes(joinedRequestTypes));
let requestMessages;
if (aRequest.secondOrigin) {
requestMessages = [
// Individual request types first.
"getUserMedia.shareCameraUnsafeDelegation.message",
"getUserMedia.shareMicrophoneUnsafeDelegation.message",
"getUserMedia.shareScreenUnsafeDelegation.message",
"getUserMedia.shareAudioCaptureUnsafeDelegation.message",
// Combinations of the above request types last.
"getUserMedia.shareCameraAndMicrophoneUnsafeDelegation.message",
"getUserMedia.shareCameraAndAudioCaptureUnsafeDelegation.message",
"getUserMedia.shareScreenAndMicrophoneUnsafeDelegation.message",
"getUserMedia.shareScreenAndAudioCaptureUnsafeDelegation.message",
];
} else {
requestMessages = [
// Individual request types first.
"getUserMedia.shareCamera2.message",
"getUserMedia.shareMicrophone2.message",
"getUserMedia.shareScreen3.message",
"getUserMedia.shareAudioCapture2.message",
// Combinations of the above request types last.
"getUserMedia.shareCameraAndMicrophone2.message",
"getUserMedia.shareCameraAndAudioCapture2.me ssage",
"getUserMedia.shareScreenAndMicrophone3.message",
"getUserMedia.shareScreenAndAudioCapture3.message",
];
}
let message = stringBundle.getFormattedString(stringId, ["<>"], 1);
let stringId = requestMessages.find(id => id.includes(joinedRequestTypes));
let message = aRequest.secondOrigin
? stringBundle.getFormattedString(stringId, ["<>", "{}"])
: stringBundle.getFormattedString(stringId, ["<>"]);
let notification; // Used by action callbacks.
let mainAction = {
@ -787,7 +809,12 @@ function prompt(aBrowser, aRequest) {
// it is handled synchronously before we add the notification.
// Handling of ALLOW is delayed until the popupshowing event,
// to avoid granting permissions automatically to background tabs.
if (checkRequestAllowed(aRequest, principal, aBrowser)) {
// If we have a secondOrigin, it means this request is lacking explicit
// trust, and we should always prompt even in with persistent permission.
if (
!aRequest.secondOrigin &&
checkRequestAllowed(aRequest, principal, aBrowser)
) {
this.remove();
return true;
}
@ -1181,11 +1208,28 @@ function prompt(aBrowser, aRequest) {
},
};
// Don't offer "always remember" action in PB mode or from third party
if (
!PrivateBrowsingUtils.isBrowserPrivate(aBrowser) &&
!aRequest.isThirdPartyOrigin
) {
function shouldShowAlwaysRemember() {
// Don't offer "always remember" action in PB mode
if (PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
return false;
}
// Don't offer "always remember" action in third party with no permission
// delegation
if (aRequest.isThirdPartyOrigin && !aRequest.shouldDelegatePermission) {
return false;
}
// Don't offer "always remember" action in maybe unsafe permission
// delegation
if (aRequest.shouldDelegatePermission && aRequest.secondOrigin) {
return false;
}
return true;
}
if (shouldShowAlwaysRemember()) {
// Disable the permanent 'Allow' action if the connection isn't secure, or for
// screen/audio sharing (because we can't guess which window the user wants to
// share without prompting).
@ -1234,6 +1278,10 @@ function prompt(aBrowser, aRequest) {
}
options.popupIconClass = iconClass + "-icon";
if (aRequest.secondOrigin) {
options.secondName = getHostOrExtensionName(null, aRequest.secondOrigin);
}
notification = chromeDoc.defaultView.PopupNotifications.show(
aBrowser,
"webRTC-shareDevices",

View File

@ -63,6 +63,7 @@
#include "nsProxyRelease.h"
#include "nss.h"
#include "nsVariant.h"
#include "PermissionDelegateHandler.h"
#include "pk11pub.h"
#include "ThreadSafeRefcountingWithMainThreadDestruction.h"
#include "VideoStreamTrack.h"
@ -2543,55 +2544,41 @@ RefPtr<MediaManager::StreamPromise> MediaManager::GetUserMedia(
if (!privileged) {
// Check if this site has had persistent permissions denied.
nsCOMPtr<nsIPermissionManager> permManager =
do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
RefPtr<PermissionDelegateHandler> permDelegate =
doc->GetPermissionDelegateHandler();
MOZ_RELEASE_ASSERT(permDelegate);
uint32_t audioPerm = nsIPermissionManager::UNKNOWN_ACTION;
if (IsOn(c.mAudio)) {
if (audioType == MediaSourceEnum::Microphone) {
if (Preferences::GetBool("media.getusermedia.microphone.deny", false) ||
!FeaturePolicyUtils::IsFeatureAllowed(
doc, NS_LITERAL_STRING("microphone"))) {
if (Preferences::GetBool("media.getusermedia.microphone.deny", false)) {
audioPerm = nsIPermissionManager::DENY_ACTION;
} else {
rv = permManager->TestExactPermissionFromPrincipal(
principal, NS_LITERAL_CSTRING("microphone"), &audioPerm);
rv = permDelegate->GetPermission(NS_LITERAL_CSTRING("microphone"),
&audioPerm, true);
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
}
} else {
if (!FeaturePolicyUtils::IsFeatureAllowed(
doc, NS_LITERAL_STRING("display-capture"))) {
audioPerm = nsIPermissionManager::DENY_ACTION;
} else {
rv = permManager->TestExactPermissionFromPrincipal(
principal, NS_LITERAL_CSTRING("screen"), &audioPerm);
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
}
rv = permDelegate->GetPermission(NS_LITERAL_CSTRING("screen"),
&audioPerm, true);
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
}
}
uint32_t videoPerm = nsIPermissionManager::UNKNOWN_ACTION;
if (IsOn(c.mVideo)) {
if (videoType == MediaSourceEnum::Camera) {
if (Preferences::GetBool("media.getusermedia.camera.deny", false) ||
!FeaturePolicyUtils::IsFeatureAllowed(
doc, NS_LITERAL_STRING("camera"))) {
if (Preferences::GetBool("media.getusermedia.camera.deny", false)) {
videoPerm = nsIPermissionManager::DENY_ACTION;
} else {
rv = permManager->TestExactPermissionFromPrincipal(
principal, NS_LITERAL_CSTRING("camera"), &videoPerm);
rv = permDelegate->GetPermission(NS_LITERAL_CSTRING("camera"),
&videoPerm, true);
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
}
} else {
if (!FeaturePolicyUtils::IsFeatureAllowed(
doc, NS_LITERAL_STRING("display-capture"))) {
videoPerm = nsIPermissionManager::DENY_ACTION;
} else {
rv = permManager->TestExactPermissionFromPrincipal(
principal, NS_LITERAL_CSTRING("screen"), &videoPerm);
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
}
rv = permDelegate->GetPermission(NS_LITERAL_CSTRING("screen"),
&videoPerm, true);
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
}
}
@ -4001,35 +3988,24 @@ bool MediaManager::IsActivelyCapturingOrHasAPermission(uint64_t aWindowId) {
// Check if this site has persistent permissions.
nsresult rv;
nsCOMPtr<nsIPermissionManager> mgr =
do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false; // no permission manager no permissions!
RefPtr<PermissionDelegateHandler> permDelegate =
doc->GetPermissionDelegateHandler();
if (NS_WARN_IF(!permDelegate)) {
return false;
}
uint32_t audio = nsIPermissionManager::UNKNOWN_ACTION;
uint32_t video = nsIPermissionManager::UNKNOWN_ACTION;
{
if (!FeaturePolicyUtils::IsFeatureAllowed(
doc, NS_LITERAL_STRING("microphone"))) {
audio = nsIPermissionManager::DENY_ACTION;
} else {
rv = mgr->TestExactPermissionFromPrincipal(
principal, NS_LITERAL_CSTRING("microphone"), &audio);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
rv = permDelegate->GetPermission(NS_LITERAL_CSTRING("microphone"), &audio,
true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
if (!FeaturePolicyUtils::IsFeatureAllowed(doc,
NS_LITERAL_STRING("camera"))) {
video = nsIPermissionManager::DENY_ACTION;
} else {
rv = mgr->TestExactPermissionFromPrincipal(
principal, NS_LITERAL_CSTRING("camera"), &video);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
rv =
permDelegate->GetPermission(NS_LITERAL_CSTRING("camera"), &video, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
}
return audio == nsIPermissionManager::ALLOW_ACTION ||

View File

@ -21,7 +21,7 @@ add_task(async function test_notifications_permission() {
await SpecialPowers.pushPrefEnv({
set: [
// Set pref to exercise relevant code path for regression test.
["permissions.delegation.enable", true],
["permissions.delegation.enabled", true],
// Automatically dismiss the permission request when it appears.
["dom.webnotifications.requireuserinteraction", true],
],

View File

@ -231,7 +231,7 @@
}
SpecialPowers.pushPrefEnv({"set": [
["permissions.delegation.enable", true],
["permissions.delegation.enabled", true],
]}).then(nextTest);
</script>
</body>

View File

@ -24,7 +24,7 @@ add_task(async function testNoPermissionPrompt() {
{
set: [
["dom.security.featurePolicy.enabled", true],
["permissions.delegation.enable", true],
["permissions.delegation.enabled", true],
["dom.security.featurePolicy.header.enabled", true],
["dom.security.featurePolicy.webidl.enabled", true],
],

View File

@ -7002,7 +7002,7 @@
# Prefs starting with "permissions."
#---------------------------------------------------------------------------
- name: permissions.delegation.enable
- name: permissions.delegation.enabled
type: bool
value: @IS_NIGHTLY_BUILD@
mirror: always

View File

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

View File

@ -22,7 +22,7 @@
".popup-notification-description > b:last-of-type":
"text=secondname,popupid",
".popup-notification-description > span:last-of-type":
"secondendlabel,popupid",
"text=secondendlabel,popupid",
".popup-notification-closebutton":
"oncommand=closebuttoncommand,hidden=closebuttonhidden",
".popup-notification-learnmore-link":

View File

@ -978,6 +978,9 @@ PopupNotifications.prototype = {
if ("secondName" in desc && "secondEnd" in desc) {
popupnotification.setAttribute("secondname", desc.secondName);
popupnotification.setAttribute("secondendlabel", desc.secondEnd);
} else {
popupnotification.removeAttribute("secondname");
popupnotification.removeAttribute("secondendlabel");
}
popupnotification.setAttribute("id", popupnotificationID);