Bug 1416045. r=mayhemer, a=RyanVM
authorvinoth <cegvinoth@gmail.com>
Thu, 22 Mar 2018 21:02:16 +0200
changeset 462851 1d6d0bc1cb27eb3e90fa33367869297b451ddb9c
parent 462850 49448fdb2c96b17a290395110f3492870ccfebd5
child 462852 2285a70156004c86a568fcc7a9fdc51bb0063fd0
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmayhemer, RyanVM
bugs1416045
milestone60.0
Bug 1416045. r=mayhemer, a=RyanVM Reviewers: mayhemer Reviewed By: mayhemer Subscribers: freddyb, dveditz, mayhemer, ckerschb, vinoth Tags: PHID-PROJ-wkydohdk6pajyfn2llkb Bug #: 1416045 Differential Revision: https://phabricator.services.mozilla.com/D675
dom/security/test/csp/file_multipart_testserver.sjs
dom/security/test/csp/test_multipartchannel.html
netwerk/streamconv/converters/nsMultiMixedConv.cpp
netwerk/streamconv/converters/nsMultiMixedConv.h
--- a/dom/security/test/csp/file_multipart_testserver.sjs
+++ b/dom/security/test/csp/file_multipart_testserver.sjs
@@ -1,50 +1,150 @@
 // SJS file specifically for the needs of bug
-// Bug 1223743 - CSP: Check baseChannel for CSP when loading multipart channel
+// Bug 1416045/Bug 1223743 - CSP: Check baseChannel for CSP when loading multipart channel
 
 var CSP = "script-src 'unsafe-inline', img-src 'none'";
-var BOUNDARY = "fooboundary"
+var rootCSP = "script-src 'unsafe-inline'";
+var part1CSP = "img-src *";
+var part2CSP = "img-src 'none'";
+var BOUNDARY = "fooboundary";
 
 // small red image
 const IMG_BYTES = atob(
   "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
   "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==");
 
 var RESPONSE = `
   <script>
   var myImg = new Image;
   myImg.src = "file_multipart_testserver.sjs?img";
   myImg.onerror = function(e) {
-    window.parent.postMessage("img-blocked", "*");
+    window.parent.postMessage({"test": "rootCSP_test",
+                               "msg": "img-blocked"}, "*");
   };
   myImg.onload = function() {
-    window.parent.postMessage("img-loaded", "*");
+    window.parent.postMessage({"test": "rootCSP_test",
+                               "msg": "img-loaded"}, "*");
   };
   document.body.appendChild(myImg);
   </script>
 `;
 
