Bug 650386 - CSP should not follow redirects for report-uri r=jst
authorIan Melven <imelven@mozilla.com>
Tue, 27 Mar 2012 10:55:50 -0700
changeset 90435 2f79b816b0daea744fb01775d3021d9defdf2813
parent 90434 f6b7b4b9f235b18ec544d7477f3fc563fd180dcf
child 90436 3c6e6afdfd4a0bbbb6fb19da1ba5de108a5c4f0e
push id22358
push userkhuey@mozilla.com
push dateWed, 28 Mar 2012 14:41:10 +0000
treeherdermozilla-central@c3fd0768d46a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjst
bugs650386
milestone14.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 650386 - CSP should not follow redirects for report-uri r=jst
content/base/src/contentSecurityPolicy.js
content/base/test/Makefile.in
content/base/test/file_bug650386_content.sjs
content/base/test/file_bug650386_report.sjs
content/base/test/test_bug650386_redirect_301.html
content/base/test/test_bug650386_redirect_302.html
content/base/test/test_bug650386_redirect_303.html
content/base/test/test_bug650386_redirect_307.html
--- a/content/base/src/contentSecurityPolicy.js
+++ b/content/base/src/contentSecurityPolicy.js
@@ -14,16 +14,17 @@
  * The Original Code is the ContentSecurityPolicy module.
  *
  * The Initial Developer of the Original Code is
  *   Mozilla Corporation
  *
  * Contributor(s):
  *   Sid Stamm <sid@mozilla.com>
  *   Brandon Sterne <bsterne@mozilla.com>
+ *   Ian Melven <imelven@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -296,28 +297,32 @@ ContentSecurityPolicy.prototype = {
       // should be taken care of in CSPRep.fromString (where it converts any
       // relative URIs into absolute ones based on "self").
       for (let i in uris) {
         if (uris[i] === "")
           continue;
 
         var failure = function(aEvt) {  
           if (req.readyState == 4 && req.status != 200) {
-            CSPError("Failed to send report to " + reportURI);
+            CSPError("Failed to send report to " + uris[i]);
           }  
         };  
         var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]  
                     .createInstance(Ci.nsIXMLHttpRequest);  
 
         try {
           req.open("POST", uris[i], true);
           req.setRequestHeader('Content-Type', 'application/json');
           req.upload.addEventListener("error", failure, false);
           req.upload.addEventListener("abort", failure, false);
 
+          // we need to set an nsIChannelEventSink on the XHR object
+          // so we can tell it to not follow redirects when posting the reports
+          req.channel.notificationCallbacks = new CSPReportRedirectSink();
+
           req.send(JSON.stringify(report));
           CSPdebug("Sent violation report to " + uris[i]);
         } catch(e) {
           // it's possible that the URI was invalid, just log a
           // warning and skip over that.
           CSPWarning("Tried to send report to invalid URI: \"" + uris[i] + "\"");
         }
       }
@@ -489,9 +494,59 @@ ContentSecurityPolicy.prototype = {
                                      CSP_VIOLATION_TOPIC,
                                      violatedDirective);
         reportSender.sendReports(blockedContentSource, violatedDirective,
                                  aSourceFile, aScriptSample, aLineNum);
       }, Ci.nsIThread.DISPATCH_NORMAL);
   },
 };
 
