Bug 1680721 - Grant a single iframe without user interaction to top windows. r=smaug,johannh

Only for top windows because for nested iframes they could get around
this without being noticed by reloading themselves which is not great.

Differential Revision: https://phabricator.services.mozilla.com/D98775
This commit is contained in:
Emilio Cobos Álvarez 2020-12-08 10:15:18 +00:00
parent fe574be463
commit 4fd5d13610
10 changed files with 168 additions and 61 deletions

View File

@ -94,6 +94,10 @@ WindowContext* WindowContext::TopWindowContext() {
bool WindowContext::IsTop() const { return mBrowsingContext->IsTop(); }
bool WindowContext::SameOriginWithTop() const {
return mBrowsingContext->SameOriginWithTop();
}
nsIGlobalObject* WindowContext::GetParentObject() const {
return xpc::NativeGlobal(xpc::PrivilegedJunkScope());
}

View File

@ -112,6 +112,8 @@ class WindowContext : public nsISupports, public nsWrapperCache {
WindowContext* GetParentWindowContext();
WindowContext* TopWindowContext();
bool SameOriginWithTop() const;
bool IsTop() const;
Span<RefPtr<BrowsingContext>> Children() { return mChildren; }

View File

@ -9943,14 +9943,30 @@ nsresult nsDocShell::DoURILoad(nsDocShellLoadState* aLoadState,
mBrowsingContext->GetParentWindowContext();
MOZ_ASSERT(parentContext);
const bool popupBlocked = [&] {
const bool active = mBrowsingContext->GetIsActive();
// For same-origin-with-top windows, we grant a single free popup
// without user activation, see bug 1680721.
//
// We consume the flag now even if there's no user activation.
const bool hasFreePass = [&] {
if (!active || !parentContext->SameOriginWithTop()) {
return false;
}
nsGlobalWindowInner* win =
parentContext->TopWindowContext()->GetInnerWindow();
return win && win->TryOpenExternalProtocolIframe();
}();
if (parentContext->ConsumeTransientUserGestureActivation()) {
// If the user has interacted with the page, consume it.
return false;
}
// TODO(emilio): Can we remove this check? It seems like what prompted
// this code (bug 1514547) should be covered by transient user
// activation, see bug 1514547.
if (mBrowsingContext->GetIsActive() &&
if (active &&
PopupBlocker::ConsumeTimerTokenForExternalProtocolIframe()) {
return false;
}
@ -9959,6 +9975,10 @@ nsresult nsDocShell::DoURILoad(nsDocShellLoadState* aLoadState,
return false;
}
if (hasFreePass) {
return false;
}
return true;
}();

View File

@ -926,6 +926,7 @@ nsGlobalWindowInner::nsGlobalWindowInner(nsGlobalWindowOuter* aOuterWindow,
mWasCurrentInnerWindow(false),
mHasSeenGamepadInput(false),
mHintedWasLoading(false),
mHasOpenedExternalProtocolFrame(false),
mSuspendDepth(0),
mFreezeDepth(0),
#ifdef DEBUG

View File

@ -1255,9 +1255,16 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget,
// Hint to the JS engine whether we are currently loading.
void HintIsLoading(bool aIsLoading);
public:
mozilla::dom::ContentMediaController* GetContentMediaController();
bool TryOpenExternalProtocolIframe() {
if (mHasOpenedExternalProtocolFrame) {
return false;
}
mHasOpenedExternalProtocolFrame = true;
return true;
}
private:
RefPtr<mozilla::dom::ContentMediaController> mContentMediaController;
@ -1325,6 +1332,10 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget,
// Whether we told the JS engine that we were in pageload.
bool mHintedWasLoading : 1;
// Whether this window has opened an external-protocol iframe without user
// activation once already. Only relevant for top windows.
bool mHasOpenedExternalProtocolFrame : 1;
nsCheapSet<nsUint32HashKey> mGamepadIndexSet;
nsRefPtrHashtable<nsGenericHashKey<mozilla::dom::GamepadHandle>,
mozilla::dom::Gamepad>

View File

@ -42,6 +42,9 @@ support-files =
[browser_protocol_ask_dialog.js]
support-files =
file_nested_protocol_request.html
[browser_first_prompt_not_blocked_without_user_interaction.js]
support-files =
file_external_protocol_iframe.html
[browser_protocol_ask_dialog_permission.js]
[browser_protocolhandler_loop.js]
[browser_remember_download_option.js]

View File

@ -0,0 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_PATH = getRootDirectory(gTestPath).replace(
"chrome://mochitests/content",
"https://example.com"
);
add_task(setupMailHandler);
add_task(async function test_open_without_user_interaction() {
await SpecialPowers.pushPrefEnv({
set: [
["dom.disable_open_during_load", true],
["dom.block_external_protocol_in_iframes", true],
["dom.delay.block_external_protocol_in_iframes.enabled", false],
],
});
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
let dialogWindowPromise = waitForProtocolAppChooserDialog(
tab.linkedBrowser,
true
);
BrowserTestUtils.loadURI(
tab.linkedBrowser,
TEST_PATH + "file_external_protocol_iframe.html"
);
let dialog = await dialogWindowPromise;
ok(dialog, "Should show the dialog even without user interaction");
let dialogClosedPromise = waitForProtocolAppChooserDialog(
tab.linkedBrowser,
false
);
// Adding another iframe without user interaction should be blocked.
let blockedWarning = new Promise(resolve => {
Services.console.registerListener(function onMessage(msg) {
let { message, logLevel } = msg;
if (logLevel != Ci.nsIConsoleMessage.warn) {
return;
}
if (!message.includes("Iframe with external protocol was blocked")) {
return;
}
Services.console.unregisterListener(onMessage);
resolve();
});
});
info("Adding another frame without user interaction");
await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
let frame = content.document.createElement("iframe");
frame.src = "mailto:foo@baz.com";
content.document.body.appendChild(frame);
});
await blockedWarning;
info("Removing tab to close the dialog.");
gBrowser.removeTab(tab);
await dialogClosedPromise;
});

View File

@ -3,15 +3,6 @@
"use strict";
ChromeUtils.import(
"resource://testing-common/HandlerServiceTestUtils.jsm",
this
);
let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
Ci.nsIHandlerService
);
const TEST_PATH = getRootDirectory(gTestPath).replace(
"chrome://mochitests/content",
"https://example.com"
@ -20,56 +11,7 @@ const TEST_PATH = getRootDirectory(gTestPath).replace(
const CONTENT_HANDLING_URL =
"chrome://mozapps/content/handling/appChooser.xhtml";
let gOldMailHandlers = [];
add_task(async function setup() {
let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto");
// Remove extant web handlers because they have icons that
// we fetch from the web, which isn't allowed in tests.
let handlers = mailHandlerInfo.possibleApplicationHandlers;
for (let i = handlers.Count() - 1; i >= 0; i--) {
try {
let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
gOldMailHandlers.push(handler);
// If we get here, this is a web handler app. Remove it:
handlers.removeElementAt(i);
} catch (ex) {}
}
let previousHandling = mailHandlerInfo.alwaysAskBeforeHandling;
mailHandlerInfo.alwaysAskBeforeHandling = true;
// Create a dummy web mail handler so we always know the mailto: protocol.
// Without this, the test fails on VMs without a default mailto: handler,
// because no dialog is ever shown, as we ignore subframe navigations to
// protocols that cannot be handled.
let dummy = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
Ci.nsIWebHandlerApp
);
dummy.name = "Handler 1";
dummy.uriTemplate = "https://example.com/first/%s";
mailHandlerInfo.possibleApplicationHandlers.appendElement(dummy);
gHandlerService.store(mailHandlerInfo);
registerCleanupFunction(() => {
// Re-add the original protocol handlers:
let mailHandlers = mailHandlerInfo.possibleApplicationHandlers;
for (let i = handlers.Count() - 1; i >= 0; i--) {
try {
// See if this is a web handler. If it is, it'll throw, otherwise,
// we will remove it.
mailHandlers.queryElementAt(i, Ci.nsIWebHandlerApp);
mailHandlers.removeElementAt(i);
} catch (ex) {}
}
for (let h of gOldMailHandlers) {
mailHandlers.appendElement(h);
}
mailHandlerInfo.alwaysAskBeforeHandling = previousHandling;
gHandlerService.store(mailHandlerInfo);
});
});
add_task(setupMailHandler);
/**
* Check that if we open the protocol handler dialog from a subframe, we close

View File

@ -0,0 +1 @@
<iframe src="mailto:foo@bar.com"></iframe>

View File

@ -1,4 +1,7 @@
var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
var { HandlerServiceTestUtils } = ChromeUtils.import(
"resource://testing-common/HandlerServiceTestUtils.jsm"
);
var gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
var gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
@ -195,3 +198,53 @@ async function promiseDownloadFinished(list) {
});
});
}
function setupMailHandler() {
let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto");
let gOldMailHandlers = [];
// Remove extant web handlers because they have icons that
// we fetch from the web, which isn't allowed in tests.
let handlers = mailHandlerInfo.possibleApplicationHandlers;
for (let i = handlers.Count() - 1; i >= 0; i--) {
try {
let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
gOldMailHandlers.push(handler);
// If we get here, this is a web handler app. Remove it:
handlers.removeElementAt(i);
} catch (ex) {}
}
let previousHandling = mailHandlerInfo.alwaysAskBeforeHandling;
mailHandlerInfo.alwaysAskBeforeHandling = true;
// Create a dummy web mail handler so we always know the mailto: protocol.
// Without this, the test fails on VMs without a default mailto: handler,
// because no dialog is ever shown, as we ignore subframe navigations to
// protocols that cannot be handled.
let dummy = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
Ci.nsIWebHandlerApp
);
dummy.name = "Handler 1";
dummy.uriTemplate = "https://example.com/first/%s";
mailHandlerInfo.possibleApplicationHandlers.appendElement(dummy);
gHandlerSvc.store(mailHandlerInfo);
registerCleanupFunction(() => {
// Re-add the original protocol handlers:
let mailHandlers = mailHandlerInfo.possibleApplicationHandlers;
for (let i = handlers.Count() - 1; i >= 0; i--) {
try {
// See if this is a web handler. If it is, it'll throw, otherwise,
// we will remove it.
mailHandlers.queryElementAt(i, Ci.nsIWebHandlerApp);
mailHandlers.removeElementAt(i);
} catch (ex) {}
}
for (let h of gOldMailHandlers) {
mailHandlers.appendElement(h);
}
mailHandlerInfo.alwaysAskBeforeHandling = previousHandling;
gHandlerSvc.store(mailHandlerInfo);
});
}