-var myTimer;
+var RESPONSE1 = `
+  <body>
+  <script>
+  var triggerNextPartFrame = document.createElement('iframe');
+  var myImg = new Image;
+  myImg.src = "file_multipart_testserver.sjs?img";
+  myImg.onerror = function(e) {
+    window.parent.postMessage({"test": "part1CSP_test",
+                               "msg": "part1-img-blocked"}, "*");
+    triggerNextPartFrame.src = 'file_multipart_testserver.sjs?sendnextpart';
+  };
+  myImg.onload = function() {
+    window.parent.postMessage({"test": "part1CSP_test",
+                               "msg": "part1-img-loaded"}, "*");
+    triggerNextPartFrame.src = 'file_multipart_testserver.sjs?sendnextpart';
+  };
+  document.body.appendChild(myImg);
+  document.body.appendChild(triggerNextPartFrame);
+  </script>
+  </body>
+`;
+
+var RESPONSE2 = `
+  <body>
+  <script>
+  var myImg = new Image;
+  myImg.src = "file_multipart_testserver.sjs?img";
+  myImg.onerror = function(e) {
+    window.parent.postMessage({"test": "part2CSP_test",
+                               "msg": "part2-img-blocked"}, "*");
+  };
+  myImg.onload = function() {
+    window.parent.postMessage({"test": "part2CSP_test",
+                               "msg": "part2-img-loaded"}, "*");
+  };
+  document.body.appendChild(myImg);
+  </script>
+  </body>
+`;
+
+function setGlobalState(data, key)
+{
+  x = { data: data, QueryInterface: function(iid) { return this } };
+  x.wrappedJSObject = x;
+  setObjectState(key, x);
+}
+
+function getGlobalState(key)
+{
+  var data;
+  getObjectState(key, function(x) {
+    data = x && x.wrappedJSObject.data;
+  });
+  return data;
+}
 
 function handleRequest(request, response)
 {
   // avoid confusing cache behaviors
   response.setHeader("Cache-Control", "no-cache", false);
 
   if (request.queryString == "doc") {
     response.setHeader("Content-Security-Policy", CSP, false);
     response.setHeader("Content-Type", "multipart/x-mixed-replace; boundary=" + BOUNDARY, false);
     response.write(BOUNDARY + "\r\n");
     response.write(RESPONSE);
     response.write(BOUNDARY + "\r\n");
     return;
   }
 
+  if (request.queryString == "partcspdoc") {
+    response.setHeader("Content-Security-Policy", rootCSP, false);
+    response.setHeader("Content-Type",
+                       "multipart/x-mixed-replace; boundary=" + BOUNDARY, false);
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    response.processAsync();
+    response.write("--"+BOUNDARY+"\r\n");
+    sendNextPart(response, 1);
+    return;
+  }
+
+  if (request.queryString == "sendnextpart") {
+    response.setStatusLine(request.httpVersion, 204, "No content");
+    var blockedResponse = getGlobalState("root-document-response");
+    if (typeof blockedResponse == "object") {
+      sendNextPart(blockedResponse, 2);
+      sendClose(blockedResponse);
+    } else {
+      dump("Couldn't find the stored response object.");
+    }
+    return;
+  }
+
   if (request.queryString == "img") {
     response.setHeader("Content-Type", "image/png");
     response.write(IMG_BYTES);
     return;
   }
 
   // we should never get here - return something unexpected
   response.write("d'oh");
 }
+
+function sendClose(response) {
+  response.write("--"+BOUNDARY+"--\r\n");
+  response.finish();
+}
+
+function sendNextPart(response, partNumber) {
+  response.write("Content-type: text/html" + "\r\n");
+  if (partNumber == 1) {
+    response.write("Content-Security-Policy:" + part1CSP + "\r\n");
+    response.write(RESPONSE1);
+    setGlobalState(response, "root-document-response");
+  } else {
+    response.write("Content-Security-Policy:" + part2CSP + "\r\n");
+    response.write(RESPONSE2);
+  }
+  response.write("--"+BOUNDARY+"\r\n");
+}
--- a/dom/security/test/csp/test_multipartchannel.html
+++ b/dom/security/test/csp/test_multipartchannel.html
@@ -1,34 +1,68 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
-  <title>Bug 1223743 - CSP: Check baseChannel for CSP when loading multipart channel</title>
+  <title>Bug 1416045/Bug 1223743 - CSP: Check baseChannel for CSP when loading multipart channel</title>
   <!-- Including SimpleTest.js so we can use waitForExplicitFinish !-->
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <iframe style="width:100%;" id="testframe"></iframe>
+<iframe style="width:100%;" id="testPartCSPframe"></iframe>
 
 <script class="testbody" type="text/javascript">
 
+var testsToRunMultipartCSP = {
+  rootCSP_test: false,
+  part1CSP_test: false,
+  part2CSP_test: false,
+};
+
 SimpleTest.waitForExplicitFinish();
 
