Bug 1254488 - Run the the prohibits mixed security contexts algorithm and check a priori unauthenticated URL in PresentationRequest, r=smaug

This commit is contained in:
Kershaw Chang 2016-08-10 23:01:00 +02:00
Родитель 02e9d4e17a
Коммит b542f5170c
11 изменённых файлов: 450 добавлений и 106 удалений

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

@ -79,6 +79,7 @@
#include "nsContentDLF.h"
#include "nsContentList.h"
#include "nsContentPolicyUtils.h"
#include "nsContentSecurityManager.h"
#include "nsCPrefetchService.h"
#include "nsCRT.h"
#include "nsCycleCollectionParticipant.h"
@ -9271,3 +9272,100 @@ nsContentUtils::GetDocShellForEventTarget(EventTarget* aTarget)
return nullptr;
}
/*
* Note: this function only relates to figuring out HTTPS state, which is an
* input to the Secure Context algorithm. We are not actually implementing any
* part of the Secure Context algorithm itself here.
*
* This is a bit of a hack. Ideally we'd propagate HTTPS state through
* nsIChannel as described in the Fetch and HTML specs, but making channels
* know about whether they should inherit HTTPS state, propagating information
* about who the channel's "client" is, exposing GetHttpsState API on channels
* and modifying the various cache implementations to store and retrieve HTTPS
* state involves a huge amount of code (see bug 1220687). We avoid that for
* now using this function.
*
* This function takes advantage of the observation that we can return true if
* nsIContentSecurityManager::IsOriginPotentiallyTrustworthy returns true for
* the document's origin (e.g. the origin has a scheme of 'https' or host
* 'localhost' etc.). Since we generally propagate a creator document's origin
* onto data:, blob:, etc. documents, this works for them too.
*
* The scenario where this observation breaks down is sandboxing without the
* 'allow-same-origin' flag, since in this case a document is given a unique
* origin (IsOriginPotentiallyTrustworthy would return false). We handle that
* by using the origin that the document would have had had it not been
* sandboxed.
*
* DEFICIENCIES: Note that this function uses nsIScriptSecurityManager's
* getChannelResultPrincipalIfNotSandboxed, and that method's ignoring of
* sandboxing is limited to the immediate sandbox. In the case that aDocument
* should inherit its origin (e.g. data: URI) but its parent has ended up
* with a unique origin due to sandboxing further up the parent chain we may
* end up returning false when we would ideally return true (since we will
* examine the parent's origin for 'https' and not finding it.) This means
* that we may restrict the privileges of some pages unnecessarily in this
* edge case.
*/
/* static */ bool
nsContentUtils::HttpsStateIsModern(nsIDocument* aDocument)
{
if (!aDocument) {
return false;
}
nsCOMPtr<nsIPrincipal> principal = aDocument->NodePrincipal();
if (principal->GetIsSystemPrincipal()) {
return true;
}
// If aDocument is sandboxed, try and get the principal that it would have
// been given had it not been sandboxed:
if (principal->GetIsNullPrincipal() &&
(aDocument->GetSandboxFlags() & SANDBOXED_ORIGIN)) {
nsIChannel* channel = aDocument->GetChannel();
if (channel) {
nsCOMPtr<nsIScriptSecurityManager> ssm =
nsContentUtils::GetSecurityManager();
nsresult rv =
ssm->GetChannelResultPrincipalIfNotSandboxed(channel,
getter_AddRefs(principal));
if (NS_FAILED(rv)) {
return false;
}
if (principal->GetIsSystemPrincipal()) {
// If a document with the system principal is sandboxing a subdocument
// that would normally inherit the embedding element's principal (e.g.
// a srcdoc document) then the embedding document does not trust the
// content that is written to the embedded document. Unlike when the
// embedding document is https, in this case we have no indication as
// to whether the embedded document's contents are delivered securely
// or not, and the sandboxing would possibly indicate that they were
// not. To play it safe we return false here. (See bug 1162772
// comment 73-80.)
return false;
}
}
}
if (principal->GetIsNullPrincipal()) {
return false;
}
MOZ_ASSERT(principal->GetIsCodebasePrincipal());
nsCOMPtr<nsIContentSecurityManager> csm =
do_GetService(NS_CONTENTSECURITYMANAGER_CONTRACTID);
NS_WARN_IF(!csm);
if (csm) {
bool isTrustworthyOrigin = false;
csm->IsOriginPotentiallyTrustworthy(principal, &isTrustworthyOrigin);
if (isTrustworthyOrigin) {
return true;
}
}
return false;
}

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

