Skip to content

Commit

Permalink
Bug 1816287 - Always exit fullscreen when triggering on external prot…
Browse files Browse the repository at this point in the history
…ocol r=edgar

It's a security risk such that the maximized external program can obscure
the fullscreen notification and the malicious site can use this trick
to load a spoofed page in the background without user notices it.

This patch minimized the risk by always exit the fullscreen mode when
an external protocol is triggered.

Differential Revision: https://phabricator.services.mozilla.com/D177771
  • Loading branch information
sefeng211 committed May 17, 2023
1 parent 0702d3f commit bd1e35c
Show file tree
Hide file tree
Showing 12 changed files with 196 additions and 31 deletions.
11 changes: 6 additions & 5 deletions docshell/base/nsDocShell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10186,11 +10186,7 @@ nsresult nsDocShell::DoURILoad(nsDocShellLoadState* aLoadState,

if (StaticPrefs::dom_block_external_protocol_in_iframes()) {
// Only allow URLs able to return data in iframes.
bool doesNotReturnData = false;
NS_URIChainHasFlags(aLoadState->URI(),
nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA,
&doesNotReturnData);
if (doesNotReturnData) {
if (nsContentUtils::IsExternalProtocol(aLoadState->URI())) {
// The context to check user-interaction with for the purposes of
// popup-blocking.
//
Expand Down Expand Up @@ -12851,6 +12847,11 @@ nsresult nsDocShell::OnLinkClick(
return NS_OK;
}

Document* ownerDoc = aContent->OwnerDoc();
if (nsContentUtils::IsExternalProtocol(aURI)) {
ownerDoc->EnsureNotEnteringAndExitFullscreen();
}

bool noOpenerImplied = false;
nsAutoString target(aTargetSpec);
if (aFileName.IsVoid() &&
Expand Down
7 changes: 7 additions & 0 deletions dom/base/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3959,6 +3959,13 @@ nsresult Document::InitFeaturePolicy(nsIChannel* aChannel) {
return NS_OK;
}

void Document::EnsureNotEnteringAndExitFullscreen() {
Document::ClearPendingFullscreenRequests(this);
if (GetFullscreenElement()) {
Document::AsyncExitFullscreen(this);
}
}

void Document::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) {
mReferrerInfo = aReferrerInfo;
mCachedReferrerInfoForInternalCSSAndSVGResources = nullptr;
Expand Down
2 changes: 2 additions & 0 deletions dom/base/Document.h
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,8 @@ class Document : public nsINode,
void InitFeaturePolicy();
nsresult InitFeaturePolicy(nsIChannel* aChannel);

void EnsureNotEnteringAndExitFullscreen();

protected:
friend class nsUnblockOnloadEvent;

Expand Down
6 changes: 6 additions & 0 deletions dom/base/LocationBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ void LocationBase::SetURI(nsIURI* aURI, nsIPrincipal& aSubjectPrincipal,
return;
}
aRv.Throw(rv);
return;
}

Document* doc = bc->GetDocument();
if (doc && nsContentUtils::IsExternalProtocol(aURI)) {
doc->EnsureNotEnteringAndExitFullscreen();
}
}

Expand Down
7 changes: 7 additions & 0 deletions dom/base/nsContentUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,13 @@ mozilla::EventClassID nsContentUtils::GetEventClassIDFromMessage(
}
}

bool nsContentUtils::IsExternalProtocol(nsIURI* aURI) {
bool doesNotReturnData = false;
nsresult rv = NS_URIChainHasFlags(
aURI, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA, &doesNotReturnData);
return NS_SUCCEEDED(rv) && doesNotReturnData;
}

static nsAtom* GetEventTypeFromMessage(EventMessage aEventMessage) {
switch (aEventMessage) {
#define MESSAGE_TO_EVENT(name_, message_, type_, struct_) \
Expand Down
2 changes: 2 additions & 0 deletions dom/base/nsContentUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -3405,6 +3405,8 @@ class nsContentUtils {
*/
static nsIContent* GetClosestLinkInFlatTree(nsIContent* aContent);

static bool IsExternalProtocol(nsIURI* aURI);

private:
static bool InitializeEventTable();

Expand Down
6 changes: 1 addition & 5 deletions dom/base/nsNoDataProtocolContentPolicy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ nsNoDataProtocolContentPolicy::ShouldLoad(nsIURI* aContentLocation,
return NS_OK;
}

bool shouldBlock;
nsresult rv = NS_URIChainHasFlags(
aContentLocation, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA,
&shouldBlock);
if (NS_SUCCEEDED(rv) && shouldBlock) {
if (nsContentUtils::IsExternalProtocol(aContentLocation)) {
NS_SetRequestBlockingReason(
aLoadInfo,
nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_NO_DATA_PROTOCOL);
Expand Down
1 change: 1 addition & 0 deletions dom/base/test/fullscreen/browser.ini
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ support-files =
file_fullscreen-bug-1798219-2.html
[browser_fullscreen-window-open-race.js]
[browser_fullscreen-sizemode.js]
[browser_fullscreen_exit_on_external_protocol.js]
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";

SimpleTest.requestCompleteLog();

requestLongerTimeout(2);

// Import helpers
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/dom/base/test/fullscreen/fullscreen_helpers.js",
this
);

add_setup(async function() {
await pushPrefs(
["full-screen-api.transition-duration.enter", "0 0"],
["full-screen-api.transition-duration.leave", "0 0"],
["full-screen-api.allow-trusted-requests-only", false]
);
});

const { HandlerServiceTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/HandlerServiceTestUtils.sys.mjs"
);

const gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
Ci.nsIHandlerService
);

const CONTENT = `data:text/html,
<!DOCTYPE html>
<html>
<body>
<button>
<a href="mailto:[email protected]"></a>
</button>
</body>
</html>
`;

// This test tends to trigger a race in the fullscreen time telemetry,
// where the fullscreen enter and fullscreen exit events (which use the
// same histogram ID) overlap. That causes TelemetryStopwatch to log an
// error.
SimpleTest.ignoreAllUncaughtExceptions(true);

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);
});
}

add_task(setupMailHandler);

add_task(async function OpenExternalProtocolOnPendingFullscreen() {
for (const useClick of [true, false]) {
await BrowserTestUtils.withNewTab(CONTENT, async browser => {
const leavelFullscreen = waitForFullscreenState(document, false, true);
await SpecialPowers.spawn(browser, [useClick], async function(
shouldClick
) {
const button = content.document.querySelector("button");

const clickDone = new Promise(r => {
button.addEventListener("click", function() {
content.document.documentElement.requestFullscreen();
// When anchor.click() is called, the fullscreen request
// is probably still pending.
if (shouldClick) {
content.document.querySelector("a").click();
} else {
content.document.location = "mailto:[email protected]";
}
r();
});
});
button.click();
await clickDone;
});

await leavelFullscreen;
ok(true, "Fullscreen should be exited");
});
}
});

add_task(async function OpenExternalProtocolOnFullscreen() {
for (const useClick of [true, false]) {
await BrowserTestUtils.withNewTab(CONTENT, async browser => {
const leavelFullscreen = waitForFullscreenState(document, false, true);
await SpecialPowers.spawn(browser, [useClick], async function(
shouldClick
) {
let button = content.document.querySelector("button");
button.addEventListener("click", function() {
content.document.documentElement.requestFullscreen();
});
button.click();

await new Promise(r => {
content.document.addEventListener("fullscreenchange", r);
});

if (shouldClick) {
content.document.querySelector("a").click();
} else {
content.document.location = "mailto:[email protected]";
}
});

await leavelFullscreen;
ok(true, "Fullscreen should be exited");
});
}
});
5 changes: 1 addition & 4 deletions image/imgLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3314,13 +3314,10 @@ imgCacheValidator::OnRedirectVerifyCallback(nsresult aResult) {
// an external application, e.g. mailto:
nsCOMPtr<nsIURI> uri;
mRedirectChannel->GetURI(getter_AddRefs(uri));
bool doesNotReturnData = false;
NS_URIChainHasFlags(uri, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA,
&doesNotReturnData);

nsresult result = NS_OK;

if (doesNotReturnData) {
if (nsContentUtils::IsExternalProtocol(uri)) {
result = NS_ERROR_ABORT;
}

Expand Down
13 changes: 2 additions & 11 deletions image/imgRequest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1219,17 +1219,8 @@ imgRequest::OnRedirectVerifyCallback(nsresult result) {

// Make sure we have a protocol that returns data rather than opens an
// external application, e.g. 'mailto:'.
bool doesNotReturnData = false;
nsresult rv = NS_URIChainHasFlags(
mFinalURI, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA,
&doesNotReturnData);

if (NS_SUCCEEDED(rv) && doesNotReturnData) {
rv = NS_ERROR_ABORT;
}

if (NS_FAILED(rv)) {
mRedirectCallback->OnRedirectVerifyCallback(rv);
if (nsContentUtils::IsExternalProtocol(mFinalURI)) {
mRedirectCallback->OnRedirectVerifyCallback(NS_ERROR_ABORT);
mRedirectCallback = nullptr;
return NS_OK;
}
Expand Down
7 changes: 1 addition & 6 deletions parser/html/nsHtml5TreeOperation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1107,12 +1107,7 @@ nsresult nsHtml5TreeOperation::Perform(nsHtml5TreeOpExecutor* aBuilder,
// URLs that return data (e.g. "http:" URLs) should be prefixed with
// "view-source:". URLs that don't return data should just be returned
// undecorated.
bool doesNotReturnData = false;
rv =
NS_URIChainHasFlags(uri, nsIProtocolHandler::URI_DOES_NOT_RETURN_DATA,
&doesNotReturnData);
NS_ENSURE_SUCCESS(rv, NS_OK);
if (!doesNotReturnData) {
if (!nsContentUtils::IsExternalProtocol(uri)) {
viewSourceUrl.AssignLiteral("view-source:");
}

Expand Down

0 comments on commit bd1e35c

Please sign in to comment.