Bug 1647519: Reject javascript: requests targeting other content processes. r=nika

Loads targeting cross-process BrowsingContexts are by definition cross-origin,
which should preclude any javascript: loads. While those loads are currently
prevented by principal checks in the final target process, sending IPC
messages for the attempts is unnecessary, and potentially opens a door to
privilege escalation exploits by a compromised content process.

This patch prevents any cross-process load requests from being sent by content
processes, and adds checks in the parent process to kill any (potentially
compromised) content process which attempts to send them.

Differential Revision: https://phabricator.services.mozilla.com/D103529
This commit is contained in:
Kris Maglione 2021-02-02 22:24:47 +00:00
Родитель 5aa3cb7f38
Коммит 8ec2442bf5
10 изменённых файлов: 306 добавлений и 0 удалений

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

@ -1798,6 +1798,19 @@ nsresult BrowsingContext::LoadURI(nsDocShellLoadState* aLoadState,
aLoadState->GetLoadIdentifier());
const auto& sourceBC = aLoadState->SourceBrowsingContext();
if (net::SchemeIsJavascript(aLoadState->URI())) {
if (!XRE_IsParentProcess()) {
// Web content should only be able to load javascript: URIs into documents
// whose principals the caller principal subsumes, which by definition
// excludes any document in a cross-process BrowsingContext.
return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI;
}
MOZ_DIAGNOSTIC_ASSERT(!sourceBC,
"Should never see a cross-process javascript: load "
"triggered from content");
}
MOZ_DIAGNOSTIC_ASSERT(!sourceBC || sourceBC->Group() == Group());
if (sourceBC && sourceBC->IsInProcess()) {
if (!sourceBC->CanAccess(this)) {
@ -1880,6 +1893,19 @@ nsresult BrowsingContext::InternalLoad(nsDocShellLoadState* aLoadState) {
MOZ_TRY(CheckSandboxFlags(aLoadState));
const auto& sourceBC = aLoadState->SourceBrowsingContext();
if (net::SchemeIsJavascript(aLoadState->URI())) {
if (!XRE_IsParentProcess()) {
// Web content should only be able to load javascript: URIs into documents
// whose principals the caller principal subsumes, which by definition
// excludes any document in a cross-process BrowsingContext.
return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI;
}
MOZ_DIAGNOSTIC_ASSERT(!sourceBC,
"Should never see a cross-process javascript: load "
"triggered from content");
}
if (XRE_IsParentProcess()) {
ContentParent* cp = Canonical()->GetContentParent();
if (!cp || !cp->CanSend()) {

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

@ -743,6 +743,22 @@ nsDocShell::SetCancelContentJSEpoch(int32_t aEpoch) {
return NS_OK;
}
nsresult nsDocShell::CheckDisallowedJavascriptLoad(
nsDocShellLoadState* aLoadState) {
if (!net::SchemeIsJavascript(aLoadState->URI())) {
return NS_OK;
}
if (nsCOMPtr<nsIPrincipal> targetPrincipal =
GetInheritedPrincipal(/* aConsiderCurrentDocument */ true)) {
if (!aLoadState->TriggeringPrincipal()->Subsumes(targetPrincipal)) {
return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI;
}
return NS_OK;
}
return NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI;
}
NS_IMETHODIMP
nsDocShell::LoadURI(nsDocShellLoadState* aLoadState, bool aSetNavigating) {
return LoadURI(aLoadState, aSetNavigating, false);
@ -767,6 +783,8 @@ nsresult nsDocShell::LoadURI(nsDocShellLoadState* aLoadState,
return NS_ERROR_FAILURE;
}
MOZ_TRY(CheckDisallowedJavascriptLoad(aLoadState));
bool oldIsNavigating = mIsNavigating;
auto cleanupIsNavigating =
MakeScopeExit([&]() { mIsNavigating = oldIsNavigating; });
@ -9195,6 +9213,8 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState,
aLoadState->TargetBrowsingContext() == GetBrowsingContext(),
"Load must be targeting this BrowsingContext");
MOZ_TRY(CheckDisallowedJavascriptLoad(aLoadState));
// If we don't have a target, we're loading into ourselves, and our load
// delegate may want to intercept that load.
SameDocumentNavigationState sameDocumentNavigationState;

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

@ -1050,6 +1050,11 @@ class nsDocShell final : public nsDocLoader,
void SetCacheKeyOnHistoryEntry(nsISHEntry* aSHEntry, uint32_t aCacheKey);
// If the LoadState's URI is a javascript: URI, checks that the triggering
// principal subsumes the principal of the current document, and returns
// NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI if it does not.
nsresult CheckDisallowedJavascriptLoad(nsDocShellLoadState* aLoadState);
nsresult LoadURI(nsDocShellLoadState* aLoadState, bool aSetNavigating,
bool aContinueHandlingSubframeHistory);

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

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script type="application/javascript">
"use strict";
addEventListener("message", event => {
if ("ping" in event.data) {
event.source.postMessage({ pong: event.data.ping }, event.origin);
}
});
</script>
</head>
<body>
</body>
</html>

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

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script type="application/javascript">
"use strict";
window.onload = () => {
opener.postMessage("ready", "*");
};
// eslint-disable-next-line no-shadow
function promiseMessage(source, filter = event => true) {
return new Promise(resolve => {
function listener(event) {
if (event.source == source && filter(event)) {
removeEventListener("message", listener);
resolve(event);
}
}
addEventListener("message", listener);
});
}
// Sends a message to the given target window and waits for the response.
function ping(target) {
let msg = { ping: Math.random() };
target.postMessage(msg, "*");
return promiseMessage(
target,
event => event.data && event.data.pong == msg.ping
);
}
function setFrameLocation(name, uri) {
window[name].location = uri;
}
</script>
</head>
<body>
</body>
</html>

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

@ -118,6 +118,10 @@ skip-if = fission # bug 1666449
[test_close_onpagehide_by_history_back.html]
[test_close_onpagehide_by_window_close.html]
[test_compressed_multipart.html]
[test_content_javascript_loads.html]
support-files =
file_content_javascript_loads_root.html
file_content_javascript_loads_frame.html
[test_forceinheritprincipal_overrule_owner.html]
[test_framedhistoryframes.html]
support-files = file_framedhistoryframes.html

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

@ -0,0 +1,163 @@
<!DOCTYPE html>
<html>
<head>
<title>Test for Bug 1647519</title>
<meta charset="utf-8">
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1647519">Mozilla Bug 1647519</a>
<script type="application/javascript">
"use strict";
function promiseMessage(source, filter = event => true) {
return new Promise(resolve => {
function listener(event) {
if (event.source == source && filter(event)) {
window.removeEventListener("message", listener);
resolve(event);
}
}
window.addEventListener("message", listener);
});
}
async function runTests(resourcePath) {
/* globals Assert, content */
let doc = content.document;
// Sends a message to the given target window and waits for a response a few
// times to (more or less) ensure that a `javascript:` load request has had
// time to succeed, if it were going to.
async function doSomeRoundTrips(target) {
for (let i = 0; i < 3; i++) {
// Note: The ping message needs to be sent from a script running in the
// content scope or there will be no source window for the reply to be
// sent to.
await content.wrappedJSObject.ping(target);
}
}
function promiseEvent(target, name) {
return new Promise(resolve => {
target.addEventListener(name, resolve, { once: true });
});
}
function createIframe(host, id) {
let iframe = doc.createElement("iframe");
iframe.id = id;
iframe.name = id;
iframe.src = `http://${host}${resourcePath}file_content_javascript_loads_frame.html`;
doc.body.appendChild(iframe);
return promiseEvent(iframe, "load");
}
const ID_SAME_ORIGIN = "frame-same-origin";
const ID_SAME_BASE_DOMAIN = "frame-same-base-domain";
const ID_CROSS_BASE_DOMAIN = "frame-cross-base-domain";
await Promise.all([
createIframe("example.com", ID_SAME_ORIGIN),
createIframe("test1.example.com", ID_SAME_BASE_DOMAIN),
createIframe("example.org", ID_CROSS_BASE_DOMAIN),
]);
let gotJSLoadFrom = null;
let pendingJSLoadID = null;
content.addEventListener("message", event => {
if ("javascriptLoadID" in event.data) {
Assert.equal(
event.data.javascriptLoadID,
pendingJSLoadID,
"Message from javascript: load should have the expected ID"
);
Assert.equal(
gotJSLoadFrom,
null,
"Should not have seen a previous load message this cycle"
);
gotJSLoadFrom = event.source.name;
}
});
async function watchForJSLoads(frameName, expected, task) {
let loadId = Math.random();
let jsURI =
"javascript:" +
encodeURI(`parent.postMessage({ javascriptLoadID: ${loadId} }, "*")`);
pendingJSLoadID = loadId;
gotJSLoadFrom = null;
await task(jsURI);
await doSomeRoundTrips(content.wrappedJSObject[frameName]);
if (expected) {
Assert.equal(
gotJSLoadFrom,
frameName,
`Should have seen javascript: URI loaded into ${frameName}`
);
} else {
Assert.equal(
gotJSLoadFrom,
null,
"Should not have seen javascript: URI loaded"
);
}
}
let frames = [
{ name: ID_SAME_ORIGIN, expectLoad: true },
{ name: ID_SAME_BASE_DOMAIN, expectLoad: false },
{ name: ID_CROSS_BASE_DOMAIN, expectLoad: false },
];
for (let { name, expectLoad } of frames) {
info(`Checking loads for frame "${name}". Expecting loads: ${expectLoad}`);
info("Checking location setter");
await watchForJSLoads(name, expectLoad, jsURI => {
// Note: We need to do this from the content scope since security checks
// depend on the JS caller scope.
content.wrappedJSObject.setFrameLocation(name, jsURI);
});
info("Checking targeted <a> load");
await watchForJSLoads(name, expectLoad, jsURI => {
let a = doc.createElement("a");
a.target = name;
a.href = jsURI;
doc.body.appendChild(a);
a.click();
a.remove();
});
info("Checking targeted window.open load");
await watchForJSLoads(name, expectLoad, jsURI => {
content.wrappedJSObject.open(jsURI, name);
});
}
}
add_task(async function() {
const resourcePath = location.pathname.replace(/[^\/]+$/, "");
let win = window.open(
`http://example.com${resourcePath}file_content_javascript_loads_root.html`
);
await promiseMessage(win, event => event.data == "ready");
await SpecialPowers.spawn(win, [resourcePath], runTests);
win.close();
});
</script>
</body>
</html>

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

@ -154,6 +154,21 @@ void LocationBase::SetURI(nsIURI* aURI, nsIPrincipal& aSubjectPrincipal,
rv = bc->LoadURI(loadState);
if (NS_WARN_IF(NS_FAILED(rv))) {
if (rv == NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI &&
net::SchemeIsJavascript(loadState->URI())) {
// Per spec[1], attempting to load a javascript: URI into a cross-origin
// BrowsingContext is a no-op, and should not raise an exception.
// Technically, Location setters run with exceptions enabled should only
// throw an exception[2] when the caller is not allowed to navigate[3] the
// target browsing context due to sandboxing flags or not being
// closely-related enough, though in practice we currently throw for other
// reasons as well.
//
// [1]: https://html.spec.whatwg.org/multipage/browsing-the-web.html#javascript-protocol
// [2]: https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate
// [3]: https://html.spec.whatwg.org/multipage/browsers.html#allowed-to-navigate
return;
}
aRv.Throw(rv);
}
}

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

@ -300,6 +300,11 @@ mozilla::ipc::IPCResult WindowGlobalParent::RecvLoadURI(
("ParentIPC: Trying to send a message with dead or detached context"));
return IPC_OK();
}
if (net::SchemeIsJavascript(aLoadState->URI())) {
return IPC_FAIL(this, "Illegal cross-process javascript: load attempt");
}
CanonicalBrowsingContext* targetBC = aTargetBC.get_canonical();
// FIXME: For cross-process loads, we should double check CanAccess() for the
@ -328,6 +333,11 @@ mozilla::ipc::IPCResult WindowGlobalParent::RecvInternalLoad(
("ParentIPC: Trying to send a message with dead or detached context"));
return IPC_OK();
}
if (net::SchemeIsJavascript(aLoadState->URI())) {
return IPC_FAIL(this, "Illegal cross-process javascript: load attempt");
}
CanonicalBrowsingContext* targetBC =
aLoadState->TargetBrowsingContext().get_canonical();

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

@ -779,6 +779,10 @@ with modules["DOM"]:
# https://fetch.spec.whatwg.org/#cross-origin-resource-policy-header
errors["NS_ERROR_DOM_CORP_FAILED"] = FAILURE(1036)
# Used to indicate that a URI may not be loaded into a cross-origin
# context.
errors["NS_ERROR_DOM_BAD_CROSS_ORIGIN_URI"] = FAILURE(1037)
# May be used to indicate when e.g. setting a property value didn't
# actually change the value, like for obj.foo = "bar"; obj.foo = "bar";
# the second assignment throws NS_SUCCESS_DOM_NO_OPERATION.