Bug 1328460 - Don't send priming to IP or non-standard ports r=ckerschb
authorKate McKinley <kmckinley@mozilla.com>
Mon, 23 Jan 2017 14:01:43 -0800
changeset 331463 5e4a1cccae923acbfc4235d48e47004ae5468e6b
parent 331462 a4117383c8869d8b1b88fc2ade41e11c8a917fc5
child 331464 761ae4db38f44a853a41644a69f192cd53ece4db
push id31272
push userphilringnalda@gmail.com
push dateSat, 28 Jan 2017 21:07:30 +0000
treeherdermozilla-central@3ef3ab119a74 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersckerschb
bugs1328460
milestone54.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 1328460 - Don't send priming to IP or non-standard ports r=ckerschb MozReview-Commit-ID: GLyLfp8gqYt
dom/security/nsMixedContentBlocker.cpp
dom/security/test/hsts/browser.ini
dom/security/test/hsts/browser_hsts-priming_no-ip-address.js
dom/security/test/hsts/browser_hsts-priming_no-non-standard-ports.js
dom/security/test/hsts/head.js
--- a/dom/security/nsMixedContentBlocker.cpp
+++ b/dom/security/nsMixedContentBlocker.cpp
@@ -25,19 +25,21 @@
 #include "nsIScriptObjectPrincipal.h"
 #include "nsISecureBrowserUI.h"
 #include "nsIDocumentLoader.h"
 #include "nsIWebNavigation.h"
 #include "nsLoadGroup.h"
 #include "nsIScriptError.h"
 #include "nsIURI.h"
 #include "nsIChannelEventSink.h"
+#include "nsNetUtil.h"
 #include "nsAsyncRedirectVerifyHelper.h"
 #include "mozilla/LoadInfo.h"
 #include "nsISiteSecurityService.h"
+#include "prnetdb.h"
 
 #include "mozilla/Logging.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/ipc/URIUtils.h"
 
 
 using namespace mozilla;
@@ -56,16 +58,43 @@ bool nsMixedContentBlocker::sBlockMixedD
 
 // Do we move HSTS before mixed-content
 bool nsMixedContentBlocker::sUseHSTS = false;
 // Do we send an HSTS priming request
 bool nsMixedContentBlocker::sSendHSTSPriming = false;
 // Default HSTS Priming failure timeout to 7 days, in seconds
 uint32_t nsMixedContentBlocker::sHSTSPrimingCacheTimeout = (60 * 24 * 7);
 