+function checkTestsCompleted() {
+  for (var prop in testsToRunMultipartCSP) {
+    // some test hasn't run yet so we're not done
+    if (!testsToRunMultipartCSP[prop]) {
+      return;
+    }
+  }
+  window.removeEventListener("message", receiveMessage);
+  SimpleTest.finish();
+}
 /* Description of the test:
  * We apply a CSP to a multipart channel and then try to load an image
  * within a segment making sure the image is blocked correctly by CSP.
+ * We also provide CSP for each part and try to load an image in each
+ * part and make sure the image is loaded in first part and blocked in
+ * second part correctly based on its CSP accordingly.
  */
 
 window.addEventListener("message", receiveMessage);
 function receiveMessage(event) {
-  is(event.data, "img-blocked", "image should be blocked");
-  window.removeEventListener("message", receiveMessage);
-  SimpleTest.finish();
+  switch (event.data.test) {
+    case "rootCSP_test":
+      is(event.data.msg, "img-blocked", "image should be blocked");
+      testsToRunMultipartCSP["rootCSP_test"] = true;
+      break;
+    case "part1CSP_test":
+      is(event.data.msg, "part1-img-loaded", "Part1 image should be loaded");
+      testsToRunMultipartCSP["part1CSP_test"] = true;
+      break;
+    case "part2CSP_test":
+      is(event.data.msg, "part2-img-blocked", "Part2 image should be blocked");
+      testsToRunMultipartCSP["part2CSP_test"] = true;
+      break;
+  }
+  checkTestsCompleted();
 }
 
 // start the test
 document.getElementById("testframe").src = "file_multipart_testserver.sjs?doc";
+document.getElementById("testPartCSPframe").src =
+                                    "file_multipart_testserver.sjs?partcspdoc";
 
 </script>
 </body>
 </html>
--- a/netwerk/streamconv/converters/nsMultiMixedConv.cpp
+++ b/netwerk/streamconv/converters/nsMultiMixedConv.cpp
@@ -483,16 +483,22 @@ nsMultiMixedConv::OnStartRequest(nsIRequ
 
     // ask the HTTP channel for the content-type and extract the boundary from it.
     nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv);
     if (NS_SUCCEEDED(rv)) {
         rv = httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("content-type"), contentType);
         if (NS_FAILED(rv)) {
             return rv;
         }
+        nsCString csp;
+        rv = httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("content-security-policy"),
+                                            csp);
+        if (NS_SUCCEEDED(rv)) {
+          mRootContentSecurityPolicy = csp;
+        }
     } else {
         // try asking the channel directly
         rv = mChannel->GetContentType(contentType);
         if (NS_FAILED(rv)) {
             return NS_ERROR_FAILURE;
         }
     }
 
@@ -523,16 +529,20 @@ nsMultiMixedConv::OnStartRequest(nsIRequ
     mHeaderTokens[HEADER_CONTENT_DISPOSITION] =
       mTokenizer.AddCustomToken("content-disposition", mTokenizer.CASE_INSENSITIVE, false);
     mHeaderTokens[HEADER_SET_COOKIE] =
       mTokenizer.AddCustomToken("set-cookie", mTokenizer.CASE_INSENSITIVE, false);
     mHeaderTokens[HEADER_CONTENT_RANGE] =
       mTokenizer.AddCustomToken("content-range", mTokenizer.CASE_INSENSITIVE, false);
     mHeaderTokens[HEADER_RANGE] =
       mTokenizer.AddCustomToken("range", mTokenizer.CASE_INSENSITIVE, false);
+    mHeaderTokens[HEADER_CONTENT_SECURITY_POLICY] =
+      mTokenizer.AddCustomToken("content-security-policy",
+                                mTokenizer.CASE_INSENSITIVE,
+                                false);
 
     mLFToken = mTokenizer.AddCustomToken("\n", mTokenizer.CASE_SENSITIVE, false);
     mCRLFToken = mTokenizer.AddCustomToken("\r\n", mTokenizer.CASE_SENSITIVE, false);
 
     SwitchToControlParsing();
 
     mBoundaryToken =
       mTokenizer.AddCustomToken(mBoundary, mTokenizer.CASE_SENSITIVE);
@@ -996,16 +1006,17 @@ nsMultiMixedConv::SendData()
 }
 
 void
 nsMultiMixedConv::HeadersToDefault()
 {
     mContentLength = UINT64_MAX;
     mContentType.Truncate();
     mContentDisposition.Truncate();
+    mContentSecurityPolicy.Truncate();
     mIsByteRangeRequest = false;
 }
 
 nsresult
 nsMultiMixedConv::ProcessHeader()
 {
     mozilla::Tokenizer p(mResponseHeaderValue);
 
@@ -1048,16 +1059,41 @@ nsMultiMixedConv::ProcessHeader()
         return NS_ERROR_CORRUPTED_CONTENT;
       }
       mIsByteRangeRequest = true;
       if (mContentLength == UINT64_MAX) {
         mContentLength = uint64_t(mByteRangeEnd - mByteRangeStart + 1);
       }
       break;
     }
