зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
5aa3cb7f38
Коммит
8ec2442bf5
|
@ -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.
|
||||
|
|
Загрузка…
Ссылка в новой задаче