Bug 558431 - Make fetching CSP policy-uri asyn, r=jst, a=blocking-betaN

This commit is contained in:
Brandon Sterne 2011-02-15 09:05:02 -08:00
Родитель 6eda66dc66
Коммит 7c7be27025
8 изменённых файлов: 251 добавлений и 65 удалений

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

@ -118,6 +118,57 @@ function CSPdebug(aMsg) {
.logStringMessage(aMsg);
}
// Callback to resume a request once the policy-uri has been fetched
function CSPPolicyURIListener(policyURI, docRequest, csp) {
this._policyURI = policyURI; // location of remote policy
this._docRequest = docRequest; // the parent document request
this._csp = csp; // parent document's CSP
this._policy = ""; // contents fetched from policyURI
this._wrapper = null; // nsIScriptableInputStream
this._docURI = docRequest.QueryInterface(Components.interfaces.nsIChannel)
.originalURI; // parent document URI (to be used as 'self')
}
CSPPolicyURIListener.prototype = {
QueryInterface: function(iid) {
if (iid.equals(Components.interfaces.nsIStreamListener) ||
iid.equals(Components.interfaces.nsIRequestObserver) ||
iid.equals(Components.interfaces.nsISupports))
return this;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
onStartRequest:
function(request, context) {},
onDataAvailable:
function(request, context, inputStream, offset, count) {
if (this._wrapper == null) {
this._wrapper = Components.classes["@mozilla.org/scriptableinputstream;1"]
.createInstance(Components.interfaces.nsIScriptableInputStream);
this._wrapper.init(inputStream);
}
// store the remote policy as it becomes available
this._policy += this._wrapper.read(count);
},
onStopRequest:
function(request, context, status) {
if (Components.isSuccessCode(status)) {
// send the policy we received back to the parent document's CSP
// for parsing
this._csp.refinePolicy(this._policy, this._docURI, this._docRequest);
}
else {
// problem fetching policy so fail closed
this._csp.refinePolicy("allow 'none'", null, this._docURI, this._docRequest);
}
// resume the parent document request
this._docRequest.resume();
}
};
//:::::::::::::::::::::::: CLASSES :::::::::::::::::::::::::://
/**
@ -162,10 +213,15 @@ CSPRep.OPTIONS_DIRECTIVE = "options";
* string rep of a CSP
* @param self (optional)
* string or CSPSource representing the "self" source
* @param docRequest (optional)
* request for the parent document which may need to be suspended
* while the policy-uri is asynchronously fetched
* @param csp (optional)
* the CSP object to update once the policy has been fetched
* @returns
* an instance of CSPRep
*/
CSPRep.fromString = function(aStr, self) {
CSPRep.fromString = function(aStr, self, docRequest, csp) {
var SD = CSPRep.SRC_DIRECTIVES;
var UD = CSPRep.URI_DIRECTIVES;
var aCSPR = new CSPRep();
@ -284,6 +340,12 @@ CSPRep.fromString = function(aStr, self) {
CSPError("policy-uri directive can only appear alone");
return CSPRep.fromString("allow 'none'");
}
// if we were called without a reference to the parent document request
// we won't be able to suspend it while we fetch the policy -> fail closed
if (!docRequest || !csp) {
CSPError("The policy-uri cannot be fetched without a parent request and a CSP.");
return CSPRep.fromString("allow 'none'");
}
var uri = '';
try {
@ -309,31 +371,25 @@ CSPRep.fromString = function(aStr, self) {
}
}
var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Components.interfaces.nsIXMLHttpRequest);
// insert error hook
req.onerror = CSPError;
// synchronous -- otherwise we need to architect a callback into the
// xpcom component so that whomever creates the policy object gets
// notified when it's loaded and ready to go.
req.open("GET", uri.asciiSpec, false);
// make request anonymous
// This prevents sending cookies with the request, in case the policy URI
// is injected, it can't be abused for CSRF.
req.channel.loadFlags |= Components.interfaces.nsIChannel.LOAD_ANONYMOUS;
req.send(null);
if (req.status == 200) {
aCSPR = CSPRep.fromString(req.responseText, self);
// remember where we got the policy
aCSPR._directives[UD.POLICY_URI] = dirvalue;
return aCSPR;
// suspend the parent document request while we fetch the policy-uri
try {
docRequest.suspend();
var chan = gIoService.newChannel(uri.asciiSpec, null, null);
// make request anonymous (no cookies, etc.) so the request for the
// policy-uri can't be abused for CSRF
chan.loadFlags |= Components.interfaces.nsIChannel.LOAD_ANONYMOUS;
chan.asyncOpen(new CSPPolicyURIListener(uri, docRequest, csp), null);
}
CSPError("Error fetching policy URI: server response was " + req.status);
return CSPRep.fromString("allow 'none'");
catch (e) {
// resume the document request and apply most restrictive policy
docRequest.resume();
CSPError("Error fetching policy-uri: " + e);
return CSPRep.fromString("allow 'none'");
}
// return a fully-open policy to be intersected with the contents of the
// policy-uri when it returns
return CSPRep.fromString("allow *");
}
// UNIDENTIFIED DIRECTIVE /////////////////////////////////////////////

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

@ -70,6 +70,7 @@ function ContentSecurityPolicy() {
this._requestHeaders = [];
this._request = "";
this._docRequest = null;
CSPdebug("CSP POLICY INITED TO 'allow *'");
}
@ -200,6 +201,7 @@ ContentSecurityPolicy.prototype = {
}
this._request = aChannel.requestMethod + " " + aChannel.URI.asciiSpec;
this._docRequest = aChannel;
// We will only be able to provide the HTTP version information if aChannel
// implements nsIHttpChannelInternal
@ -244,7 +246,9 @@ ContentSecurityPolicy.prototype = {
// If there is a policy-uri, fetch the policy, then re-call this function.
// (1) parse and create a CSPRep object
var newpolicy = CSPRep.fromString(aPolicy,
selfURI.scheme + "://" + selfURI.hostPort);
selfURI.scheme + "://" + selfURI.hostPort,
this._docRequest,
this);
// (2) Intersect the currently installed CSPRep object with the new one
var intersect = this._policy.intersectWith(newpolicy);

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

@ -465,6 +465,9 @@ _TEST_FILES2 = \
bug466080.sjs \
test_bug625722.html \
test_bug631615.html \
test_bug558431.html \
file_bug558431.html \
file_bug558431.html^headers^ \
$(NULL)
# This test fails on the Mac for some reason

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

@ -0,0 +1,2 @@
<iframe id="inner"
src="/tests/content/base/test/file_CSP.sjs?content=%3Cdiv%20id%3D%22test%22%3Etest%20558431%3C/div%3E"></iframe>

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

@ -0,0 +1 @@
X-Content-Security-Policy: invalid-uri

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

@ -0,0 +1,32 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for CSP async policy-uri</title>
<script type="text/javascript" src="/MochiKit/packed.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<iframe id="cspframe"></iframe>
<script type="text/javascript">
// This tests that a policy is still attempted to be fetched
// asynchronously (bug 558431) and that a default policy of
// |allow 'none'| is applied when the fetching fails.
var f = document.getElementById("cspframe");
// run our test functions when the inner frame is finished loading
f.addEventListener('load', function() {
var inner = this.contentDocument.getElementById("inner");
var test = inner.contentDocument.getElementById("test");
// the inner document should not exist because it has an invalid
// policy-uri and should have been blocked by the default
// |allow 'none'| policy that was applied
is(test, null, "test inner document");
SimpleTest.finish();
}, false);
// load the test frame
f.src = "file_bug558431.html";
SimpleTest.waitForExplicitFinish();
</script>
</body>
</html>

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

@ -0,0 +1,127 @@
Components.utils.import('resource://gre/modules/CSPUtils.jsm');
do_load_httpd_js();
var httpserv = null;
const POLICY_FROM_URI = "allow 'self'; img-src *";
const POLICY_PORT = 9000;
const POLICY_URI = "http://localhost:" + POLICY_PORT + "/policy";
const POLICY_URI_RELATIVE = "/policy";
const DOCUMENT_URI = "http://localhost:" + POLICY_PORT + "/document";
const CSP_DOC_BODY = "CSP doc content";
const SD = CSPRep.SRC_DIRECTIVES;
const MAX_TESTS = 2;
var TESTS_COMPLETED = 0;
var cspr, cspr_static;
// helper to use .equals on stuff
function do_check_equivalent(foo, bar, stack) {
if (!stack)
stack = Components.stack.caller;
var text = foo + ".equals(" + bar + ")";
if (foo.equals && foo.equals(bar)) {
dump("TEST-PASS | " + stack.filename + " | [" + stack.name + " : " +
stack.lineNumber + "] " + text + "\n");
return;
}
do_throw(text, stack);
}
function listener() {
this.buffer = "";
}
listener.prototype = {
onStartRequest: function (request, ctx) {
},
onDataAvailable: function (request, ctx, stream, offset, count) {
var sInputStream = Cc["@mozilla.org/scriptableinputstream;1"]
.createInstance(Ci.nsIScriptableInputStream);
sInputStream.init(stream);
this.buffer = this.buffer.concat(sInputStream.read(count));
},
onStopRequest: function (request, ctx, status) {
// make sure that we have the full document content, guaranteeing that
// the document channel has been resumed, before we do the comparisons
if (this.buffer == CSP_DOC_BODY) {
// "policy-uri failed to load"
do_check_neq(null, cspr);
// other directives inherit self
for (var i in SD) {
do_check_equivalent(cspr._directives[SD[i]],
cspr_static._directives[SD[i]]);
}
do_test_finished();
TESTS_COMPLETED++;
// final teardown
if (TESTS_COMPLETED == MAX_TESTS) {
httpserv.stop(function(){});
}
}
}
};
function run_test() {
httpserv = new nsHttpServer();
httpserv.registerPathHandler("/document", csp_doc_response);
httpserv.registerPathHandler("/policy", csp_policy_response);
httpserv.start(POLICY_PORT);
var tests = [ test_CSPRep_fromPolicyURI, test_CSPRep_fromRelativePolicyURI];
for (var i = 0 ; i < tests.length ; i++) {
tests[i]();
do_test_pending();
}
}
function makeChan(url) {
var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
var chan = ios.newChannel(url, null, null).QueryInterface(Ci.nsIHttpChannel);
return chan;
}
function csp_doc_response(metadata, response) {
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.setHeader("Content-Type", "text/html", false);
response.bodyOutputStream.write(CSP_DOC_BODY, CSP_DOC_BODY.length);
}
function csp_policy_response(metadata, response) {
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.setHeader("Content-Type", "text/csp", false);
response.bodyOutputStream.write(POLICY_FROM_URI, POLICY_FROM_URI.length);
}
///////////////////// TEST POLICY_URI //////////////////////
function test_CSPRep_fromPolicyURI() {
var csp = Components.classes["@mozilla.org/contentsecuritypolicy;1"]
.createInstance[Components.interfaces.nsIContentSecurityPolicy];
// once the policy-uri is returned we will compare our static CSPRep with one
// we generated from the content we got back from the network to make sure
// they are equivalent
cspr_static = CSPRep.fromString(POLICY_FROM_URI, DOCUMENT_URI);
// simulates the request for the parent document
var docChan = makeChan(DOCUMENT_URI);
docChan.asyncOpen(new listener(), null);
cspr = CSPRep.fromString("policy-uri " + POLICY_URI, DOCUMENT_URI, docChan, csp);
}
function test_CSPRep_fromRelativePolicyURI() {
var csp = Components.classes["@mozilla.org/contentsecuritypolicy;1"]
.createInstance[Components.interfaces.nsIContentSecurityPolicy];
// once the policy-uri is returned we will compare our static CSPRep with one
// we generated from the content we got back from the network to make sure
// they are equivalent
cspr_static = CSPRep.fromString(POLICY_FROM_URI, DOCUMENT_URI);
// simulates the request for the parent document
var docChan = makeChan(DOCUMENT_URI);
docChan.asyncOpen(new listener(), null);
cspr = CSPRep.fromString("policy-uri " + POLICY_URI_RELATIVE, DOCUMENT_URI, docChan, csp);
}

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

@ -461,45 +461,6 @@ test(function test_CSPRep_fromString_withself() {
do_check_true(cspr.permits("https://evil.com:100", SD.SCRIPT_SRC));
});
///////////////////// TEST POLICY_URI //////////////////////
test(function test_CSPRep_fromPolicyURI() {
var cspr;
var SD = CSPRep.SRC_DIRECTIVES;
var self = "http://localhost:" + POLICY_PORT;
cspr = CSPRep.fromString("policy-uri " + POLICY_URI, self);
cspr_static = CSPRep.fromString(POLICY_FROM_URI, self);
//"policy-uri failed to load"
do_check_neq(null,cspr);
// other directives inherit self
for(var i in SD) {
//SD[i] + " parsed wrong from policy uri"
do_check_equivalent(cspr._directives[SD[i]],
cspr_static._directives[SD[i]]);
}
});
test(function test_CSPRep_fromRelativePolicyURI() {
var cspr;
var SD = CSPRep.SRC_DIRECTIVES;
var self = "http://localhost:" + POLICY_PORT;
cspr = CSPRep.fromString("policy-uri " + POLICY_URI_RELATIVE, self);
cspr_static = CSPRep.fromString(POLICY_FROM_URI, self);
//"policy-uri failed to load"
do_check_neq(null,cspr);
// other directives inherit self
for(var i in SD) {
//SD[i] + " parsed wrong from policy uri"
do_check_equivalent(cspr._directives[SD[i]],
cspr_static._directives[SD[i]]);
}
});
//////////////// TEST FRAME ANCESTOR DEFAULTS /////////////////
// (see bug 555068)
test(function test_FrameAncestor_defaults() {