+// The POST of the violation report (if it happens) should not follow
+// redirects, per the spec. hence, we implement an nsIChannelEventSink
+// with an object so we can tell XHR to abort if a redirect happens.
+function CSPReportRedirectSink() {
+}
+
+CSPReportRedirectSink.prototype = {
+  QueryInterface: function requestor_qi(iid) {
+    if (iid.equals(Ci.nsISupports) ||
+        iid.equals(Ci.nsIInterfaceRequestor) ||
+        iid.equals(Ci.nsIChannelEventSink))
+      return this;
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  },
+
+  // nsIInterfaceRequestor
+  getInterface: function requestor_gi(iid) {
+    if (iid.equals(Ci.nsIChannelEventSink))
+      return this;
+
+    throw Components.results.NS_ERROR_NO_INTERFACE;
+  },
+
+  // nsIChannelEventSink
+  asyncOnChannelRedirect: function channel_redirect(oldChannel, newChannel,
+                                                    flags, callback) {
+    CSPWarning("Post of violation report to " + oldChannel.URI.asciiSpec +
+               " failed, as a redirect occurred");
+
+    // cancel the old channel so XHR failure callback happens
+    oldChannel.cancel(Cr.NS_ERROR_ABORT);
+
+    // notify an observer that we have blocked the report POST due to a redirect,
+    // used in testing, do this async since we're in an async call now to begin with
+    Services.tm.mainThread.dispatch(
+      function() {
+        observerSubject = Cc["@mozilla.org/supports-cstring;1"]
+                             .createInstance(Ci.nsISupportsCString);
+        observerSubject.data = oldChannel.URI.asciiSpec;
+
+        Services.obs.notifyObservers(observerSubject,
+                                     CSP_VIOLATION_TOPIC,
+                                     "denied redirect while sending violation report");
+      }, Ci.nsIThread.DISPATCH_NORMAL);
+
+    // throw to stop the redirect happening
+    throw Cr.NS_BINDING_REDIRECTED;
+  }
+};
+
 var NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentSecurityPolicy]);
--- a/content/base/test/Makefile.in
+++ b/content/base/test/Makefile.in
@@ -567,16 +567,22 @@ include $(topsrcdir)/config/rules.mk
 		test_bug696301-2.html \
 		bug696301-script-1.js \
 		bug696301-script-1.js^headers^ \
 		bug696301-script-2.js \
 		test_bug737565.html \
 		test_bug737612.html \
 		test_bug738108.html \
 		test_bug366944.html \
+		test_bug650386_redirect_301.html \
+		test_bug650386_redirect_302.html \
+		test_bug650386_redirect_303.html \
+		test_bug650386_redirect_307.html \
+		file_bug650386_content.sjs \
+		file_bug650386_report.sjs \
 		$(NULL)
 
 _CHROME_FILES =	\
 		test_bug357450.js \
 		$(NULL)
 
 # This test fails on the Mac for some reason
 ifneq (,$(filter gtk2 windows,$(MOZ_WIDGET_TOOLKIT)))
