Bug 1313595 - Lower HSTS priming timeout draft
authorKate McKinley <kmckinley@mozilla.com>
Thu, 08 Dec 2016 11:07:55 -1000
changeset 459340 b4eb50929007b6c0f05cebff9eb7206f2d156f1b
parent 459339 7ba48de0e02171fcec169935e06329b3410e23a8
child 541858 a0403ab4e42108947293b1112b0f257012084e2f
push id41183
push userbmo:kmckinley@mozilla.com
push dateWed, 11 Jan 2017 19:26:08 +0000
bugs1313595
milestone53.0a1
Bug 1313595 - Lower HSTS priming timeout MozReview-Commit-ID: 5wOqtYM1MfD
dom/security/test/hsts/browser.ini
dom/security/test/hsts/browser_hsts-priming_no-duplicates.js
dom/security/test/hsts/browser_hsts-priming_timeout.js
dom/security/test/hsts/file_priming-top.html
dom/security/test/hsts/file_stylesheet.css
dom/security/test/hsts/file_testserver.sjs
dom/security/test/hsts/head.js
modules/libpref/init/all.js
netwerk/protocol/http/HSTSPrimerListener.cpp
netwerk/protocol/http/HSTSPrimerListener.h
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsIHstsPrimingCallback.idl
toolkit/components/telemetry/Histograms.json
xpcom/base/ErrorList.h
--- a/dom/security/test/hsts/browser.ini
+++ b/dom/security/test/hsts/browser.ini
@@ -12,8 +12,9 @@ support-files =
 [browser_hsts-priming_block_active.js]
 [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]
--- a/dom/security/test/hsts/browser_hsts-priming_no-duplicates.js
+++ b/dom/security/test/hsts/browser_hsts-priming_no-duplicates.js
@@ -14,17 +14,15 @@ add_task(function*() {
   let which = "block_display";
 
   SetupPrefTestEnvironment(which);
 
   for (let server of Object.keys(test_servers)) {
     yield execute_test(server, test_settings[which].mimetype);
   }
 
-  test_settings[which].priming = {};
-
   // run the tests twice to validate the cache is being used
   for (let server of Object.keys(test_servers)) {
     yield execute_test(server, test_settings[which].mimetype);
   }
 
   SpecialPowers.popPrefEnv();
 });
