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 idunknown
push userunknown
push dateunknown
reviewersjst, blocker
bugs558431
milestone2.0b11pre
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>