new file mode 100644
--- /dev/null
+++ b/content/base/test/file_bug650386_content.sjs
@@ -0,0 +1,37 @@
+// SJS file for tests for bug650386, serves file_bug650386_content.html
+// with a CSP that will trigger a violation and that will report it
+// to file_bug650386_report.sjs
+//
+// This handles 301, 302, 303 and 307 redirects. The HTTP status code
+// returned/type of redirect to do comes from the query string
+// parameter passed in from the test_bug650386_* files and then also
+// uses that value in the report-uri parameter of the CSP
+function handleRequest(request, response) {
+  response.setHeader("Cache-Control", "no-cache", false);
+
+  // this gets used in the CSP as part of the report URI.
+  var redirect = request.queryString;
+
+  if (redirect < 301 || (redirect > 303 && redirect <= 306) || redirect > 307) {
+    // if we somehow got some bogus redirect code here,
+    // do a 302 redirect to the same URL as the report URI
+    // redirects to - this will fail the test.
+    var loc = "http://example.com/some/fake/path";
+    response.setStatusLine("1.1", 302, "Found");
+    response.setHeader("Location", loc, false);
+    return;
+  }
+
+  var csp = "default-src \'self\';report-uri http://mochi.test:8888/tests/content/base/test/file_bug650386_report.sjs?" + redirect;
+
+  response.setHeader("X-Content-Security-Policy", csp, false);
+
+  // the actual file content.
+  // this image load will (intentionally) fail due to the CSP policy of default-src: 'self'
+  // specified by the CSP string above.
+  var content = "<!DOCTYPE HTML><html><body><img src = \"http://some.other.domain.example.com\"></body></html>";
+
+  response.write(content);
+
+  return;
+}
new file mode 100644
--- /dev/null
+++ b/content/base/test/file_bug650386_report.sjs
@@ -0,0 +1,16 @@
+// SJS file for tests for bug650386, this serves as CSP violation report target
+// and issues a redirect, to make sure the browser does not post to the target
+// of the redirect, per CSP spec.
+// This handles 301, 302, 303 and 307 redirects. The HTTP status code
+// returned/type of redirect to do comes from the query string
+// parameter
+function handleRequest(request, response) {
+  response.setHeader("Cache-Control", "no-cache", false);
+
+  var redirect = request.queryString;
+
+  var loc = "http://example.com/some/fake/path";
+  response.setStatusLine("1.1", redirect, "Found");
+  response.setHeader("Location", loc, false);
+  return;
+}
new file mode 100644
--- /dev/null
+++ b/content/base/test/test_bug650386_redirect_301.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+Test that CSP violation reports are not sent when a 301 redirect is encountered
+-->
+<head>
+  <title>Test for Bug 650386</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.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=650386">Mozilla Bug 650386</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id = "content_iframe"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 650386 **/
+
+// This is used to watch the redirect of the report POST get blocked
+function examiner() {
+  SpecialPowers.addObserver(this, "csp-on-violate-policy", false);
+  SpecialPowers.addObserver(this, "http-on-modify-request", false);
+}
+
+examiner.prototype  = {
+  observe: function(subject, topic, data) {
+    // subject should be an nsURI
+    if(!SpecialPowers.can_QI(subject))
+       return;
+
+    if (topic === "http-on-modify-request") {
+      // this is used to fail the test - if we see the POST to the target of the redirect
+      // we know this is a fail
+      var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIHttpChannel"), "URI.asciiSpec");
+
+      if (asciiSpec == "http://example.com/some/fake/path")
+        window.done(false);
+    }
+
+    if(topic === "csp-on-violate-policy") {
+      // something was blocked, but we are looking specifically for the redirect being blocked
+      if (data == "denied redirect while sending violation report")
+        window.done(true);
+    }
+  },
+
+  // must eventually call this to remove the listener,
+  // or mochitests might get borked.
+  remove: function() {
+    SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+    SpecialPowers.removeObserver(this, "http-on-modify-request");
+  }
+}
+
+window.examiner = new examiner();
+
+// result == true if we saw the redirect blocked notify, false if we saw the post
+// to the redirect target go out
+window.done = function(result) {
+  ok(result, "a 301 redirect when posting violation report should be blocked");
+
+  // clean up observers and finish the test
+  window.examiner.remove();
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+document.getElementById('content_iframe').src = 'file_bug650386_content.sjs?301';
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/content/base/test/test_bug650386_redirect_302.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+Test that CSP violation reports are not sent when a 302 redirect is encountered
+-->
+<head>
+  <title>Test for Bug 650386</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.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=650386">Mozilla Bug 650386</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id = "content_iframe"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 650386 **/
+
+// This is used to watch the redirect of the report POST get blocked
+function examiner() {
+  SpecialPowers.addObserver(this, "csp-on-violate-policy", false);
+  SpecialPowers.addObserver(this, "http-on-modify-request", false);
+}
+
+examiner.prototype  = {
+  observe: function(subject, topic, data) {
+    // subject should be an nsURI
+    if(!SpecialPowers.can_QI(subject))
+       return;
+
+    if (topic === "http-on-modify-request") {
+      // this is used to fail the test - if we see the POST to the target of the redirect
+      // we know this is a fail
+      var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIHttpChannel"), "URI.asciiSpec");
+
+      if (asciiSpec == "http://example.com/some/fake/path")
+        window.done(false);
+    }
+
+    if(topic === "csp-on-violate-policy") {
+      // something was blocked, but we are looking specifically for the redirect being blocked
+      if (data == "denied redirect while sending violation report")
+        window.done(true);
+    }
+  },
+
+  // must eventually call this to remove the listener,
+  // or mochitests might get borked.
+  remove: function() {
+    SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+    SpecialPowers.removeObserver(this, "http-on-modify-request");
+  }
+}
+
+window.examiner = new examiner();
+
+// result == true if we saw the redirect blocked notify, false if we saw the post
+// to the redirect target go out
+window.done = function(result) {
+  ok(result, "a 302 redirect when posting violation report should be blocked");
+
+  // clean up observers and finish the test
+  window.examiner.remove();
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+document.getElementById('content_iframe').src = 'file_bug650386_content.sjs?302';
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/content/base/test/test_bug650386_redirect_303.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+Test that CSP violation reports are not sent when a 303 redirect is encountered
+-->
+<head>
+  <title>Test for Bug 650386</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.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=650386">Mozilla Bug 650386</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id = "content_iframe"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 650386 **/
+
+// This is used to watch the redirect of the report POST get blocked
+function examiner() {
+  SpecialPowers.addObserver(this, "csp-on-violate-policy", false);
+  SpecialPowers.addObserver(this, "http-on-modify-request", false);
+}
+
+examiner.prototype  = {
+  observe: function(subject, topic, data) {
+    // subject should be an nsURI
+    if(!SpecialPowers.can_QI(subject))
+       return;
+
+    if (topic === "http-on-modify-request") {
+      // this is used to fail the test - if we see the POST to the target of the redirect
+      // we know this is a fail
+      var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIHttpChannel"), "URI.asciiSpec");
+
+      if (asciiSpec == "http://example.com/some/fake/path")
+        window.done(false);
+    }
+
+    if(topic === "csp-on-violate-policy") {
+      // something was blocked, but we are looking specifically for the redirect being blocked
+      if (data == "denied redirect while sending violation report")
+        window.done(true);
+    }
+  },
+
+  // must eventually call this to remove the listener,
+  // or mochitests might get borked.
+  remove: function() {
+    SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+    SpecialPowers.removeObserver(this, "http-on-modify-request");
+  }
+}
+
+window.examiner = new examiner();
+
+// result == true if we saw the redirect blocked notify, false if we saw the post
+// to the redirect target go out
+window.done = function(result) {
+  ok(result, "a 303 redirect when posting violation report should be blocked");
+
+  // clean up observers and finish the test
+  window.examiner.remove();
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+document.getElementById('content_iframe').src = 'file_bug650386_content.sjs?303';
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/content/base/test/test_bug650386_redirect_307.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650386
+Test that CSP violation reports are not sent when a 307 redirect is encountered
+-->
+<head>
+  <title>Test for Bug 650386</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.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=650386">Mozilla Bug 650386</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe id = "content_iframe"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 650386 **/
+
+// This is used to watch the redirect of the report POST get blocked
+function examiner() {
+  SpecialPowers.addObserver(this, "csp-on-violate-policy", false);
+  SpecialPowers.addObserver(this, "http-on-modify-request", false);
+}
+
+examiner.prototype  = {
+  observe: function(subject, topic, data) {
+    // subject should be an nsURI
+    if(!SpecialPowers.can_QI(subject))
+       return;
+
+    if (topic === "http-on-modify-request") {
+      // this is used to fail the test - if we see the POST to the target of the redirect
+      // we know this is a fail
+      var asciiSpec = SpecialPowers.getPrivilegedProps(SpecialPowers.do_QueryInterface(subject, "nsIHttpChannel"), "URI.asciiSpec");
+
+      if (asciiSpec == "http://example.com/some/fake/path")
+        window.done(false);
+    }
+
+    if(topic === "csp-on-violate-policy") {
+      // something was blocked, but we are looking specifically for the redirect being blocked
+      if (data == "denied redirect while sending violation report")
+        window.done(true);
+    }
+  },
+
+  // must eventually call this to remove the listener,
+  // or mochitests might get borked.
+  remove: function() {
+    SpecialPowers.removeObserver(this, "csp-on-violate-policy");
+    SpecialPowers.removeObserver(this, "http-on-modify-request");
+  }
+}
+
+window.examiner = new examiner();
+
+// result == true if we saw the redirect blocked notify, false if we saw the post
+// to the redirect target go out
+window.done = function(result) {
+  ok(result, "a 307 redirect when posting violation report should be blocked");
+
+  // clean up observers and finish the test
+  window.examiner.remove();
+  SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// save this for last so that our listeners are registered.
+document.getElementById('content_iframe').src = 'file_bug650386_content.sjs?307';
+</script>
+</pre>
+</body>
+</html>