@ -2666,6 +2666,14 @@ public:
*/
static nsIDocShell* GetDocShellForEventTarget(mozilla::dom::EventTarget* aTarget);
/**
* Returns true if the "HTTPS state" of the document should be "modern". See:
*
* https://html.spec.whatwg.org/#concept-document-https-state
* https://fetch.spec.whatwg.org/#concept-response-https-state
*/
static bool HttpsStateIsModern(nsIDocument* aDocument);
private:
static bool InitializeEventTable();

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

@ -2340,103 +2340,6 @@ InitializeLegacyNetscapeObject(JSContext* aCx, JS::Handle<JSObject*> aGlobal)
return JS_DefineFunctions(aCx, obj, EnablePrivilegeSpec);
}
/**
* Returns true if the "HTTPS state" of the document should be "modern". See:
*
* https://html.spec.whatwg.org/#concept-document-https-state
* https://fetch.spec.whatwg.org/#concept-response-https-state
*
* Note: this function only relates to figuring out HTTPS state, which is an
* input to the Secure Context algorithm. We are not actually implementing any
* part of the Secure Context algorithm itself here.
*
* This is a bit of a hack. Ideally we'd propagate HTTPS state through
* nsIChannel as described in the Fetch and HTML specs, but making channels
* know about whether they should inherit HTTPS state, propagating information
* about who the channel's "client" is, exposing GetHttpsState API on channels
* and modifying the various cache implementations to store and retrieve HTTPS
* state involves a huge amount of code (see bug 1220687). We avoid that for
* now using this function.
*
* This function takes advantage of the observation that we can return true if
* nsIContentSecurityManager::IsOriginPotentiallyTrustworthy returns true for
* the document's origin (e.g. the origin has a scheme of 'https' or host
* 'localhost' etc.). Since we generally propagate a creator document's origin
* onto data:, blob:, etc. documents, this works for them too.
*
* The scenario where this observation breaks down is sandboxing without the
* 'allow-same-origin' flag, since in this case a document is given a unique
* origin (IsOriginPotentiallyTrustworthy would return false). We handle that
* by using the origin that the document would have had had it not been
* sandboxed.
*
* DEFICIENCIES: Note that this function uses nsIScriptSecurityManager's
* getChannelResultPrincipalIfNotSandboxed, and that method's ignoring of
* sandboxing is limited to the immediate sandbox. In the case that aDocument
* should inherit its origin (e.g. data: URI) but its parent has ended up
* with a unique origin due to sandboxing further up the parent chain we may
* end up returning false when we would ideally return true (since we will
* examine the parent's origin for 'https' and not finding it.) This means
* that we may restrict the privileges of some pages unnecessarily in this
* edge case.
*/
static bool HttpsStateIsModern(nsIDocument* aDocument)
{
nsCOMPtr<nsIPrincipal> principal = aDocument->NodePrincipal();
if (principal->GetIsSystemPrincipal()) {
return true;
}
// If aDocument is sandboxed, try and get the principal that it would have
// been given had it not been sandboxed:
if (principal->GetIsNullPrincipal() &&
(aDocument->GetSandboxFlags() & SANDBOXED_ORIGIN)) {
nsIChannel* channel = aDocument->GetChannel();
if (channel) {
nsCOMPtr<nsIScriptSecurityManager> ssm =
nsContentUtils::GetSecurityManager();
nsresult rv =
ssm->GetChannelResultPrincipalIfNotSandboxed(channel,
getter_AddRefs(principal));
if (NS_FAILED(rv)) {
return false;
}
if (principal->GetIsSystemPrincipal()) {
// If a document with the system principal is sandboxing a subdocument
// that would normally inherit the embedding element's principal (e.g.
// a srcdoc document) then the embedding document does not trust the
// content that is written to the embedded document. Unlike when the
// embedding document is https, in this case we have no indication as
// to whether the embedded document's contents are delivered securely
// or not, and the sandboxing would possibly indicate that they were
// not. To play it safe we return false here. (See bug 1162772
// comment 73-80.)
return false;
}
}
}
if (principal->GetIsNullPrincipal()) {
return false;
}
MOZ_ASSERT(principal->GetIsCodebasePrincipal());
nsCOMPtr<nsIContentSecurityManager> csm =
do_GetService(NS_CONTENTSECURITYMANAGER_CONTRACTID);
NS_WARN_IF(!csm);
if (csm) {
bool isTrustworthyOrigin = false;
csm->IsOriginPotentiallyTrustworthy(principal, &isTrustworthyOrigin);
if (isTrustworthyOrigin) {
return true;
}
}
return false;
}
bool
nsGlobalWindow::ComputeIsSecureContext(nsIDocument* aDocument)
{
@ -2482,7 +2385,7 @@ nsGlobalWindow::ComputeIsSecureContext(nsIDocument* aDocument)
return false;
}
if (HttpsStateIsModern(aDocument)) {
if (nsContentUtils::HttpsStateIsModern(aDocument)) {
return true;
}

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

@ -7,10 +7,12 @@
#include "PresentationRequest.h"
#include "ControllerConnectionCollection.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/dom/PresentationRequestBinding.h"
#include "mozilla/dom/PresentationConnectionAvailableEvent.h"
#include "mozilla/dom/Promise.h"
#include "mozIThirdPartyUtil.h"
#include "nsContentSecurityManager.h"
#include "nsCycleCollectionParticipant.h"
#include "nsIDocument.h"
#include "nsIPresentationService.h"
@ -155,6 +157,12 @@ PresentationRequest::StartWithDevice(const nsAString& aDeviceId,
return nullptr;
}
if (IsProhibitMixedSecurityContexts(doc) &&
!IsPrioriAuthenticatedURL(mUrl)) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
if (doc->GetSandboxFlags() & SANDBOXED_PRESENTATION) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
@ -196,10 +204,6 @@ already_AddRefed<Promise>
PresentationRequest::Reconnect(const nsAString& aPresentationId,
ErrorResult& aRv)
{
// TODO: Before starting to reconnect, we have to run the prohibits
// mixed security contexts algorithm first. This will be implemented
// in bug 1254488.
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner());
if (NS_WARN_IF(!global)) {
aRv.Throw(NS_ERROR_UNEXPECTED);
@ -217,6 +221,12 @@ PresentationRequest::Reconnect(const nsAString& aPresentationId,
return nullptr;
}
if (IsProhibitMixedSecurityContexts(doc) &&
!IsPrioriAuthenticatedURL(mUrl)) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
if (doc->GetSandboxFlags() & SANDBOXED_PRESENTATION) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
@ -324,6 +334,12 @@ PresentationRequest::GetAvailability(ErrorResult& aRv)
return nullptr;
}
if (IsProhibitMixedSecurityContexts(doc) &&
!IsPrioriAuthenticatedURL(mUrl)) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
if (doc->GetSandboxFlags() & SANDBOXED_PRESENTATION) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
@ -352,3 +368,71 @@ PresentationRequest::DispatchConnectionAvailableEvent(PresentationConnection* aC
new AsyncEventDispatcher(this, event);
return asyncDispatcher->PostDOMEvent();
}
bool
PresentationRequest::IsProhibitMixedSecurityContexts(nsIDocument* aDocument)
{
MOZ_ASSERT(aDocument);
if (nsContentUtils::IsChromeDoc(aDocument)) {
return true;
}
nsCOMPtr<nsIDocument> doc = aDocument;
while (doc && !nsContentUtils::IsChromeDoc(doc)) {
if (nsContentUtils::HttpsStateIsModern(doc)) {
return true;
}
doc = doc->GetParentDocument();
}
return false;
}
bool
PresentationRequest::IsPrioriAuthenticatedURL(const nsAString& aUrl)
{
nsCOMPtr<nsIURI> uri;
if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), aUrl))) {
return false;
}
nsAutoCString scheme;
nsresult rv = uri->GetScheme(scheme);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
if (scheme.EqualsLiteral("data")) {
return true;
}
nsAutoCString uriSpec;
rv = uri->GetSpec(uriSpec);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
if (uriSpec.EqualsLiteral("about:blank") ||
uriSpec.EqualsLiteral("about:srcdoc")) {
return true;
}
PrincipalOriginAttributes attrs;
nsCOMPtr<nsIPrincipal> principal =
BasePrincipal::CreateCodebasePrincipal(uri, attrs);
if (NS_WARN_IF(!principal)) {
return false;
}
nsCOMPtr<nsIContentSecurityManager> csm =
do_GetService(NS_CONTENTSECURITYMANAGER_CONTRACTID);
if (NS_WARN_IF(!csm)) {
return false;
}
bool isTrustworthyOrigin = false;
csm->IsOriginPotentiallyTrustworthy(principal, &isTrustworthyOrigin);
return isTrustworthyOrigin;
}

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