new file mode 100644
--- /dev/null
+++ b/dom/security/test/hsts/browser_hsts-priming_timeout.js
@@ -0,0 +1,24 @@
+/*
+ * Description of the test:
+ *   Only one request should be sent per host, even if we run the test more
+ *   than once.
+ */
+'use strict';
+
+//jscs:disable
+add_task(function*() {
+  //jscs:enable
+  Observer.add_observers(Services);
+  registerCleanupFunction(do_cleanup);
+
+  let which = "timeout";
+
+  SetupPrefTestEnvironment(which, [["security.mixed_content.hsts_priming_request_timeout",
+                1000]]);
+
+  for (let server of Object.keys(test_servers)) {
+    yield execute_test(server, test_settings[which].mimetype);
+  }
+
+  SpecialPowers.popPrefEnv();
+});
--- a/dom/security/test/hsts/file_priming-top.html
+++ b/dom/security/test/hsts/file_priming-top.html
@@ -32,24 +32,28 @@ var args = parse_query_string();
 var subresources = {
   css: { mimetype: 'text/css', file: 'file_stylesheet.css' },
   img: { mimetype: 'image/png', file: 'file_1x1.png' },
   script: { mimetype: 'text/javascript', file: 'file_priming.js' },
 };
 
 function handler(ev) {
   console.log("HSTS_PRIMING: Blocked "+args.id);
+  let elem = document.getElementById(args.id);
+  elem.parentElement.removeChild(elem);
 }
 
 function loadCss(src) {
   let head = document.getElementsByTagName("head")[0];
   let link = document.createElement("link");
   link.setAttribute("rel", "stylesheet");
+  link.setAttribute("id", args.id);
   link.setAttribute("type", subresources[args.type].mimetype);
   link.setAttribute("href", src);
+  link.onerror = handler;
   head.appendChild(link);
 }
 
 function loadResource(src) {
   let content = document.getElementById("content");
   let testElem = document.createElement(args.type);
   testElem.setAttribute("id", args.id);
   testElem.setAttribute("charset", "UTF-8");
@@ -62,16 +66,17 @@ function loadTest() {
   let subresource = subresources[args.type];
 
   let src = "http://"
     + args.host
     + "/browser/dom/security/test/hsts/file_testserver.sjs"
     + "?file=" +escape("browser/dom/security/test/hsts/" + subresource.file)
     + "&primer=" + escape(args.id)
     + "&mimetype=" + escape(subresource.mimetype)
+    + "&timeout=" + escape(args.timeout)
     ;
   if (args.type == 'css') {
     loadCss(src);
     return;
   }
 
   loadResource(src);
 }
--- a/dom/security/test/hsts/file_stylesheet.css
+++ b/dom/security/test/hsts/file_stylesheet.css
@@ -0,0 +1,1 @@
+body {}
--- a/dom/security/test/hsts/file_testserver.sjs
+++ b/dom/security/test/hsts/file_testserver.sjs
@@ -22,45 +22,63 @@ function loadFromFile(path) {
   var test = NetUtil.readInputStreamToString(testFileStream, testFileStream.available());
   return test;
 }
 
 function handleRequest(request, response)
 {
   const query = new URLSearchParams(request.queryString);
 
-  redir = query.get('redir');
-  if (redir == 'same') {
-    query.delete("redir");
-    response.setStatus(302);
-    let newURI = request.uri;
-    newURI.queryString = query.serialize();
-    response.setHeader("Location", newURI.spec)
-  }
+  var timeout = parseInt(query.get('timeout'));
+  response.processAsync();
+
+  timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+  timer.initWithCallback(function()
+    {
+      if (!response) {
+        return;
+      }
 
-  // avoid confusing cache behaviors
-  response.setHeader("Cache-Control", "no-cache", false);
+      // avoid confusing cache behaviors
+      response.setHeader("Cache-Control", "no-cache", false);
+
+      redir = query.get('redir');
+      if (redir == 'same') {
+        query.delete("redir");
+        response.setStatus(302);
+        let newURI = request.uri;
+        newURI.queryString = query.serialize();
+        response.setHeader("Location", newURI.spec)
+        response.write('xyzzy');
+        response.finish();
+        return;
+      }
 
-  // if we have a priming header, check for required behavior
-  // and set header appropriately
-  if (request.hasHeader('Upgrade-Insecure-Requests')) {
-    var expected = query.get('primer');
-    if (expected == 'prime-hsts') {
-      // set it for 5 minutes
-      response.setHeader("Strict-Transport-Security", "max-age="+(60*5), false);
-    } else if (expected == 'reject-upgrade') {
-      response.setHeader("Strict-Transport-Security", "max-age=0", false);
-    }
-    response.write('');
-    return;
-  }
+      // if we have a priming header, check for required behavior
+      // and set header appropriately
+      if (request.hasHeader('Upgrade-Insecure-Requests')) {
+        var expected = query.get('primer');
+        if (expected == 'prime-hsts') {
+          // set it for 5 minutes
+          response.setHeader("Strict-Transport-Security", "max-age="+(60*5), false);
+        } else if (expected == 'reject-upgrade') {
+          response.setHeader("Strict-Transport-Security", "max-age=0", false);
+        }
+        response.write('xyzzy');
+        response.finish();
+        return;
+      }
 
-  var file = query.get('file');
-  if (file) {
-    var mimetype = unescape(query.get('mimetype'));
-    response.setHeader("Content-Type", mimetype, false);
-    response.write(loadFromFile(unescape(file)));
-    return;
-  }
+      var file = query.get('file');
+      if (file) {
+        var mimetype = unescape(query.get('mimetype'));
+        response.setHeader("Content-Type", mimetype, false);
+        let contents = loadFromFile(unescape(file));
+        response.write(contents);
+        response.finish();
+        return;
+      }
 
-  response.setHeader("Content-Type", "application/json", false);
-  response.write('{}');
+      response.setHeader("Content-Type", "application/json", false);
+      response.write('{}');
+      response.finish();
+    }, timeout, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
 }
--- a/dom/security/test/hsts/head.js
+++ b/dom/security/test/hsts/head.js
@@ -42,108 +42,164 @@ var test_servers = {
 var test_settings = {
   // mixed active content is allowed, priming will upgrade
   allow_active: {
     block_active: false,
     block_display: false,
     use_hsts: true,
     send_hsts_priming: true,
     type: 'script',
+    timeout: 0,
     result: {
       'no-ssl': 'insecure',
       'reject-upgrade': 'insecure',
       'prime-hsts': 'secure',
     },
   },
   // mixed active content is blocked, priming will upgrade
   block_active: {
     block_active: true,
     block_display: false,
     use_hsts: true,
     send_hsts_priming: true,
     type: 'script',
+    timeout: 0,
     result: {
       'no-ssl': 'blocked',
       'reject-upgrade': 'blocked',
       'prime-hsts': 'secure',
     },
   },
   // keep the original order of mixed-content and HSTS, but send
   // priming requests
   hsts_after_mixed: {
     block_active: true,
     block_display: false,
     use_hsts: false,
     send_hsts_priming: true,
     type: 'script',
+    timeout: 0,
     result: {
       'no-ssl': 'blocked',
       'reject-upgrade': 'blocked',
       'prime-hsts': 'blocked',
     },
   },
   // mixed display content is allowed, priming will upgrade
   allow_display: {
     block_active: true,
     block_display: false,
     use_hsts: true,
     send_hsts_priming: true,
     type: 'img',
+    timeout: 0,
     result: {
       'no-ssl': 'insecure',
       'reject-upgrade': 'insecure',
       'prime-hsts': 'secure',
     },
   },
   // mixed display content is blocked, priming will upgrade
   block_display: {
     block_active: true,
     block_display: true,
     use_hsts: true,
     send_hsts_priming: true,
     type: 'img',
+    timeout: 0,
     result: {
       'no-ssl': 'blocked',
       'reject-upgrade': 'blocked',
       'prime-hsts': 'secure',
     },
   },
   // mixed active content is blocked, priming will upgrade (css)
   block_active_css: {
     block_active: true,
-    block_display: false,
+    block_display: true,
     use_hsts: true,
     send_hsts_priming: true,
     type: 'css',
+    timeout: 0,
     result: {
       'no-ssl': 'blocked',
       'reject-upgrade': 'blocked',
       'prime-hsts': 'secure',
     },
   },
   // mixed active content is blocked, priming will upgrade
   // redirect to the same host
   block_active_with_redir_same: {
     block_active: true,
     block_display: false,
     use_hsts: true,
     send_hsts_priming: true,
     type: 'script',
     redir: 'same',
+    timeout: 0,
     result: {
       'no-ssl': 'blocked',
       'reject-upgrade': 'blocked',
       'prime-hsts': 'secure',
     },
   },
+  // mixed active content is blocked, priming will upgrade
+  // redirect to the same host
+  timeout: {
+    block_active: true,
+    block_display: true,
+    use_hsts: true,
+    send_hsts_priming: true,
+    type: 'script',
+    timeout: 100000,
+    result: {
+      'no-ssl': 'blocked',
+      'reject-upgrade': 'blocked',
+      'prime-hsts': 'blocked',
+    },
+  },
 }
 // track which test we are on
 var which_test = "";
 
-const Observer = {
+/**
+ * A stream listener that just forwards all calls
+ */
+var StreamListener = function(subject) {
+  let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+  let traceable = subject.QueryInterface(Ci.nsITraceableChannel);
+
+  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) {
+  let listener = this.listener;
+  listener.onDataAvailable(request, context, input, offset, count);
+};
+
+// Next two methods implement `nsIRequestObserver` interface and are invoked
+// by `nsIInputStreamPump.asyncRead`.
+StreamListener.prototype.onStartRequest = function(request, context) {
+  let listener = this.listener;
+  listener.onStartRequest(request, context);
+};
+
+// Called to signify the end of an asynchronous request. We only care to
+// discover errors.
+StreamListener.prototype.onStopRequest = function(request, context, status) {
+  let listener = this.listener;
+  listener.onStopRequest(request, context, status);
+};
+
+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);
@@ -186,34 +242,45 @@ const Observer = {
       if (re.test(uri)) {
         return test_servers[item];
       }
     }
     return null;
   },
   http_on_modify_request: function (subject, topic, data) {
     let channel = subject.QueryInterface(Ci.nsIHttpChannel);
-    if (channel.requestMethod != 'HEAD') {
-      return;
-    }
+    let uri = channel.URI.asciiSpec;
 
     let curTest = this.get_current_test(channel.URI.asciiSpec);
 
     if (!curTest) {
       return;
     }
+    
+    if (!(uri in this.listeners)) {
+      // Add an nsIStreamListener to ensure that the listener is not NULL
+      this.listeners[uri] = new StreamListener(subject);
+    }
 
+    if (channel.requestMethod != 'HEAD') {
+      return;
+    }
+    if (typeof ok === 'undefined') {
+      // we are in the wrong thread and ok and is not available
+      return;
+    }
     ok(!(curTest.id in test_settings[which_test].priming), "Already saw a priming request for " + curTest.id);
     test_settings[which_test].priming[curTest.id] = true;
   },
   // When we see a response come back, peek at the response and test it is secure
   // or insecure as needed. Addtionally, watch the response for priming requests.
   http_on_examine_response: function (subject, topic, data) {
     let channel = subject.QueryInterface(Ci.nsIHttpChannel);
     let curTest = this.get_current_test(channel.URI.asciiSpec);
+    let uri = channel.URI.asciiSpec;
 
     if (!curTest) {
       return;
     }
 
     let result = (channel.URI.asciiSpec.startsWith('https:')) ? "secure" : "insecure";
 
     // This is a priming request, go ahead and validate we were supposed to see
@@ -222,16 +289,19 @@ const Observer = {
       is(true, curTest.response, "HSTS priming response found " + curTest.id);
       return;
     }
 
     // This is the response to our query, make sure it matches
     is(result, test_settings[which_test].result[curTest.id],
         "HSTS priming result " + which_test + ":" + curTest.id);
     test_settings[which_test].finished[curTest.id] = result;
+    if (this.listeners[uri]) {
+      this.listeners[uri] = undefined;
+    }
   },
 };
 
 // opens `uri' in a new tab and focuses it.
 // returns the newly opened tab
 function openTab(uri) {
   let tab = gBrowser.addTab(uri);
 
@@ -267,41 +337,43 @@ function SetupPrefTestEnvironment(which,
 
   var prefs = [["security.mixed_content.block_active_content",
                 settings.block_active],
                ["security.mixed_content.block_display_content",
                 settings.block_display],
                ["security.mixed_content.use_hsts",
                 settings.use_hsts],
                ["security.mixed_content.send_hsts_priming",
-                settings.send_hsts_priming]];
+                settings.send_hsts_priming],
+  ];
 
   if (additional_prefs) {
     for (let idx in additional_prefs) {
       prefs.push(additional_prefs[idx]);
     }
   }
 
-  console.log("prefs=%s", prefs);
-
   SpecialPowers.pushPrefEnv({'set': prefs});
 }
 
 // make the top-level test uri