+    case HEADER_CONTENT_SECURITY_POLICY: {
+      mContentSecurityPolicy = mResponseHeaderValue;
+      mContentSecurityPolicy.CompressWhitespace();
+      nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
+      if (httpChannel) {
+        nsCString resultCSP = mRootContentSecurityPolicy;
+        if (!mContentSecurityPolicy.IsEmpty()) {
+          // We are updating the root channel CSP header respectively for
+          // each part as: CSP-root + CSP-partN, where N is the part number.
+          // Here we append current part's CSP to root CSP and reset CSP
+          // header for each part.
+          if (!resultCSP.IsEmpty()) {
+            resultCSP.Append(";");
+          }
+          resultCSP.Append(mContentSecurityPolicy);
+        }
+        nsresult rv = httpChannel->SetResponseHeader(
+                        NS_LITERAL_CSTRING("Content-Security-Policy"),
+                        resultCSP, false);
+        if (NS_FAILED(rv)) {
+          return NS_ERROR_CORRUPTED_CONTENT;
+        }
+      }
+      break;
+    }
     case HEADER_UNKNOWN:
       // We ignore anything else...
       break;
     }
 
     return NS_OK;
 }
 
--- a/netwerk/streamconv/converters/nsMultiMixedConv.h
+++ b/netwerk/streamconv/converters/nsMultiMixedConv.h
@@ -146,16 +146,18 @@ protected:
     nsCOMPtr<nsIStreamListener> mFinalListener; // this guy gets the converted data via his OnDataAvailable()
 
     nsCOMPtr<nsIChannel> mChannel; // The channel as we get in in OnStartRequest call
     RefPtr<nsPartChannel> mPartChannel;   // the channel for the given part we're processing.
                                         // one channel per part.
     nsCOMPtr<nsISupports> mContext;
     nsCString           mContentType;
     nsCString           mContentDisposition;
+    nsCString           mContentSecurityPolicy;
+    nsCString           mRootContentSecurityPolicy;
     uint64_t            mContentLength;
     uint64_t            mTotalSent;
 
     // The following members are for tracking the byte ranges in
     // multipart/mixed content which specified the 'Content-Range:'
     // header...
     int64_t             mByteRangeStart;
     int64_t             mByteRangeEnd;
@@ -193,16 +195,17 @@ protected:
     enum EHeader : uint32_t {
       HEADER_FIRST,
       HEADER_CONTENT_TYPE = HEADER_FIRST,
       HEADER_CONTENT_LENGTH,
       HEADER_CONTENT_DISPOSITION,
       HEADER_SET_COOKIE,
       HEADER_CONTENT_RANGE,
       HEADER_RANGE,
+      HEADER_CONTENT_SECURITY_POLICY,
       HEADER_UNKNOWN
     } mResponseHeader;
     // Cumulated value of a response header.
     nsCString mResponseHeaderValue;
 
     nsCString mBoundary;
     mozilla::IncrementalTokenizer mTokenizer;