+bool
+IsEligibleForHSTSPriming(nsIURI* aContentLocation) {
+  bool isHttpScheme = false;
+  nsresult rv = aContentLocation->SchemeIs("http", &isHttpScheme);
+  NS_ENSURE_SUCCESS(rv, false);
+  if (!isHttpScheme) {
+    return false;
+  }
+
+  int32_t port = -1;
+  rv = aContentLocation->GetPort(&port);
+  NS_ENSURE_SUCCESS(rv, false);
+  int32_t defaultPort = NS_GetDefaultPort("https");
+
+  if (port != -1 && port != defaultPort) {
+    // HSTS priming requests are only sent if the port is the default port
+    return false;
+  }
+
+  nsAutoCString hostname;
+  rv = aContentLocation->GetHost(hostname);
+  NS_ENSURE_SUCCESS(rv, false);
+
+  PRNetAddr hostAddr;
+  return (PR_StringToNetAddr(hostname.get(), &hostAddr) != PR_SUCCESS);
+}
+
 // Fired at the document that attempted to load mixed content.  The UI could
 // handle this event, for example, by displaying an info bar that offers the
 // choice to reload the page with mixed content permitted.
 class nsMixedContentEvent : public Runnable
 {
 public:
   nsMixedContentEvent(nsISupports *aContext, MixedContentTypes aType, bool aRootHasSecureConnection)
     : mContext(aContext), mType(aType), mRootHasSecureConnection(aRootHasSecureConnection)
@@ -827,17 +856,17 @@ nsMixedContentBlocker::ShouldLoad(bool a
   // Allow load and return early.
   if (!securityUI) {
     *aDecision = nsIContentPolicy::ACCEPT;
     return NS_OK;
   }
   nsresult stateRV = securityUI->GetState(&state);
 
   bool doHSTSPriming = false;
-  if (isHttpScheme) {
+  if (IsEligibleForHSTSPriming(aContentLocation)) {
     bool hsts = false;
     bool cached = false;
     nsCOMPtr<nsISiteSecurityService> sss =
       do_GetService(NS_SSSERVICE_CONTRACTID, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = sss->IsSecureURI(nsISiteSecurityService::HEADER_HSTS, aContentLocation,
         0, &cached, &hsts);
     NS_ENSURE_SUCCESS(rv, rv);
--- a/dom/security/test/hsts/browser.ini
+++ b/dom/security/test/hsts/browser.ini
@@ -13,8 +13,10 @@ support-files =
 [browser_hsts-priming_hsts_after_mixed.js]
 [browser_hsts-priming_allow_display.js]
 [browser_hsts-priming_block_display.js]
 [browser_hsts-priming_block_active_css.js]
 [browser_hsts-priming_block_active_with_redir_same.js]
 [browser_hsts-priming_no-duplicates.js]
 [browser_hsts-priming_cache-timeout.js]
 [browser_hsts-priming_timeout.js]
+[browser_hsts-priming_no-non-standard-ports.js]
+[browser_hsts-priming_no-ip-address.js]
new file mode 100644
--- /dev/null
+++ b/dom/security/test/hsts/browser_hsts-priming_no-ip-address.js
@@ -0,0 +1,30 @@
+/*
+ * Description of the test:
+ *   If the top-level domain sends the STS header but does not have
+ *   includeSubdomains, HSTS priming requests should still be sent to
+ *   subdomains.
+ */
+'use strict';
+
+//jscs:disable
+add_task(function*() {
+  //jscs:enable
+  Observer.add_observers(Services);
+  registerCleanupFunction(do_cleanup);
+
+  // add the top-level server
+  test_servers['localhost-ip'] = {
+    host: '127.0.0.1',
+    response: true,
+    id: 'localhost-ip',
+  };
+  test_settings.block_active.result['localhost-ip'] = 'blocked';
+
+  let which = "block_active";
+
+  SetupPrefTestEnvironment(which);
+
+  yield execute_test("localhost-ip", test_settings[which].mimetype);
+
+  SpecialPowers.popPrefEnv();
+});
new file mode 100644
--- /dev/null
+++ b/dom/security/test/hsts/browser_hsts-priming_no-non-standard-ports.js
@@ -0,0 +1,34 @@
+/*
+ * Description of the test:
+ *   If the top-level domain sends the STS header but does not have
+ *   includeSubdomains, HSTS priming requests should still be sent to
+ *   subdomains.
+ */
+'use strict';
+
+//jscs:disable
+add_task(function*() {
+  //jscs:enable
+  Observer.add_observers(Services);
+  registerCleanupFunction(do_cleanup);
+
+  // add the top-level server
+  test_servers['non-standard-port'] = {
+    host: 'example.com:1234',
+    response: true,
+    id: 'non-standard-port',
+  };
+  test_settings.block_active.result['non-standard-port'] = 'blocked';
+
+  let which = "block_active";
+
+  SetupPrefTestEnvironment(which);
+
+  yield execute_test("non-standard-port", test_settings[which].mimetype);
+
+  yield execute_test("prime-hsts", test_settings[which].mimetype);
+
+  ok("prime-hsts" in test_settings[which_test].priming, "Sent priming request on standard port after non-standard was not primed");
+
+  SpecialPowers.popPrefEnv();
+});
--- a/dom/security/test/hsts/head.js
+++ b/dom/security/test/hsts/head.js
@@ -11,16 +11,18 @@
  *   For each server, test that it response appropriately when the we allow
  *   or block active or display content, as well as when we send an hsts priming
  *   request, but do not change the order of mixed-content and HSTS.
  *
  *   Test use http-on-examine-response, so must be run in browser context.
  */
 'use strict';
 
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
 var TOP_URI = "https://example.com/browser/dom/security/test/hsts/file_priming-top.html";
 
 var test_servers = {
   // a test server that does not support TLS
   'no-ssl': {
     host: 'example.co.jp',
     response: false,
     id: 'no-ssl',
@@ -170,52 +172,109 @@ var StreamListener = function(subject) {
   this.uri = channel.URI.asciiSpec;
   this.listener = traceable.setNewListener(this);
   return this;
 };
 
 // Next three methods are part of `nsIStreamListener` interface and are
 // invoked by `nsIInputStreamPump.asyncRead`.
 StreamListener.prototype.onDataAvailable = function(request, context, input, offset, count) {
+  if (request.status == Cr.NS_ERROR_ABORT) {
+    this.listener = null;
+    return Cr.NS_SUCCESS;
+  }
   let listener = this.listener;
-  listener.onDataAvailable(request, context, input, offset, count);
+  if (listener) {
+    try {
+      let rv = listener.onDataAvailable(request, context, input, offset, count);
+      if (rv != Cr.NS_ERROR_ABORT) {
+        // If the channel gets canceled, we sometimes get NS_ERROR_ABORT here.
+        // Anything else is an error.
+        return rv;
+      }
+    } catch (e) {
+      if (e != Cr.NS_ERROR_ABORT) {
+        return e;
+      }
+    }
+  }
+  return Cr.NS_SUCCESS;
 };
 
 // Next two methods implement `nsIRequestObserver` interface and are invoked
 // by `nsIInputStreamPump.asyncRead`.
 StreamListener.prototype.onStartRequest = function(request, context) {
+  if (request.status == Cr.NS_ERROR_ABORT) {
+    this.listener = null;
+    return Cr.NS_SUCCESS;
+  }
   let listener = this.listener;
-  listener.onStartRequest(request, context);
+  if (listener) {
+    try {
+      let rv = listener.onStartRequest(request, context);
+      if (rv != Cr.NS_ERROR_ABORT) {
+        // If the channel gets canceled, we sometimes get NS_ERROR_ABORT here.
+        // Anything else is an error.
+        return rv;
+      }
+    } catch (e) {
+      if (e != Cr.NS_ERROR_ABORT) {
+        return e;
+      }
+    }
+  }
+  return Cr.NS_SUCCESS;
 };
 
 // Called to signify the end of an asynchronous request. We only care to
 // discover errors.
 StreamListener.prototype.onStopRequest = function(request, context, status) {
+  if (status == Cr.NS_ERROR_ABORT) {
+    this.listener = null;
+    return Cr.NS_SUCCESS;
+  }
   let listener = this.listener;
-  listener.onStopRequest(request, context, status);
+  if (listener) {
+    try {
+      let rv = listener.onStopRequest(request, context, status);
+      if (rv != Cr.NS_ERROR_ABORT) {
+        // If the channel gets canceled, we sometimes get NS_ERROR_ABORT here.
+        // Anything else is an error.
+        return rv;
+      }
+    } catch (e) {
+      if (e != Cr.NS_ERROR_ABORT) {
+        return e;
+      }
+    }
+  }
+  return Cr.NS_SUCCESS;
 };
 
 var Observer = {
   listeners: {},
   observe: function (subject, topic, data) {
     switch (topic) {
       case 'console-api-log-event':
         return Observer.console_api_log_event(subject, topic, data);
       case 'http-on-examine-response':
         return Observer.http_on_examine_response(subject, topic, data);
       case 'http-on-modify-request':
         return Observer.http_on_modify_request(subject, topic, data);
     }
     throw "Can't handle topic "+topic;
   },
-  add_observers: function (services) {
+  add_observers: function (services, include_on_modify = false) {
     services.obs.addObserver(Observer, "console-api-log-event", false);
     services.obs.addObserver(Observer, "http-on-examine-response", false);
     services.obs.addObserver(Observer, "http-on-modify-request", false);
   },
+  cleanup: function () {
+    this.listeners = {};
+  },
   // When a load is blocked which results in an error event within a page, the
   // test logs to the console.
   console_api_log_event: function (subject, topic, data) {
     var message = subject.wrappedJSObject.arguments[0];
     // when we are blocked, this will match the message we sent to the console,
     // ignore everything else.
     var re = RegExp(/^HSTS_PRIMING: Blocked ([-\w]+).*$/);
     if (!re.test(message)) {
@@ -318,16 +377,18 @@ function clear_sts_data() {
   }
 }
 
 function do_cleanup() {
   clear_sts_data();
 
   Services.obs.removeObserver(Observer, "console-api-log-event");
   Services.obs.removeObserver(Observer, "http-on-examine-response");
+
+  Observer.cleanup();
 }
 
 function SetupPrefTestEnvironment(which, additional_prefs) {
   which_test = which;
   clear_sts_data();
 
   var settings = test_settings[which];
   // priming counts how many priming requests we saw