-function build_test_uri(base_uri, host, test_id, type) {
+function build_test_uri(base_uri, host, test_id, type, timeout) {
   return base_uri +
           "?host=" + escape(host) +
           "&id=" + escape(test_id) +
-          "&type=" + escape(type);
+          "&type=" + escape(type) +
+          "&timeout=" + escape(timeout)
+    ;
 }
 
 // open a new tab, load the test, and wait for it to finish
 function execute_test(test, mimetype) {
   var src = build_test_uri(TOP_URI, test_servers[test].host,
-      test, test_settings[which_test].type);
+      test, test_settings[which_test].type,
+      test_settings[which_test].timeout);
 
   let tab = openTab(src);
   test_servers[test]['tab'] = tab;
 
   let browser = gBrowser.getBrowserForTab(tab);
   yield BrowserTestUtils.browserLoaded(browser);
 
   yield BrowserTestUtils.removeTab(tab);
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5541,18 +5541,21 @@ pref("security.mixed_content.send_hsts_p
 // Don't change the order of evaluation of mixed-content and HSTS upgrades in
 // order to be most compatible with current standards
 pref("security.mixed_content.use_hsts", false);
 #else
 // Change the order of evaluation so HSTS upgrades happen before
 // mixed-content blocking
 pref("security.mixed_content.use_hsts", true);
 #endif
-// Approximately 1 week default cache for HSTS priming failures
+// Approximately 1 week default cache for HSTS priming failures, in seconds
 pref ("security.mixed_content.hsts_priming_cache_timeout", 10080);
+// Force the channel to timeout in 3 seconds if we have not received
+// expects a time in milliseconds
+pref ("security.mixed_content.hsts_priming_request_timeout", 3000);
 
 // Disable Storage api in release builds.
 #ifdef NIGHTLY_BUILD
 pref("dom.storageManager.enabled", true);
 #else
 pref("dom.storageManager.enabled", false);
 #endif
 
--- a/netwerk/protocol/http/HSTSPrimerListener.cpp
+++ b/netwerk/protocol/http/HSTSPrimerListener.cpp
@@ -10,55 +10,88 @@
 #include "nsIHstsPrimingCallback.h"
 #include "nsIPrincipal.h"
 #include "nsSecurityHeaderParser.h"
 #include "nsISiteSecurityService.h"
 #include "nsISocketProvider.h"
 #include "nsISSLStatus.h"
 #include "nsISSLStatusProvider.h"
 #include "nsStreamUtils.h"
+#include "nsStreamListenerWrapper.h"
 #include "nsHttpChannel.h"
 #include "LoadInfo.h"
+#include "mozilla/Unused.h"
 
 namespace mozilla {
 namespace net {
 
 using namespace mozilla;
 
 NS_IMPL_ISUPPORTS(HSTSPrimingListener, nsIStreamListener,
-                  nsIRequestObserver, nsIInterfaceRequestor)
+                  nsIRequestObserver, nsIInterfaceRequestor,
+                  nsITimerCallback)
+
+// default to 3000ms, same as the preference
+uint32_t HSTSPrimingListener::sHSTSPrimingTimeout = 3000;
+
+
+HSTSPrimingListener::HSTSPrimingListener(nsIHstsPrimingCallback* aCallback)
+  : mCallback(aCallback)
+{
+  static nsresult rv =
+    Preferences::AddUintVarCache(&sHSTSPrimingTimeout,
+        "security.mixed_content.hsts_priming_request_timeout");
+  Unused << rv;
+}
 
 NS_IMETHODIMP
 HSTSPrimingListener::GetInterface(const nsIID & aIID, void **aResult)
 {
   return QueryInterface(aIID, aResult);
 }
 
-NS_IMETHODIMP
-HSTSPrimingListener::OnStartRequest(nsIRequest *aRequest,
-                                    nsISupports *aContext)
+void
+HSTSPrimingListener::ReportTiming(nsresult aResult)
 {
-  nsresult primingResult = CheckHSTSPrimingRequestStatus(aRequest);
-  nsCOMPtr<nsIHstsPrimingCallback> callback(mCallback);
-  mCallback = nullptr;
-
   nsCOMPtr<nsITimedChannel> timingChannel =
-    do_QueryInterface(callback);
+    do_QueryInterface(mCallback);
   if (timingChannel) {
     TimeStamp channelCreationTime;
     nsresult rv = timingChannel->GetChannelCreation(&channelCreationTime);
     if (NS_SUCCEEDED(rv) && !channelCreationTime.IsNull()) {
       PRUint32 interval =
         (PRUint32) (TimeStamp::Now() - channelCreationTime).ToMilliseconds();
       Telemetry::Accumulate(Telemetry::HSTS_PRIMING_REQUEST_DURATION,
-          (NS_SUCCEEDED(primingResult)) ? NS_LITERAL_CSTRING("success")
-                                        : NS_LITERAL_CSTRING("failure"),
+          (NS_SUCCEEDED(aResult)) ? NS_LITERAL_CSTRING("success")
+                                  : NS_LITERAL_CSTRING("failure"),
           interval);
     }
   }
+}
+
+NS_IMETHODIMP
+HSTSPrimingListener::OnStartRequest(nsIRequest *aRequest,
+                                    nsISupports *aContext)
+{
+  nsCOMPtr<nsIHstsPrimingCallback> callback;
+  callback.swap(mCallback);
+
+  if (mHSTSPrimingTimer) {
+    Unused << mHSTSPrimingTimer->Cancel();
+    mHSTSPrimingTimer = nullptr;
+  }
+
+  // if callback is null, we have already canceled this request and reported
+  // the failure
+  if (!callback) {
+    return NS_OK;
+  }
+
+  nsresult primingResult = CheckHSTSPrimingRequestStatus(aRequest);
+  ReportTiming(primingResult);
 
   if (NS_FAILED(primingResult)) {
     LOG(("HSTS Priming Failed (request was not approved)"));
     return callback->OnHSTSPrimingFailed(primingResult, false);
   }
 
   LOG(("HSTS Priming Succeeded (request was approved)"));
   return callback->OnHSTSPrimingSucceeded(false);
@@ -134,22 +167,50 @@ HSTSPrimingListener::OnDataAvailable(nsI
                                      nsIInputStream *inStr,
                                      uint64_t sourceOffset,
                                      uint32_t count)
 {
   uint32_t totalRead;
   return inStr->ReadSegments(NS_DiscardSegment, nullptr, count, &totalRead);
 }
 
+/** nsITimerCallback **/
+NS_IMETHODIMP
+HSTSPrimingListener::Notify(nsITimer* timer)
+{
+  nsresult rv;
+  nsCOMPtr<nsIHstsPrimingCallback> callback;
+  callback.swap(mCallback);
+  if (!callback) {
+    // we already processed this channel
+    return NS_OK;
+  }
+
+  ReportTiming(NS_ERROR_HSTS_PRIMING_TIMEOUT);
+
+  if (mPrimingChannel) {
+    rv = mPrimingChannel->Cancel(NS_ERROR_HSTS_PRIMING_TIMEOUT);
+    if (NS_FAILED(rv)) {
+      NS_ERROR("HSTS Priming timed out, and we got an error canceling the priming channel.");
+    }
+  }
+
+  rv = callback->OnHSTSPrimingFailed(NS_ERROR_HSTS_PRIMING_TIMEOUT, false);
+  if (NS_FAILED(rv)) {
+    NS_ERROR("HSTS Priming timed out, and we got an error reporting the failure.");
+  }
+
+  return NS_OK; // unused
+}
+
 // static
 nsresult
 HSTSPrimingListener::StartHSTSPriming(nsIChannel* aRequestChannel,
                                       nsIHstsPrimingCallback* aCallback)
 {
-
   nsCOMPtr<nsIURI> finalChannelURI;
   nsresult rv = NS_GetFinalChannelURI(aRequestChannel, getter_AddRefs(finalChannelURI));
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIURI> uri;
   rv = NS_GetSecureUpgradedURI(finalChannelURI, getter_AddRefs(uri));
   NS_ENSURE_SUCCESS(rv,rv);
 
@@ -225,16 +286,18 @@ HSTSPrimingListener::StartHSTSPriming(ns
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Set method and headers
   nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(primingChannel);
   if (!httpChannel) {
     NS_ERROR("HSTSPrimingListener: Failed to QI to nsIHttpChannel!");
     return NS_ERROR_FAILURE;
   }
+  nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(primingChannel);
+  NS_ENSURE_STATE(internal);
 
   // Currently using HEAD per the draft, but under discussion to change to GET
   // with credentials so if the upgrade is approved the result is already cached.
   rv = httpChannel->SetRequestMethod(NS_LITERAL_CSTRING("HEAD"));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = httpChannel->
     SetRequestHeader(NS_LITERAL_CSTRING("Upgrade-Insecure-Requests"),
@@ -244,30 +307,51 @@ HSTSPrimingListener::StartHSTSPriming(ns
   // attempt to set the class of service flags on the new channel
   nsCOMPtr<nsIClassOfService> requestClass = do_QueryInterface(aRequestChannel);
   if (!requestClass) {
     NS_ERROR("HSTSPrimingListener: aRequestChannel is not an nsIClassOfService");
     return NS_ERROR_FAILURE;
   }
   nsCOMPtr<nsIClassOfService> primingClass = do_QueryInterface(httpChannel);
   if (!primingClass) {
-    NS_ERROR("HSTSPrimingListener: aRequestChannel is not an nsIClassOfService");
+    NS_ERROR("HSTSPrimingListener: httpChannel is not an nsIClassOfService");
     return NS_ERROR_FAILURE;
   }
 
   uint32_t classFlags = 0;
   rv = requestClass ->GetClassFlags(&classFlags);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = primingClass->SetClassFlags(classFlags);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Set up listener which will start the original channel
-  nsCOMPtr<nsIStreamListener> primingListener(new HSTSPrimingListener(aCallback));
+  // The priming channel should have highest priority so that it completes as
+  // quickly as possible, allowing the load to proceed.
+  nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(primingChannel);
+  if (p) {
+    uint32_t priority = nsISupportsPriority::PRIORITY_HIGHEST;
+
+    p->SetPriority(priority);
+  }
 
+  // Set up listener which will start the original channel
+  HSTSPrimingListener* listener = new HSTSPrimingListener(aCallback);
   // Start priming
-  rv = primingChannel->AsyncOpen2(primingListener);
+  rv = primingChannel->AsyncOpen2(listener);
   NS_ENSURE_SUCCESS(rv, rv);
+  listener->mPrimingChannel.swap(primingChannel);
+
+  nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID);
+  NS_ENSURE_STATE(timer);
+
+  rv = timer->InitWithCallback(listener,
+                               sHSTSPrimingTimeout,
+                               nsITimer::TYPE_ONE_SHOT);
+  if (NS_FAILED(rv)) {
+    NS_ERROR("HSTS Priming failed to initialize channel cancellation timer");
+  }
+
+  listener->mHSTSPrimingTimer.swap(timer);
 
   return NS_OK;
 }
 
 } // namespace net
 } // namespace mozilla
--- a/netwerk/protocol/http/HSTSPrimerListener.h
+++ b/netwerk/protocol/http/HSTSPrimerListener.h
@@ -44,36 +44,41 @@ enum HSTSPrimingResult {
   // of mixed-content and hsts, and mixed-content blocks the load
   eHSTS_PRIMING_SUCCEEDED_BLOCK   = 5,
   // When priming succeeds, but preferences require preservation of the order
   // of mixed-content and hsts, and mixed-content allows the load over http
   eHSTS_PRIMING_SUCCEEDED_HTTP    = 6,
   // HSTS priming failed, and the load is blocked by mixed-content
   eHSTS_PRIMING_FAILED_BLOCK      = 7,
   // HSTS priming failed, and the load is allowed by mixed-content
-  eHSTS_PRIMING_FAILED_ACCEPT     = 8
+  eHSTS_PRIMING_FAILED_ACCEPT     = 8,
+  // The HSTS Priming request timed out, and the load is blocked by
+  // mixed-content
+  eHSTS_PRIMING_TIMEOUT_BLOCK     = 9,
+  // The HSTS Priming request timed out, and the load is allowed by
+  // mixed-content
+  eHSTS_PRIMING_TIMEOUT_ACCEPT    = 10
 };
 
 //////////////////////////////////////////////////////////////////////////
 // Class used as streamlistener and notification callback when
 // doing the HEAD request for an HSTS Priming check. Needs to be an
 // nsIStreamListener in order to receive events from AsyncOpen2
 class HSTSPrimingListener final : public nsIStreamListener,
-                                  public nsIInterfaceRequestor
+                                  public nsIInterfaceRequestor,
+                                  public nsITimerCallback
 {
 public:
-  explicit HSTSPrimingListener(nsIHstsPrimingCallback* aCallback)
-   : mCallback(aCallback)
-  {
-  }
+  explicit HSTSPrimingListener(nsIHstsPrimingCallback* aCallback);
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSISTREAMLISTENER
   NS_DECL_NSIREQUESTOBSERVER
   NS_DECL_NSIINTERFACEREQUESTOR
+  NS_DECL_NSITIMERCALLBACK
 
 private:
   ~HSTSPrimingListener() {}
 
   // Only nsHttpChannel can invoke HSTS priming
   friend class mozilla::net::nsHttpChannel;
 
   /**
@@ -91,18 +96,38 @@ private:
                                    nsIHstsPrimingCallback* aCallback);
 
   /**
    * Given a request, return NS_OK if it has resulted in a cached HSTS update.
    * We don't need to check for the header as that has already been done for us.
    */
   nsresult CheckHSTSPrimingRequestStatus(nsIRequest* aRequest);
 
+  // send telemetry about how long HSTS priming requests take
+  void ReportTiming(nsresult aResult);
+
   /**
    * the nsIHttpChannel to notify with the result of HSTS priming.
    */
   nsCOMPtr<nsIHstsPrimingCallback> mCallback;
+
+  /**
+   * Keep a handle to the priming channel so we can cancel it on timeout
+   */
+  nsCOMPtr<nsIChannel> mPrimingChannel;
+
+  /**
+   * Keep a handle to the timer around so it can be canceled if we don't time
+   * out.
+   */
+  nsCOMPtr<nsITimer> mHSTSPrimingTimer;
+
+  /**
+   * How long (in ms) before an HSTS Priming channel times out.
+   * Preference: security.mixed_content.hsts_priming_request_timeout
+   */
+  static uint32_t sHSTSPrimingTimeout;
 };
 
 
 }} // mozilla::net
 
 #endif // HSTSPrimingListener_h__
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -8089,29 +8089,34 @@ nsHttpChannel::OnHSTSPrimingSucceeded(bo
  */
 nsresult
 nsHttpChannel::OnHSTSPrimingFailed(nsresult aError, bool aCached)
 {
     bool wouldBlock = mLoadInfo->GetMixedContentWouldBlock();
 
     LOG(("HSTS Priming Failed [this=%p], %s the load", this,
                 (wouldBlock) ? "blocking" : "allowing"));
-    if (aCached) {
+    if (aError == NS_ERROR_HSTS_PRIMING_TIMEOUT) {
+        // A priming request was sent, but timed out
+        Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT,
+                (wouldBlock) ?  HSTSPrimingResult::eHSTS_PRIMING_TIMEOUT_BLOCK :
+                HSTSPrimingResult::eHSTS_PRIMING_TIMEOUT_ACCEPT);
+    } else if (aCached) {
         // Between the time we marked for priming and started the priming request,
         // the host was found to not allow the upgrade, probably from another
         // priming request.
         Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT,
                 (wouldBlock) ?  HSTSPrimingResult::eHSTS_PRIMING_CACHED_BLOCK :
-                                HSTSPrimingResult::eHSTS_PRIMING_CACHED_NO_UPGRADE);
+                HSTSPrimingResult::eHSTS_PRIMING_CACHED_NO_UPGRADE);
     } else {
         // A priming request was sent, and no HSTS header was found that allows
         // the upgrade.
         Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT,
                 (wouldBlock) ?  HSTSPrimingResult::eHSTS_PRIMING_FAILED_BLOCK :
-                                HSTSPrimingResult::eHSTS_PRIMING_FAILED_ACCEPT);
+                HSTSPrimingResult::eHSTS_PRIMING_FAILED_ACCEPT);
     }
 
     // Don't visit again for at least
     // security.mixed_content.hsts_priming_cache_timeout seconds.
     nsISiteSecurityService* sss = gHttpHandler->GetSSService();
     NS_ENSURE_TRUE(sss, NS_ERROR_OUT_OF_MEMORY);
     nsresult rv = sss->CacheNegativeHSTSResult(mURI,
             nsMixedContentBlocker::sHSTSPrimingCacheTimeout);