@ -9,6 +9,8 @@
#include "mozilla/DOMEventTargetHelper.h"
class nsIDocument;
namespace mozilla {
namespace dom {
@ -56,6 +58,12 @@ private:
void FindOrCreatePresentationConnection(const nsAString& aPresentationId,
Promise* aPromise);
// Implement https://w3c.github.io/webappsec-mixed-content/#categorize-settings-object
bool IsProhibitMixedSecurityContexts(nsIDocument* aDocument);
// Implement https://w3c.github.io/webappsec-mixed-content/#a-priori-authenticated-url
bool IsPrioriAuthenticatedURL(const nsAString& aUrl);
nsString mUrl;
RefPtr<PresentationAvailability> mAvailability;
};

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

@ -0,0 +1,159 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test allow-presentation sandboxing flag</title>
<script type="application/javascript;version=1.8">
"use strict";
function is(a, b, msg) {
window.parent.postMessage((a === b ? "OK " : "KO ") + msg, "*");
}
function ok(a, msg) {
window.parent.postMessage((a ? "OK " : "KO ") + msg, "*");
}
function info(msg) {
window.parent.postMessage("INFO " + msg, "*");
}
function command(msg) {
window.parent.postMessage("COMMAND " + JSON.stringify(msg), "*");
}
function finish() {
window.parent.postMessage("DONE", "*");
}
function testGetAvailability() {
return new Promise(function(aResolve, aReject) {
ok(navigator.presentation, "navigator.presentation should be available.");
var request = new PresentationRequest("http://example.com");
request.getAvailability().then(
function(aAvailability) {
ok(false, "Unexpected success, should get a security error.");
aReject();
},
function(aError) {
is(aError.name, "SecurityError", "Should get a security error.");
aResolve();
}
);
});
}
function testStartRequest() {
return new Promise(function(aResolve, aReject) {
var request = new PresentationRequest("http://example.com");
request.start().then(
function(aAvailability) {
ok(false, "Unexpected success, should get a security error.");
aReject();
},
function(aError) {
is(aError.name, "SecurityError", "Should get a security error.");
aResolve();
}
);
});
}
function testReconnectRequest() {
return new Promise(function(aResolve, aReject) {
var request = new PresentationRequest("http://example.com");
request.reconnect("dummyId").then(
function(aConnection) {
ok(false, "Unexpected success, should get a security error.");
aReject();
},
function(aError) {
is(aError.name, "SecurityError", "Should get a security error.");
aResolve();
}
);
});
}
function testGetAvailabilityForAboutBlank() {
return new Promise(function(aResolve, aReject) {
var request = new PresentationRequest("about:blank");
request.getAvailability().then(
function(aAvailability) {
ok(true, "Success due to a priori authenticated URL.");
aResolve();
},
function(aError) {
ok(false, "Error occurred when getting availability: " + aError);
aReject();
}
);
});
}
function testGetAvailabilityForAboutSrcdoc() {
return new Promise(function(aResolve, aReject) {
var request = new PresentationRequest("about:srcdoc");
request.getAvailability().then(
function(aAvailability) {
ok(true, "Success due to a priori authenticated URL.");
aResolve();
},
function(aError) {
ok(false, "Error occurred when getting availability: " + aError);
aReject();
}
);
});
}
function testGetAvailabilityForDataURL() {
return new Promise(function(aResolve, aReject) {
var request = new PresentationRequest("data:text/html,1");
request.getAvailability().then(
function(aAvailability) {
ok(true, "Success due to a priori authenticated URL.");
aResolve();
},
function(aError) {
ok(false, "Error occurred when getting availability: " + aError);
aReject();
}
);
});
}
function runTest() {
testGetAvailability()
.then(testStartRequest)
.then(testReconnectRequest)
.then(testGetAvailabilityForAboutBlank)
.then(testGetAvailabilityForAboutSrcdoc)
.then(testGetAvailabilityForDataURL)
.then(finish);
}
window.addEventListener("message", function onMessage(evt) {
window.removeEventListener("message", onMessage);
if (evt.data === "start") {
runTest();
}
}, false);
window.setTimeout(function() {
command("ready-to-start");
}, 3000);
</script>
</head>
<body>
</body>
</html>

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

@ -23,6 +23,7 @@ support-files =
file_presentation_unknown_content_type.test
file_presentation_unknown_content_type.test^headers^
test_presentation_tcp_receiver_establish_connection_unknown_content_type.js
file_presentation_mixed_security_contexts.html
[test_presentation_dc_sender.html]
[test_presentation_dc_receiver.html]
@ -70,3 +71,4 @@ skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android')
skip-if = toolkit == 'android'
[test_presentation_sandboxed_presentation.html]
[test_presentation_reconnect.html]
[test_presentation_mixed_security_contexts.html]

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

@ -0,0 +1,82 @@
<!DOCTYPE HTML>
<html>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<head>
<meta charset="utf-8">
<title>Test default request for B2G Presentation API at sender side</title>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268758">Test allow-presentation sandboxing flag</a>
<iframe id="iframe" src="https://example.com/tests/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html"></iframe>
<script type="application/javascript;version=1.8">
"use strict";
var iframe = document.getElementById("iframe");
var readyToStart = false;
var testSetuped = false;
function setup() {
SpecialPowers.addPermission("presentation",
true, { url: "https://example.com/tests/dom/presentation/tests/mochitest/file_presentation_mixed_security_contexts.html",
originAttributes: {
appId: SpecialPowers.Ci.nsIScriptSecurityManager.NO_APP_ID,
inIsolatedMozBrowser: false }});
return new Promise(function(aResolve, aReject) {
addEventListener("message", function listener(event) {
var message = event.data;
if (/^OK /.exec(message)) {
ok(true, message.replace(/^OK /, ""));
} else if (/^KO /.exec(message)) {
ok(false, message.replace(/^KO /, ""));
} else if (/^INFO /.exec(message)) {
info(message.replace(/^INFO /, ""));
} else if (/^COMMAND /.exec(message)) {
var command = JSON.parse(message.replace(/^COMMAND /, ""));
if (command === "ready-to-start") {
readyToStart = true;
startTest();
}
} else if (/^DONE$/.exec(message)) {
window.removeEventListener('message', listener);
SimpleTest.finish();
}
}, false);
testSetuped = true;
aResolve();
});
}
iframe.onload = startTest();
function startTest() {
if (!(testSetuped && readyToStart)) {
return;
}
iframe.contentWindow.postMessage("start", "*");
}
function runTests() {
ok(navigator.presentation, "navigator.presentation should be available.");
setup().then(startTest);
}
SimpleTest.waitForExplicitFinish();
SpecialPowers.pushPermissions([
{type: "presentation-device-manage", allow: false, context: document},
{type: "presentation", allow: true, context: document},
{type: "presentation", allow: true, context: iframe.contentDocument},
], function() {
SpecialPowers.pushPrefEnv({ "set": [["dom.presentation.enabled", true],
["dom.presentation.session_transport.data_channel.enable", false]]},
runTests);
});
</script>
</body>
</html>

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

@ -20,7 +20,7 @@ var connection;
function testSetup() {
return new Promise(function(aResolve, aReject) {
request = new PresentationRequest("http://example.com");
request = new PresentationRequest("https://example.com");
request.getAvailability().then(
function(aAvailability) {

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

@ -20,7 +20,7 @@ var connection;
function testSetup() {
return new Promise(function(aResolve, aReject) {
request = new PresentationRequest("http://example.com/");
request = new PresentationRequest("https://example.com");
request.getAvailability().then(
function(aAvailability) {
@ -178,7 +178,7 @@ function testReconnect() {
gScript.addMessageListener('start-reconnect', function startReconnectHandler(url) {
gScript.removeMessageListener('start-reconnect', startReconnectHandler);
is(url, "http://example.com/", "URLs should be the same.")
is(url, "https://example.com/", "URLs should be the same.")
gScript.sendAsyncMessage('trigger-reconnected-acked', url);
});

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

@ -19,7 +19,7 @@ var connection;
function testSetup() {
return new Promise(function(aResolve, aReject) {
navigator.presentation.defaultRequest = new PresentationRequest("http://example.com");
navigator.presentation.defaultRequest = new PresentationRequest("https://example.com");
navigator.presentation.defaultRequest.getAvailability().then(
function(aAvailability) {