Bug 515460 - Mochitests for CSP redirect handling, a=dholbert_sheriff
--- a/content/base/test/Makefile.in
+++ b/content/base/test/Makefile.in
@@ -368,16 +368,20 @@ include $(topsrcdir)/config/rules.mk
test_bug553896.xhtml \
test_bug541937.html \
file_bug541937.html \
file_bug541937.xhtml \
test_bug558726.html \
test_bug557892.html \
file_bug557892.html \
test_bug559526.html \
+ test_csp_redirects.html \
+ file_csp_redirects_page.sjs \
+ file_csp_redirects_main.html \
+ file_csp_redirects_resource.sjs \
$(NULL)
# This test fails on the Mac for some reason
ifneq (,$(filter gtk2 windows,$(MOZ_WIDGET_TOOLKIT)))
_TEST_FILES += test_copyimage.html \
$(NULL)
endif
new file mode 100644
--- /dev/null
+++ b/content/base/test/file_csp_redirects_main.html
@@ -0,0 +1,35 @@
+<html>
+<head>
+<title>CSP redirect tests</title>
+</head>
+<body>
+<div id="container"></div>
+</body>
+
+<script>
+var thisSite = "http://mochi.test:8888";
+var otherSite = "http://example.com";
+var page = "/tests/content/base/test/file_csp_redirects_page.sjs";
+
+var tests = { "font-src": thisSite+page+"?testid=font-src&csp=1",
+ "frame-src": thisSite+page+"?testid=frame-src&csp=1",
+ "img-src": thisSite+page+"?testid=img-src&csp=1",
+ "media-src": thisSite+page+"?testid=media-src&csp=1",
+ "object-src": thisSite+page+"?testid=object-src&csp=1",
+ "script-src": thisSite+page+"?testid=script-src&csp=1",
+ "style-src": thisSite+page+"?testid=style-src&csp=1",
+ "worker": thisSite+page+"?testid=worker&csp=1",
+ "xhr-src": thisSite+page+"?testid=xhr-src&csp=1",
+ };
+
+var container = document.getElementById("container");
+
+// load each test in its own iframe
+for (tid in tests) {
+ var i = document.createElement("iframe");
+ i.id = tid;
+ i.src = tests[tid];
+ container.appendChild(i);
+}
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/content/base/test/file_csp_redirects_page.sjs
@@ -0,0 +1,79 @@
+// SJS file for CSP redirect mochitests
+// This file serves pages which can optionally specify a Content Security Policy
+function handleRequest(request, response)
+{
+ var query = {};
+ request.queryString.split('&').forEach(function (val) {
+ var [name, value] = val.split('=');
+ query[name] = unescape(value);
+ });
+
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "text/html", false);
+
+ var resource = "/tests/content/base/test/file_csp_redirects_resource.sjs";
+
+ // CSP header value
+ if (query["csp"] == 1) {
+ response.setHeader("X-Content-Security-Policy", "allow 'self'", false);
+ }
+
+ // downloadable font that redirects to another site
+ if (query["testid"] == "font-src") {
+ var resp = '<style type="text/css"> @font-face { font-family:' +
+ '"Redirecting Font"; src: url("' + resource +
+ '?res=font&redir=other&id=font-src-redir")} #test{font-family:' +
+ '"Redirecting Font"}</style></head><body>' +
+ '<div id="test">test</div></body>';
+ response.write(resp);
+ return;
+ }
+
+ // iframe that redirects to another site
+ if (query["testid"] == "frame-src") {
+ response.write('<iframe src="'+resource+'?res=iframe&redir=other&id=frame-src-redir"></iframe>');
+ return;
+ }
+
+ // image that redirects to another site
+ if (query["testid"] == "img-src") {
+ response.write('<img src="'+resource+'?res=image&redir=other&id=img-src-redir" />');
+ return;
+ }
+
+ // video content that redirects to another site
+ if (query["testid"] == "media-src") {
+ response.write('<video src="'+resource+'?res=media&redir=other&id=media-src-redir"></video>');
+ return;
+ }
+
+ // object content that redirects to another site
+ if (query["testid"] == "object-src") {
+ response.write('<object type="text/html" data="'+resource+'?res=object&redir=other&id=object-src-redir"></object>');
+ return;
+ }
+
+ // external script that redirects to another site
+ if (query["testid"] == "script-src") {
+ response.write('<script src="'+resource+'?res=script&redir=other&id=script-src-redir"></script>');
+ return;
+ }
+
+ // external stylesheet that redirects to another site
+ if (query["testid"] == "style-src") {
+ response.write('<link rel="stylesheet" type="text/css" href="'+resource+'?res=style&redir=other&id=style-src-redir"></script>');
+ return;
+ }
+
+ // worker script resource that redirects to another site
+ if (query["testid"] == "worker") {
+ response.write('<script src="'+resource+'?res=worker&redir=other&id=worker-redir"></script>');
+ return;
+ }
+
+ // script that XHR's to a resource that redirects to another site
+ if (query["testid"] == "xhr-src") {
+ response.write('<script src="'+resource+'?res=xhr"></script>');
+ return;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/content/base/test/file_csp_redirects_resource.sjs
@@ -0,0 +1,112 @@
+// SJS file to serve resources for CSP redirect tests
+// This file mimics serving resources, e.g. fonts, images, etc., which a CSP
+// can include. The resource may redirect to a different resource, if specified.
+function handleRequest(request, response)
+{
+ var query = {};
+ request.queryString.split('&').forEach(function (val) {
+ var [name, value] = val.split('=');
+ query[name] = unescape(value);
+ });
+
+ var thisSite = "http://mochi.test:8888";
+ var otherSite = "http://example.com";
+ var resource = "/tests/content/base/test/file_csp_redirects_resource.sjs";
+
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // redirect to a resource on this site
+ if (query["redir"] == "same") {
+ var loc = thisSite+resource+"?res="+query["res"]+"&testid="+query["id"];
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", loc, false);
+ return;
+ }
+
+ // redirect to a resource on a different site
+ else if (query["redir"] == "other") {
+ var loc = otherSite+resource+"?res="+query["res"]+"&testid="+query["id"];
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader("Location", loc, false);
+ return;
+ }
+
+ // not a redirect. serve some content.
+ // the content doesn't have to be valid, since we're only checking whether
+ // the request for the content was sent or not.
+
+ // downloadable font
+ if (query["res"] == "font") {
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Content-Type", "text/plain", false);
+ response.write("font data...");
+ return;
+ }
+
+ // iframe with arbitrary content
+ if (query["res"] == "iframe") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("iframe content...");
+ return;
+ }
+
+ // image
+ if (query["res"] == "image") {
+ response.setHeader("Content-Type", "image/gif", false);
+ response.write("image data...");
+ return;
+ }
+
+ // media content, e.g. Ogg video
+ if (query["res"] == "media") {
+ response.setHeader("Content-Type", "video/ogg", false);
+ response.write("video data...");
+ return;
+ }
+
+ // plugin content, e.g. <object>
+ if (query["res"] == "object") {
+ response.setHeader("Content-Type", "text/html", false);
+ response.write("object data...");
+ return;
+ }
+
+ // script
+ if (query["res"] == "script") {
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.write("some script...");
+ return;
+ }
+
+ // external stylesheet
+ if (query["res"] == "style") {
+ response.setHeader("Content-Type", "text/css", false);
+ response.write("css data...");
+ return;
+ }
+
+ // web worker resource
+ if (query["res"] == "worker") {
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.write("worker script data...");
+ return;
+ }
+
+ // script that invokes XHR
+ if (query["res"] == "xhr") {
+ response.setHeader("Content-Type", "text/html", false);
+ var resp = 'var x = new XMLHttpRequest(); x.open("GET", "' + otherSite +
+ resource+'?res=xhr-resp&testid=xhr-src-redir", false); ' +
+ 'x.send(null);';
+ response.write(resp);
+ return;
+ }
+
+ // response to XHR
+ if (query["res"] == "xhr-resp") {
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Content-Type", "text/html", false);
+ response.write('XHR response...');
+ return;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/content/base/test/test_csp_redirects.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Tests for Content Security Policy during redirects</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>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+
+<iframe style="width:100%;height:300px;" id="harness"></iframe>
+<pre id="log"></pre>
+<script class="testbody" type="text/javascript">
+
+var path = "/tests/content/base/test/";
+
+// debugging
+function log(s) {
+ return;
+ var log = document.getElementById("log");
+ log.textContent = log.textContent+s+"\n";
+}
+
+// used to watch if requests are blocked by CSP or allowed through
+function examiner() {
+ netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+ var obsvc = Components.classes['@mozilla.org/observer-service;1']
+ .getService(Components.interfaces.nsIObserverService);
+ obsvc.addObserver(this, "csp-on-violate-policy", false);
+ obsvc.addObserver(this, "http-on-modify-request", false);
+}
+examiner.prototype = {
+ observe: function(subject, topic, data) {
+ netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+ // subject should be an nsURI, and should be either allowed or blocked.
+ if(!subject.QueryInterface)
+ return;
+
+ var testpat = new RegExp("testid=([a-z0-9-]+)");
+ var uri;
+ var testid;
+
+ if (topic === "http-on-modify-request") {
+ // request was sent
+ uri = subject.QueryInterface(Components.interfaces.nsIHttpChannel).URI;
+ if (!testpat.test(uri.asciiSpec)) return;
+ testid = testpat.exec(uri.asciiSpec)[1];
+ if (testExpectedResults[testid] == "completed") return;
+ log("allowed: "+uri.asciiSpec);
+ window.testResult(testid, uri.asciiSpec, true);
+ }
+
+ else if (topic === "csp-on-violate-policy") {
+ // request was blocked
+ uri = subject.QueryInterface(Components.interfaces.nsIURI);
+ if (!testpat.test(uri.asciiSpec)) return;
+ testid = testpat.exec(uri.asciiSpec)[1];
+ // had to add this check because http-on-modify-request can fire after
+ // csp-on-violate-policy, apparently, even though the request does
+ // not hit the wire.
+ if (testExpectedResults[testid] == "completed") return;
+ log("BLOCKED: "+uri.asciiSpec);
+ window.testResult(testid, uri.asciiSpec, false);
+ }
+ },
+
+ remove: function() {
+ netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+ var obsvc = Components.classes['@mozilla.org/observer-service;1']
+ .getService(Components.interfaces.nsIObserverService);
+ obsvc.removeObserver(this, "csp-on-violate-policy");
+ obsvc.removeObserver(this, "http-on-modify-request");
+ }
+}
+window.examiner = new examiner();
+
+// contains { test_frame_id : expected_result }
+var testExpectedResults = { "font-src": true,
+ "font-src-redir": false,
+ "frame-src": true,
+ "frame-src-redir": false,
+ "img-src": true,
+ "img-src-redir": false,
+ "media-src": true,
+ "media-src-redir": false,
+ "object-src": true,
+ "object-src-redir": false,
+ "script-src": true,
+ "script-src-redir": false,
+ "style-src": true,
+ "style-src-redir": false,
+ "worker": true,
+ "worker-redir": false,
+ "xhr-src": true,
+ "xhr-src-redir": false,
+ };
+
+// takes the name of the test, the URL that was tested, and whether the
+// load occured
+var testResult = function(testName, url, result) {
+ netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+ log(" testName: "+testName+", result: "+result+", expected: "+testExpectedResults[testName]+"\n");
+ is(result, testExpectedResults[testName], testName+" test: "+url);
+
+ // mark test as completed
+ testExpectedResults[testName] = "completed";
+
+ // don't finish until we've run all the tests
+ for (var t in testExpectedResults) {
+ if (testExpectedResults[t] != "completed")
+ return;
+ }
+
+ window.examiner.remove();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// load the test harness
+document.getElementById("harness").src = "file_csp_redirects_main.html";
+
+</script>
+</pre>
+
+</body>
+</html>