Bug 558431 - make CSP policy-uri fetching asynchronous, r=jst, a=blocker
☠☠ backed out by 9797baa9246d ☠ ☠
authorBrandon Sterne <bsterne@mozilla.com>
Tue, 01 Feb 2011 15:17:06 -0800
changeset 61759 9ce4d80efab6876ed990089f97169d9984c696e0
parent 61758 84c8b33619e0f950e4f024f1fd3824891f0396e8
child 61760 9797baa9246d12c9c7e96627dd0494c15abcc5db
push id18473
push userbsterne@mozilla.com
push dateTue, 01 Feb 2011 23:17:53 +0000
treeherdermozilla-central@9ce4d80efab6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjst, blocker
bugs558431
milestone2.0b11pre
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 558431 - make CSP policy-uri fetching asynchronous, r=jst, a=blocker
content/base/src/CSPUtils.jsm
content/base/src/contentSecurityPolicy.js
content/base/test/Makefile.in
content/base/test/file_bug558431.html
content/base/test/file_bug558431.html^headers^
content/base/test/test_bug558431.html
--- a/content/base/src/CSPUtils.jsm
+++ b/content/base/src/CSPUtils.jsm
@@ -113,16 +113,67 @@ function CSPdebug(aMsg) {
   if (!gPrefObserver.debugEnabled) return;
 
   aMsg = 'CSP debug: ' + aMsg + "\n";
   Components.classes["@mozilla.org/consoleservice;1"]
                     .getService(Components.interfaces.nsIConsoleService)
                     .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 ::::::::::::::::::::::::::// 
 
 /**
  * Class that represents a parsed policy structure.
  */
 function CSPRep() {
   // this gets set to true when the policy is done parsing, or when a
   // URI-borne policy has finished loading.
@@ -157,20 +208,25 @@ CSPRep.OPTIONS_DIRECTIVE = "options";
 
 /**
   * Factory to create a new CSPRep, parsed from a string.
   *
   * @param aStr
   *        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();
   aCSPR._originalText = aStr;
 
   var selfUri = null;
   if (self instanceof Components.interfaces.nsIURI)
     selfUri = self.clone();
@@ -279,16 +335,22 @@ CSPRep.fromString = function(aStr, self)
 
     // POLICY URI //////////////////////////////////////////////////////////
     if (dirname === UD.POLICY_URI) {
       // POLICY_URI can only be alone
       if (aCSPR._directives.length > 0 || dirs.length > 1) {
         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 {
         uri = gIoService.newURI(dirvalue, null, selfUri);
       } catch(e) {
         CSPError("could not parse URI in policy URI: " + dirvalue);
         return CSPRep.fromString("allow 'none'");
       }
@@ -304,41 +366,35 @@ CSPRep.fromString = function(aStr, self)
           return CSPRep.fromString("allow 'none'");
         }
         if (selfUri.scheme !== uri.scheme){
           CSPError("can't fetch policy uri from non-matching scheme: " + uri.scheme);
           return CSPRep.fromString("allow 'none'");
         }
       }
 
-      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);
+      // 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);
+      }
+      catch (e) {
+        // resume the document request and apply most restrictive policy
+        docRequest.resume();
+        CSPError("Error fetching policy-uri: " + e);
+        return CSPRep.fromString("allow 'none'");
+      }
 
-      // 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;
-      }
-      CSPError("Error fetching policy URI: server response was " + req.status);
-      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 /////////////////////////////////////////////
     CSPWarning("Couldn't process unknown directive '" + dirname + "'");
 
   } // end directive: loop
 
   // if makeExplicit fails for any reason, default to allow 'none'.  This
--- a/content/base/src/contentSecurityPolicy.js
+++ b/content/base/src/contentSecurityPolicy.js
@@ -65,16 +65,17 @@ function ContentSecurityPolicy() {
   this._policy = CSPRep.fromString("allow *");
 
   // default options "wide open" since this policy will be intersected soon
   this._policy._allowInlineScripts = true;
   this._policy._allowEval = true;
 
   this._requestHeaders = []; 
   this._request = "";
+  this._docRequest = null;
   CSPdebug("CSP POLICY INITED TO 'allow *'");
 }
 
 /*
  * Set up mappings from nsIContentPolicy content types to CSP directives.
  */
 {
   let cp = Ci.nsIContentPolicy;
@@ -195,16 +196,17 @@ ContentSecurityPolicy.prototype = {
     var internalChannel = null;
     try {
       internalChannel = aChannel.QueryInterface(Ci.nsIHttpChannelInternal);
     } catch (e) {
       CSPdebug("No nsIHttpChannelInternal for " + aChannel.URI.asciiSpec);
     }
 
     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
     if (internalChannel) {
       var reqMaj = {};
       var reqMin = {};
       var reqVersion = internalChannel.getRequestVersion(reqMaj, reqMin);
       this._request += " HTTP/" + reqMaj.value + "." + reqMin.value;
@@ -239,17 +241,19 @@ ContentSecurityPolicy.prototype = {
     }
 
     // stay uninitialized until policy merging is done
     this._isInitialized = false;
 
     // 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);
  
     // (3) Save the result
     this._policy = intersect;
     this._isInitialized = true;
   },
--- a/content/base/test/Makefile.in
+++ b/content/base/test/Makefile.in
@@ -454,16 +454,19 @@ include $(topsrcdir)/config/rules.mk
 		test_bug614058.html \
 		test_bug590771.html \
 		test_bug622117.html \
 		test_bug622246.html \
 		test_bug484396.html \
 		test_bug466080.html \
 		bug466080.sjs \
 		test_bug625722.html \
+		test_bug558431.html \
+		file_bug558431.html \
+		file_bug558431.html^headers^ \
 		$(NULL)
 
 # This test fails on the Mac for some reason
 ifneq (,$(filter gtk2 windows,$(MOZ_WIDGET_TOOLKIT)))
 _TEST_FILES2 += 	test_copyimage.html \
 		$(NULL)
 endif
 
new file mode 100644
--- /dev/null
+++ b/content/base/test/file_bug558431.html
@@ -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>
new file mode 100644
--- /dev/null
+++ b/content/base/test/file_bug558431.html^headers^
@@ -0,0 +1,1 @@
+X-Content-Security-Policy: invalid-uri
new file mode 100644
--- /dev/null
+++ b/content/base/test/test_bug558431.html
@@ -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>