--- a/netwerk/protocol/http/nsIHstsPrimingCallback.idl
+++ b/netwerk/protocol/http/nsIHstsPrimingCallback.idl
@@ -28,16 +28,17 @@ interface nsIHstsPrimingCallback : nsISu
    *
    * May be invoked synchronously if HSTS priming has already been performed
    * for the host.
    *
    * @param aCached whether the result was already in the HSTS cache
    */
   [noscript, nostdcall]
   void onHSTSPrimingSucceeded(in bool aCached);
+
   /**
    * HSTS priming has seen no STS header, the request itself has failed,
    * or some other failure which does not constitute a positive signal that the
    * site can be upgraded safely to HTTPS. The request may still be allowed
    * based on the user's preferences.
    *
    * May be invoked synchronously if HSTS priming has already been performed
    * for the host.
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8226,17 +8226,17 @@
     "description": "How often would blocked mixed content be allowed if HSTS upgrades were allowed, including how often would we send an HSTS priming request? 0=display/no-HSTS, 1=display/HSTS, 2=active/no-HSTS, 3=active/HSTS, 4=display/no-HSTS-priming, 5=display/do-HSTS-priming, 6=active/no-HSTS-priming, 7=active/do-HSTS-priming"
   },
   "MIXED_CONTENT_HSTS_PRIMING_RESULT": {
     "alert_emails": ["seceng@mozilla.org"],
     "bug_numbers": [1246540],
     "expires_in_version": "60",
     "kind": "enumerated",
     "n_values": 16,
-    "description": "How often do we get back an HSTS priming result which upgrades the connection to HTTPS? 0=cached (no upgrade), 1=cached (do upgrade), 2=cached (blocked), 3=already upgraded, 4=priming succeeded, 5=priming succeeded (block due to pref), 6=priming succeeded (no upgrade due to pref), 7=priming failed (block), 8=priming failed (accept)"
+    "description": "How often do we get back an HSTS priming result which upgrades the connection to HTTPS? 0=cached (no upgrade), 1=cached (do upgrade), 2=cached (blocked), 3=already upgraded, 4=priming succeeded, 5=priming succeeded (block due to pref), 6=priming succeeded (no upgrade due to pref), 7=priming failed (block), 8=priming failed (accept), 9=timeout (block), 10=timeout (accept)"
   },
   "HSTS_PRIMING_REQUEST_DURATION": {
     "alert_emails": ["seceng-telemetry@mozilla.org"],
     "bug_numbers": [1311893],
     "expires_in_version": "58",
     "kind": "exponential",
     "low": 100,
     "high": 30000,
--- a/xpcom/base/ErrorList.h
+++ b/xpcom/base/ErrorList.h
@@ -333,16 +333,20 @@
   ERROR(NS_NET_STATUS_CONNECTED_TO,    FAILURE(4)),
   ERROR(NS_NET_STATUS_SENDING_TO,      FAILURE(5)),
   ERROR(NS_NET_STATUS_WAITING_FOR,     FAILURE(10)),
   ERROR(NS_NET_STATUS_RECEIVING_FROM,  FAILURE(6)),
 
   /* nsIInterceptedChannel */
   /* Generic error for non-specific failures during service worker interception */
   ERROR(NS_ERROR_INTERCEPTION_FAILED,                  FAILURE(100)),
+
+  /* nsIHstsPrimingListener */
+  /* Error code for HSTS priming timeout to distinguish from blocking */
+  ERROR(NS_ERROR_HSTS_PRIMING_TIMEOUT, FAILURE(110)),
 #undef MODULE
 
 
   /* ======================================================================= */
   /* 7: NS_ERROR_MODULE_PLUGINS */
   /* ======================================================================= */
 #define MODULE NS_ERROR_MODULE_PLUGINS
   ERROR(NS_ERROR_PLUGINS_PLUGINSNOTCHANGED,        FAILURE(1000)),