From 27d5157e2b92a6ef3cc1eacfdec339e99664fd50 Mon Sep 17 00:00:00 2001 From: David Keeler Date: Mon, 15 Oct 2012 14:43:57 -0700 Subject: [PATCH] bug 786417 - filter the hsts preload list to sites that actually send the header r=bsmith, mayhemer --- modules/libpref/src/init/all.js | 3 + .../public/nsIStrictTransportSecurityService.idl | 8 +- netwerk/protocol/http/nsHttpChannel.cpp | 2 +- netwerk/test/TestSTSParser.cpp | 64 +++--- security/manager/boot/src/nsSTSPreloadList.errors | 85 ++++++++ security/manager/boot/src/nsSTSPreloadList.inc | 200 ++++++----------- .../boot/src/nsStrictTransportSecurityService.cpp | 50 ++++- .../boot/src/nsStrictTransportSecurityService.h | 4 +- .../manager/ssl/tests/unit/test_sts_preloadlist.js | 76 ++++--- security/manager/tools/getHSTSPreloadList.js | 242 +++++++++++++++++++++ security/manager/tools/getHSTSPreloadList.py | 115 ---------- 11 files changed, 522 insertions(+), 327 deletions(-) create mode 100644 security/manager/boot/src/nsSTSPreloadList.errors rewrite security/manager/boot/src/nsSTSPreloadList.inc (65%) create mode 100644 security/manager/tools/getHSTSPreloadList.js delete mode 100644 security/manager/tools/getHSTSPreloadList.py diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index fac9af852613..57c4dbb5ebd0 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -1249,6 +1249,9 @@ pref("network.proxy.autoconfig_url", ""); pref("network.proxy.autoconfig_retry_interval_min", 5); // 5 seconds pref("network.proxy.autoconfig_retry_interval_max", 300); // 5 minutes +// Use the HSTS preload list by default +pref("network.stricttransportsecurity.preloadlist", true); + pref("converter.html2txt.structs", true); // Output structured phrases (strong, em, code, sub, sup, b, i, u) pref("converter.html2txt.header_strategy", 1); // 0 = no indention; 1 = indention, increased with header level; 2 = numbering and slight indention diff --git a/netwerk/base/public/nsIStrictTransportSecurityService.idl b/netwerk/base/public/nsIStrictTransportSecurityService.idl index f809eb3f8a21..b65ace8797d7 100644 --- a/netwerk/base/public/nsIStrictTransportSecurityService.idl +++ b/netwerk/base/public/nsIStrictTransportSecurityService.idl @@ -8,7 +8,7 @@ interface nsIURI; interface nsIObserver; interface nsIHttpChannel; -[scriptable, uuid(16955eee-6c48-4152-9309-c42a465138a1)] +[scriptable, uuid(aee925d1-2bc9-469e-9582-b27b1d6b5192)] interface nsIStrictTransportSecurityService : nsISupports { /** @@ -20,13 +20,17 @@ interface nsIStrictTransportSecurityService : nsISupports * * @param aSourceURI the URI of the resource with the HTTP header. * @param aHeader the HTTP response header specifying STS data. + * @param aMaxAge the parsed max-age directive of the header. + * @param aIncludeSubdomains the parsed includeSubdomains directive. * @return NS_OK if it succeeds * NS_ERROR_FAILURE if it can't be parsed * NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA * if there are unrecognized tokens in the header. */ void processStsHeader(in nsIURI aSourceURI, - in string aHeader); + in string aHeader, + [optional] out unsigned long long aMaxAge, + [optional] out boolean aIncludeSubdomains); /** * Removes the STS state of a host, including the includeSubdomains state diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp index a016981ea867..d72e81746fd4 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -1149,7 +1149,7 @@ nsHttpChannel::ProcessSTSHeader() // All other failures are fatal. NS_ENSURE_SUCCESS(rv, rv); - rv = stss->ProcessStsHeader(mURI, stsHeader.get()); + rv = stss->ProcessStsHeader(mURI, stsHeader.get(), NULL, NULL); if (NS_FAILED(rv)) { LOG(("STS: Failed to parse STS header, continuing load.\n")); return NS_OK; diff --git a/netwerk/test/TestSTSParser.cpp b/netwerk/test/TestSTSParser.cpp index 7dc525ecdac3..0f8469aec47b 100644 --- a/netwerk/test/TestSTSParser.cpp +++ b/netwerk/test/TestSTSParser.cpp @@ -39,6 +39,7 @@ bool TestSuccess(const char* hdr, bool extraTokens, + uint64_t expectedMaxAge, bool expectedIncludeSubdomains, nsIStrictTransportSecurityService* stss, nsIPermissionManager* pm) { @@ -46,9 +47,14 @@ TestSuccess(const char* hdr, bool extraTokens, nsresult rv = NS_NewURI(getter_AddRefs(dummyUri), "https://foo.com/bar.html"); EXPECT_SUCCESS(rv, "Failed to create URI"); - rv = stss->ProcessStsHeader(dummyUri, hdr); + uint64_t maxAge = 0; + bool includeSubdomains = false; + rv = stss->ProcessStsHeader(dummyUri, hdr, &maxAge, &includeSubdomains); EXPECT_SUCCESS(rv, "Failed to process valid header: %s", hdr); + REQUIRE_EQUAL(maxAge, expectedMaxAge, "Did not correctly parse maxAge"); + REQUIRE_EQUAL(includeSubdomains, expectedIncludeSubdomains, "Did not correctly parse presence/absence of includeSubdomains"); + if (extraTokens) { REQUIRE_EQUAL(rv, NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA, "Extra tokens were expected when parsing, but were not encountered."); @@ -68,7 +74,7 @@ bool TestFailure(const char* hdr, nsresult rv = NS_NewURI(getter_AddRefs(dummyUri), "https://foo.com/bar.html"); EXPECT_SUCCESS(rv, "Failed to create URI"); - rv = stss->ProcessStsHeader(dummyUri, hdr); + rv = stss->ProcessStsHeader(dummyUri, hdr, NULL, NULL); EXPECT_FAILURE(rv, "Parsed invalid header: %s", hdr); passed(hdr); return true; @@ -106,36 +112,36 @@ main(int32_t argc, char *argv[]) printf("*** Attempting to parse valid STS headers ...\n"); // SHOULD SUCCEED: - rvs.AppendElement(TestSuccess("max-age=100", false, stss, pm)); - rvs.AppendElement(TestSuccess("max-age =100", false, stss, pm)); - rvs.AppendElement(TestSuccess(" max-age=100", false, stss, pm)); - rvs.AppendElement(TestSuccess("max-age = 100 ", false, stss, pm)); - rvs.AppendElement(TestSuccess("max-age = 100 ", false, stss, pm)); - - rvs.AppendElement(TestSuccess("maX-aGe=100", false, stss, pm)); - rvs.AppendElement(TestSuccess("MAX-age =100", false, stss, pm)); - rvs.AppendElement(TestSuccess("max-AGE=100", false, stss, pm)); - rvs.AppendElement(TestSuccess("Max-Age = 100 ", false, stss, pm)); - rvs.AppendElement(TestSuccess("MAX-AGE = 100 ", false, stss, pm)); - - rvs.AppendElement(TestSuccess("max-age=100;includeSubdomains", false, stss, pm)); - rvs.AppendElement(TestSuccess("max-age=100; includeSubdomains", false, stss, pm)); - rvs.AppendElement(TestSuccess(" max-age=100; includeSubdomains", false, stss, pm)); - rvs.AppendElement(TestSuccess("max-age = 100 ; includeSubdomains", false, stss, pm)); - rvs.AppendElement(TestSuccess("max-age = 100 ; includeSubdomains", false, stss, pm)); - - rvs.AppendElement(TestSuccess("maX-aGe=100; includeSUBDOMAINS", false, stss, pm)); - rvs.AppendElement(TestSuccess("MAX-age =100; includeSubDomains", false, stss, pm)); - rvs.AppendElement(TestSuccess("max-AGE=100; iNcLuDeSuBdoMaInS", false, stss, pm)); - rvs.AppendElement(TestSuccess("Max-Age = 100; includesubdomains ", false, stss, pm)); - rvs.AppendElement(TestSuccess("INCLUDESUBDOMAINS;MaX-AgE = 100 ", false, stss, pm)); + rvs.AppendElement(TestSuccess("max-age=100", false, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess("max-age =100", false, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess(" max-age=100", false, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess("max-age = 100 ", false, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess("max-age = 100 ", false, 100, false, stss, pm)); + + rvs.AppendElement(TestSuccess("maX-aGe=100", false, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess("MAX-age =100", false, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess("max-AGE=100", false, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess("Max-Age = 100 ", false, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess("MAX-AGE = 100 ", false, 100, false, stss, pm)); + + rvs.AppendElement(TestSuccess("max-age=100;includeSubdomains", false, 100, true, stss, pm)); + rvs.AppendElement(TestSuccess("max-age=100; includeSubdomains", false, 100, true, stss, pm)); + rvs.AppendElement(TestSuccess(" max-age=100; includeSubdomains", false, 100, true, stss, pm)); + rvs.AppendElement(TestSuccess("max-age = 100 ; includeSubdomains", false, 100, true, stss, pm)); + rvs.AppendElement(TestSuccess("max-age = 100 ; includeSubdomains", false, 100, true, stss, pm)); + + rvs.AppendElement(TestSuccess("maX-aGe=100; includeSUBDOMAINS", false, 100, true, stss, pm)); + rvs.AppendElement(TestSuccess("MAX-age =100; includeSubDomains", false, 100, true, stss, pm)); + rvs.AppendElement(TestSuccess("max-AGE=100; iNcLuDeSuBdoMaInS", false, 100, true, stss, pm)); + rvs.AppendElement(TestSuccess("Max-Age = 100; includesubdomains ", false, 100, true, stss, pm)); + rvs.AppendElement(TestSuccess("INCLUDESUBDOMAINS;MaX-AgE = 100 ", false, 100, true, stss, pm)); // these are weird tests, but are testing that some extended syntax is // still allowed (but it is ignored) - rvs.AppendElement(TestSuccess("max-age=100randomstuffhere", true, stss, pm)); - rvs.AppendElement(TestSuccess("max-age=100 includesubdomains", true, stss, pm)); - rvs.AppendElement(TestSuccess("max-age=100 bar foo", true, stss, pm)); - rvs.AppendElement(TestSuccess("max-age=100 ; includesubdomainsSomeStuff", true, stss, pm)); + rvs.AppendElement(TestSuccess("max-age=100randomstuffhere", true, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess("max-age=100 includesubdomains", true, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess("max-age=100 bar foo", true, 100, false, stss, pm)); + rvs.AppendElement(TestSuccess("max-age=100 ; includesubdomainsSomeStuff", true, 100, false, stss, pm)); rv0 = rvs.Contains(false) ? 1 : 0; if (rv0 == 0) diff --git a/security/manager/boot/src/nsSTSPreloadList.errors b/security/manager/boot/src/nsSTSPreloadList.errors new file mode 100644 index 000000000000..91407a9f9cd2 --- /dev/null +++ b/security/manager/boot/src/nsSTSPreloadList.errors @@ -0,0 +1,85 @@ +accounts.google.com: max-age too low: 2592000 +aladdinschools.appspot.com: did not receive HSTS header +api.recurly.com: did not receive HSTS header +apis.google.com: did not receive HSTS header +appengine.google.com: did not receive HSTS header +betnet.fr: could not connect to host +bigshinylock.minazo.net: could not connect to host +braintreegateway.com: could not connect to host +braintreepayments.com: did not receive HSTS header +browserid.org: did not receive HSTS header +cert.se: did not receive HSTS header +checkout.google.com: did not receive HSTS header +chrome.google.com: did not receive HSTS header +chromiumcodereview.appspot.com: did not receive HSTS header +codereview.appspot.com: did not receive HSTS header +docs.google.com: did not receive HSTS header +download.jitsi.org: did not receive HSTS header +drive.google.com: did not receive HSTS header +dropcam.com: did not receive HSTS header +emailprivacytester.com: max-age too low: 8640000 +encrypted.google.com: did not receive HSTS header +entropia.de: max-age too low: 2678402 +epoxate.com: max-age too low: 259200 +fatzebra.com.au: did not receive HSTS header +gmail.com: did not receive HSTS header +googlemail.com: did not receive HSTS header +googleplex.com: could not connect to host +greplin.com: did not receive HSTS header +grepular.com: max-age too low: 8640000 +groups.google.com: did not receive HSTS header +health.google.com: did not receive HSTS header +hostedtalkgadget.google.com: did not receive HSTS header +howrandom.org: max-age too low: 2592000 +iop.intuit.com: did not receive HSTS header +irccloud.com: did not receive HSTS header +jitsi.org: did not receive HSTS header +jottit.com: could not connect to host +kyps.net: did not receive HSTS header +lastpass.com: max-age too low: 8640000 +ledgerscope.net: max-age too low: 86400 +linx.net: could not connect to host +lists.mayfirst.org: did not receive HSTS header +login.persona.org: max-age too low: 2592000 +lookout.com: did not receive HSTS header +mail.google.com: did not receive HSTS header +market.android.com: did not receive HSTS header +mydigipass.com: did not receive HSTS header +mylookout.com: did not receive HSTS header +neonisi.com: could not connect to host +ottospora.nl: could not connect to host +packagist.org: max-age too low: 2592000 +plus.google.com: did not receive HSTS header +profiles.google.com: did not receive HSTS header +romab.com: max-age too low: 2628000 +script.google.com: did not receive HSTS header +shops.neonisi.com: could not connect to host +simon.butcher.name: max-age too low: 2629743 +sites.google.com: did not receive HSTS header +sol.io: could not connect to host +spreadsheets.google.com: did not receive HSTS header +squareup.com: max-age too low: 1296000 +ssl.google-analytics.com: did not receive HSTS header +sunshinepress.org: could not connect to host +talk.google.com: did not receive HSTS header +talkgadget.google.com: did not receive HSTS header +torproject.org: did not receive HSTS header +uprotect.it: could not connect to host +www.developer.mydigipass.com: did not receive HSTS header +www.dropcam.com: max-age too low: 2592000 +www.entropia.de: max-age too low: 2678402 +www.gmail.com: did not receive HSTS header +www.googlemail.com: did not receive HSTS header +www.greplin.com: did not receive HSTS header +www.irccloud.com: did not receive HSTS header +www.jitsi.org: did not receive HSTS header +www.kyps.net: did not receive HSTS header +www.lastpass.com: did not receive HSTS header +www.ledgerscope.net: max-age too low: 86400 +www.logentries.com: did not receive HSTS header +www.makeyourlaws.org: did not receive HSTS header +www.moneybookers.com: did not receive HSTS header +www.neonisi.com: could not connect to host +www.paycheckrecords.com: did not receive HSTS header +www.paypal.com: max-age too low: 14400 +www.sandbox.mydigipass.com: did not receive HSTS header diff --git a/security/manager/boot/src/nsSTSPreloadList.inc b/security/manager/boot/src/nsSTSPreloadList.inc dissimilarity index 65% index 39d5d10b47e6..8d4f5b6df11a 100644 --- a/security/manager/boot/src/nsSTSPreloadList.inc +++ b/security/manager/boot/src/nsSTSPreloadList.inc @@ -1,135 +1,65 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/*****************************************************************************/ -/* This is an automatically generated file. If you're not */ -/* nsStrictTransportSecurityService.cpp, you shouldn't be #including it. */ -/*****************************************************************************/ - -#include - -class nsSTSPreload -{ - public: - const char *mHost; - const bool mIncludeSubdomains; -}; - -static const nsSTSPreload kSTSPreloadList[] = { - { "accounts.google.com", true }, - { "aladdinschools.appspot.com", false }, - { "alpha.irccloud.com", false }, - { "api.recurly.com", true }, - { "apis.google.com", true }, - { "app.recurly.com", true }, - { "appengine.google.com", false }, - { "arivo.com.br", true }, - { "betnet.fr", true }, - { "bigshinylock.minazo.net", true }, - { "blog.torproject.org", true }, - { "braintreegateway.com", true }, - { "braintreepayments.com", false }, - { "browserid.org", true }, - { "business.medbank.com.mt", true }, - { "cert.se", true }, - { "check.torproject.org", true }, - { "checkout.google.com", true }, - { "chrome.google.com", true }, - { "chromiumcodereview.appspot.com", true }, - { "cloudsecurityalliance.org", true }, - { "codereview.appspot.com", true }, - { "crate.io", true }, - { "crypto.cat", true }, - { "crypto.is", true }, - { "developer.mydigipass.com", false }, - { "docs.google.com", true }, - { "download.jitsi.org", false }, - { "drive.google.com", true }, - { "dropcam.com", false }, - { "ebanking.indovinabank.com.vn", true }, - { "emailprivacytester.com", false }, - { "encrypted.google.com", true }, - { "entropia.de", false }, - { "epoxate.com", false }, - { "factor.cc", false }, - { "gmail.com", false }, - { "googlemail.com", false }, - { "googleplex.com", true }, - { "greplin.com", false }, - { "grepular.com", true }, - { "groups.google.com", true }, - { "health.google.com", true }, - { "hostedtalkgadget.google.com", true }, - { "howrandom.org", true }, - { "id.mayfirst.org", false }, - { "irccloud.com", false }, - { "jitsi.org", false }, - { "jottit.com", true }, - { "keyerror.com", true }, - { "kyps.net", false }, - { "lastpass.com", false }, - { "ledgerscope.net", false }, - { "linx.net", true }, - { "lists.mayfirst.org", false }, - { "logentries.com", false }, - { "login.persona.org", true }, - { "login.sapo.pt", true }, - { "luneta.nearbuysystems.com", true }, - { "mail.google.com", true }, - { "market.android.com", true }, - { "mattmccutchen.net", true }, - { "members.mayfirst.org", false }, - { "mydigipass.com", false }, - { "neg9.org", false }, - { "neonisi.com", false }, - { "ottospora.nl", true }, - { "passwd.io", true }, - { "piratenlogin.de", true }, - { "pixi.me", true }, - { "plus.google.com", true }, - { "profiles.google.com", true }, - { "riseup.net", true }, - { "romab.com", true }, - { "sandbox.mydigipass.com", false }, - { "script.google.com", true }, - { "shops.neonisi.com", true }, - { "simon.butcher.name", true }, - { "sites.google.com", true }, - { "sol.io", true }, - { "spreadsheets.google.com", true }, - { "squareup.com", false }, - { "ssl.google-analytics.com", true }, - { "stripe.com", true }, - { "sunshinepress.org", true }, - { "support.mayfirst.org", false }, - { "talk.google.com", true }, - { "talkgadget.google.com", true }, - { "torproject.org", false }, - { "ubertt.org", true }, - { "uprotect.it", true }, - { "www.apollo-auto.com", true }, - { "www.braintreepayments.com", false }, - { "www.cueup.com", true }, - { "www.developer.mydigipass.com", false }, - { "www.dropcam.com", false }, - { "www.elanex.biz", false }, - { "www.entropia.de", false }, - { "www.gmail.com", false }, - { "www.googlemail.com", false }, - { "www.greplin.com", false }, - { "www.irccloud.com", false }, - { "www.jitsi.org", false }, - { "www.kyps.net", false }, - { "www.lastpass.com", false }, - { "www.ledgerscope.net", false }, - { "www.logentries.com", false }, - { "www.moneybookers.com", true }, - { "www.mydigipass.com", false }, - { "www.neonisi.com", true }, - { "www.noisebridge.net", false }, - { "www.paycheckrecords.com", false }, - { "www.paypal.com", false }, - { "www.sandbox.mydigipass.com", false }, - { "www.torproject.org", true }, -}; +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/*****************************************************************************/ +/* This is an automatically generated file. If you're not */ +/* nsStrictTransportSecurityService.cpp, you shouldn't be #including it. */ +/*****************************************************************************/ + +class nsSTSPreload +{ + public: + const char *mHost; + const bool mIncludeSubdomains; +}; + +static const nsSTSPreload kSTSPreloadList[] = { + { "alpha.irccloud.com", false }, + { "api.intercom.io", false }, + { "app.recurly.com", false }, + { "arivo.com.br", true }, + { "blog.torproject.org", false }, + { "business.medbank.com.mt", true }, + { "check.torproject.org", false }, + { "cloudsecurityalliance.org", false }, + { "crate.io", true }, + { "crypto.cat", true }, + { "crypto.is", true }, + { "csawctf.poly.edu", true }, + { "developer.mydigipass.com", false }, + { "dm.lookout.com", false }, + { "dm.mylookout.com", false }, + { "ebanking.indovinabank.com.vn", false }, + { "factor.cc", false }, + { "id.mayfirst.org", false }, + { "intercom.io", false }, + { "keyerror.com", true }, + { "logentries.com", false }, + { "login.sapo.pt", true }, + { "luneta.nearbuysystems.com", false }, + { "makeyourlaws.org", false }, + { "mattmccutchen.net", true }, + { "members.mayfirst.org", false }, + { "neg9.org", false }, + { "passwd.io", true }, + { "piratenlogin.de", true }, + { "pixi.me", true }, + { "riseup.net", true }, + { "sandbox.mydigipass.com", false }, + { "stripe.com", true }, + { "support.mayfirst.org", false }, + { "surfeasy.com", false }, + { "ubertt.org", true }, + { "www.apollo-auto.com", true }, + { "www.braintreepayments.com", false }, + { "www.cueup.com", false }, + { "www.elanex.biz", false }, + { "www.intercom.io", false }, + { "www.lookout.com", false }, + { "www.mydigipass.com", false }, + { "www.mylookout.com", false }, + { "www.noisebridge.net", false }, + { "www.surfeasy.com", false }, + { "www.torproject.org", false }, +}; diff --git a/security/manager/boot/src/nsStrictTransportSecurityService.cpp b/security/manager/boot/src/nsStrictTransportSecurityService.cpp index 9bbf1e42188a..94da57fe63ee 100644 --- a/security/manager/boot/src/nsStrictTransportSecurityService.cpp +++ b/security/manager/boot/src/nsStrictTransportSecurityService.cpp @@ -16,6 +16,7 @@ #include "nsThreadUtils.h" #include "nsStringGlue.h" #include "nsIScriptSecurityManager.h" +#include "mozilla/Preferences.h" // A note about the preload list: // When a site specifically disables sts by sending a header with @@ -67,7 +68,7 @@ nsSTSHostEntry::nsSTSHostEntry(const nsSTSHostEntry& toCopy) nsStrictTransportSecurityService::nsStrictTransportSecurityService() - : mInPrivateMode(false) + : mInPrivateMode(false), mUsePreloadList(true) { } @@ -93,6 +94,8 @@ nsStrictTransportSecurityService::Init() if (pbs) pbs->GetPrivateBrowsingEnabled(&mInPrivateMode); + mUsePreloadList = mozilla::Preferences::GetBool("network.stricttransportsecurity.preloadlist", true); + mozilla::Preferences::AddStrongObserver(this, "network.stricttransportsecurity.preloadlist"); mObserverService = mozilla::services::GetObserverService(); if (mObserverService) mObserverService->AddObserver(this, NS_PRIVATE_BROWSING_SWITCH_TOPIC, false); @@ -209,22 +212,35 @@ nsStrictTransportSecurityService::RemoveStsState(nsIURI* aURI) NS_IMETHODIMP nsStrictTransportSecurityService::ProcessStsHeader(nsIURI* aSourceURI, - const char* aHeader) + const char* aHeader, + uint64_t *aMaxAge, + bool *aIncludeSubdomains) { // Should be called on the main thread (or via proxy) since the permission // manager is used and it's not threadsafe. NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED); + if (aMaxAge != nullptr) { + *aMaxAge = 0; + } + + if (aIncludeSubdomains != nullptr) { + *aIncludeSubdomains = false; + } + char * header = NS_strdup(aHeader); if (!header) return NS_ERROR_OUT_OF_MEMORY; - nsresult rv = ProcessStsHeaderMutating(aSourceURI, header); + nsresult rv = ProcessStsHeaderMutating(aSourceURI, header, aMaxAge, + aIncludeSubdomains); NS_Free(header); return rv; } nsresult nsStrictTransportSecurityService::ProcessStsHeaderMutating(nsIURI* aSourceURI, - char* aHeader) + char* aHeader, + uint64_t *aMaxAge, + bool *aIncludeSubdomains) { STSLOG(("STS: ProcessStrictTransportHeader(%s)\n", aHeader)); @@ -317,6 +333,14 @@ nsStrictTransportSecurityService::ProcessStsHeaderMutating(nsIURI* aSourceURI, // record the successfully parsed header data. SetStsState(aSourceURI, maxAge, includeSubdomains); + if (aMaxAge != nullptr) { + *aMaxAge = (uint64_t)maxAge; + } + + if (aIncludeSubdomains != nullptr) { + *aIncludeSubdomains = includeSubdomains; + } + return foundUnrecognizedTokens ? NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA : NS_OK; @@ -350,11 +374,16 @@ int STSPreloadCompare(const void *key, const void *entry) const nsSTSPreload * nsStrictTransportSecurityService::GetPreloadListEntry(const char *aHost) { - return (const nsSTSPreload *) bsearch(aHost, - kSTSPreloadList, - PR_ARRAY_SIZE(kSTSPreloadList), - sizeof(nsSTSPreload), - STSPreloadCompare); + if (mUsePreloadList) { + return (const nsSTSPreload *) bsearch(aHost, + kSTSPreloadList, + PR_ARRAY_SIZE(kSTSPreloadList), + sizeof(nsSTSPreload), + STSPreloadCompare); + } + else { + return nullptr; + } } NS_IMETHODIMP @@ -552,6 +581,9 @@ nsStrictTransportSecurityService::Observe(nsISupports *subject, mInPrivateMode = false; } } + else if (strcmp(topic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) { + mUsePreloadList = mozilla::Preferences::GetBool("network.stricttransportsecurity.preloadlist", true); + } return NS_OK; } diff --git a/security/manager/boot/src/nsStrictTransportSecurityService.h b/security/manager/boot/src/nsStrictTransportSecurityService.h index 6df96e09fe8c..216c08768995 100644 --- a/security/manager/boot/src/nsStrictTransportSecurityService.h +++ b/security/manager/boot/src/nsStrictTransportSecurityService.h @@ -136,7 +136,8 @@ private: nsresult GetHost(nsIURI *aURI, nsACString &aResult); nsresult GetPrincipalForURI(nsIURI *aURI, nsIPrincipal **aPrincipal); nsresult SetStsState(nsIURI* aSourceURI, int64_t maxage, bool includeSubdomains); - nsresult ProcessStsHeaderMutating(nsIURI* aSourceURI, char* aHeader); + nsresult ProcessStsHeaderMutating(nsIURI* aSourceURI, char* aHeader, + uint64_t *aMaxAge, bool *aIncludeSubdomains); const nsSTSPreload *GetPreloadListEntry(const char *aHost); // private-mode-preserving permission manager overlay functions @@ -154,6 +155,7 @@ private: bool mInPrivateMode; nsTHashtable mPrivateModeHostTable; + bool mUsePreloadList; }; #endif // __nsStrictTransportSecurityService_h__ diff --git a/security/manager/ssl/tests/unit/test_sts_preloadlist.js b/security/manager/ssl/tests/unit/test_sts_preloadlist.js index 8c340720168c..af9104035679 100644 --- a/security/manager/ssl/tests/unit/test_sts_preloadlist.js +++ b/security/manager/ssl/tests/unit/test_sts_preloadlist.js @@ -35,9 +35,9 @@ var gObserver = new Observer(); // This is a list of every host we call processStsHeader with // (we have to remove any state added to the sts service so as to not muck // with other tests). -var hosts = ["http://keyerror.com", "http://subdomain.kyps.net", - "http://subdomain.cert.se", "http://crypto.cat", - "http://www.logentries.com"]; +var hosts = ["http://keyerror.com", "http://subdomain.intercom.io", + "http://subdomain.pixi.me", "http://crypto.cat", + "http://logentries.com"]; function cleanup() { Services.obs.removeObserver(gObserver, "private-browsing-transition-complete"); @@ -72,27 +72,33 @@ function test_part1() { do_check_false(gSTSService.isStsHost("com")); // Note: the following were taken from the STS preload list - // as of June 2012. If the list changes, this test will need to be modified. + // as of Sept. 2012. If the list changes, this test will need to be modified. + // check that the pref to toggle using the preload list works + Services.prefs.setBoolPref("network.stricttransportsecurity.preloadlist", false); + do_check_false(gSTSService.isStsHost("factor.cc")); + Services.prefs.setBoolPref("network.stricttransportsecurity.preloadlist", true); + do_check_true(gSTSService.isStsHost("factor.cc")); + // check that an entry at the beginning of the list is an sts host - do_check_true(gSTSService.isStsHost("health.google.com")); + do_check_true(gSTSService.isStsHost("arivo.com.br")); // check that a subdomain is an sts host (includeSubdomains is set) - do_check_true(gSTSService.isStsHost("subdomain.health.google.com")); + do_check_true(gSTSService.isStsHost("subdomain.arivo.com.br")); // check that another subdomain is an sts host (includeSubdomains is set) - do_check_true(gSTSService.isStsHost("a.b.c.subdomain.health.google.com")); + do_check_true(gSTSService.isStsHost("a.b.c.subdomain.arivo.com.br")); // check that an entry in the middle of the list is an sts host - do_check_true(gSTSService.isStsHost("epoxate.com")); + do_check_true(gSTSService.isStsHost("neg9.org")); // check that a subdomain is not an sts host (includeSubdomains is not set) - do_check_false(gSTSService.isStsHost("subdomain.epoxate.com")); + do_check_false(gSTSService.isStsHost("subdomain.neg9.org")); // check that an entry at the end of the list is an sts host - do_check_true(gSTSService.isStsHost("www.googlemail.com")); + do_check_true(gSTSService.isStsHost("www.noisebridge.net")); // check that a subdomain is not an sts host (includeSubdomains is not set) - do_check_false(gSTSService.isStsHost("a.subdomain.www.googlemail.com")); + do_check_false(gSTSService.isStsHost("a.subdomain.www.noisebridge.net")); // check that a host with a dot on the end won't break anything do_check_false(gSTSService.isStsHost("notsts.nonexistent.mozilla.com.")); @@ -112,35 +118,35 @@ function test_part1() { // check that processing a header with max-age: 0 from a subdomain of a site // will not remove that (ancestor) site from the list - var uri = Services.io.newURI("http://subdomain.kyps.net", null, null); + var uri = Services.io.newURI("http://subdomain.intercom.io", null, null); gSTSService.processStsHeader(uri, "max-age=0"); - do_check_true(gSTSService.isStsHost("kyps.net")); - do_check_false(gSTSService.isStsHost("subdomain.kyps.net")); + do_check_true(gSTSService.isStsHost("intercom.io")); + do_check_false(gSTSService.isStsHost("subdomain.intercom.io")); - var uri = Services.io.newURI("http://subdomain.cert.se", null, null); + var uri = Services.io.newURI("http://subdomain.pixi.me", null, null); gSTSService.processStsHeader(uri, "max-age=0"); // we received a header with "max-age=0", so we have "no information" - // regarding the sts state of subdomain.cert.se specifically, but - // it is actually still an STS host, because of the preloaded cert.se + // regarding the sts state of subdomain.pixi.me specifically, but + // it is actually still an STS host, because of the preloaded pixi.me // including subdomains. // Here's a drawing: - // |-- cert.se (in preload list, includes subdomains) IS sts host - // |-- subdomain.cert.se IS sts host - // | `-- another.subdomain.cert.se IS sts host - // `-- sibling.cert.se IS sts host - do_check_true(gSTSService.isStsHost("subdomain.cert.se")); - do_check_true(gSTSService.isStsHost("sibling.cert.se")); - do_check_true(gSTSService.isStsHost("another.subdomain.cert.se")); + // |-- pixi.me (in preload list, includes subdomains) IS sts host + // |-- subdomain.pixi.me IS sts host + // | `-- another.subdomain.pixi.me IS sts host + // `-- sibling.pixi.me IS sts host + do_check_true(gSTSService.isStsHost("subdomain.pixi.me")); + do_check_true(gSTSService.isStsHost("sibling.pixi.me")); + do_check_true(gSTSService.isStsHost("another.subdomain.pixi.me")); gSTSService.processStsHeader(uri, "max-age=1000"); // Here's what we have now: - // |-- cert.se (in preload list, includes subdomains) IS sts host - // |-- subdomain.cert.se (include subdomains is false) IS sts host - // | `-- another.subdomain.cert.se IS NOT sts host - // `-- sibling.cert.se IS sts host - do_check_true(gSTSService.isStsHost("subdomain.cert.se")); - do_check_true(gSTSService.isStsHost("sibling.cert.se")); - do_check_false(gSTSService.isStsHost("another.subdomain.cert.se")); + // |-- pixi.me (in preload list, includes subdomains) IS sts host + // |-- subdomain.pixi.me (include subdomains is false) IS sts host + // | `-- another.subdomain.pixi.me IS NOT sts host + // `-- sibling.pixi.me IS sts host + do_check_true(gSTSService.isStsHost("subdomain.pixi.me")); + do_check_true(gSTSService.isStsHost("sibling.pixi.me")); + do_check_false(gSTSService.isStsHost("another.subdomain.pixi.me")); // Test private browsing correctly interacts with removing preloaded sites. // If we don't have the private browsing service, don't run those tests @@ -183,12 +189,12 @@ function test_private_browsing1() { // a site on the preload list, and that header later expires. We need to // then treat that host as no longer an sts host.) // (sanity check first - this should be in the preload list) - do_check_true(gSTSService.isStsHost("www.logentries.com")); - var uri = Services.io.newURI("http://www.logentries.com", null, null); + do_check_true(gSTSService.isStsHost("logentries.com")); + var uri = Services.io.newURI("http://logentries.com", null, null); // according to the rfc, max-age can't be negative, but this is a great // way to test an expired entry gSTSService.processStsHeader(uri, "max-age=-1000"); - do_check_false(gSTSService.isStsHost("www.logentries.com")); + do_check_false(gSTSService.isStsHost("logentries.com")); // if this test gets this far, it means there's a private browsing service getPBSvc().privateBrowsingEnabled = false; @@ -202,7 +208,7 @@ function test_private_browsing2() { // Now that we're out of private browsing mode, we need to make sure // we've "forgotten" that we "forgot" this site's sts status. - do_check_true(gSTSService.isStsHost("www.logentries.com")); + do_check_true(gSTSService.isStsHost("logentries.com")); run_next_test(); } diff --git a/security/manager/tools/getHSTSPreloadList.js b/security/manager/tools/getHSTSPreloadList.js new file mode 100644 index 000000000000..b449b8925b72 --- /dev/null +++ b/security/manager/tools/getHSTSPreloadList.js @@ -0,0 +1,242 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// +// +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +// Register resource://app/ URI +let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); +let resHandler = ios.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); +let mozDir = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("CurProcD", Ci.nsILocalFile); +let mozDirURI = ios.newFileURI(mozDir); +resHandler.setSubstitution("app", mozDirURI); + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource:///modules/XPCOMUtils.jsm"); + +const SOURCE = "https://src.chromium.org/viewvc/chrome/trunk/src/net/base/transport_security_state_static.json"; +const OUTPUT = "nsSTSPreloadList.inc"; +const ERROR_OUTPUT = "nsSTSPreloadList.errors"; +const MINIMUM_REQUIRED_MAX_AGE = 60 * 60 * 24 * 7 * 18; +const PREFIX = "/* This Source Code Form is subject to the terms of the Mozilla Public\n" + +" * License, v. 2.0. If a copy of the MPL was not distributed with this\n" + +" * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n" + +"\n" + +"/*****************************************************************************/\n" + +"/* This is an automatically generated file. If you're not */\n" + +"/* nsStrictTransportSecurityService.cpp, you shouldn't be #including it. */\n" + +"/*****************************************************************************/\n" + +"\n" + +"class nsSTSPreload\n" + +"{\n" + +" public:\n" + +" const char *mHost;\n" + +" const bool mIncludeSubdomains;\n" + +"};\n" + +"\n" + +"static const nsSTSPreload kSTSPreloadList[] = {\n"; +const POSTFIX = "};\n"; + +function download() { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + req.open("GET", SOURCE, false); // doing the request synchronously + try { + req.send(); + } + catch (e) { + throw "ERROR: problem downloading '" + SOURCE + "': " + e; + } + + if (req.status != 200) { + throw "ERROR: problem downloading '" + SOURCE + "': status " + req.status; + } + + // we have to filter out '//' comments + var result = req.responseText.replace(/\/\/[^\n]*\n/g, ""); + var data = null; + try { + data = JSON.parse(result); + } + catch (e) { + throw "ERROR: could not parse data from '" + SOURCE + "': " + e; + } + return data; +} + +function getHosts(rawdata) { + var hosts = []; + + if (!rawdata || !rawdata.entries) { + throw "ERROR: source data not formatted correctly: 'entries' not found"; + } + + for (entry of rawdata.entries) { + if (entry.mode && entry.mode == "force-https") { + if (entry.name) { + hosts.push(entry); + } else { + throw "ERROR: entry not formatted correctly: no name found"; + } + } + } + + return hosts; +} + +var gSTSService = Cc["@mozilla.org/stsservice;1"] + .getService(Ci.nsIStrictTransportSecurityService); + +function processStsHeader(hostname, header, status) { + var maxAge = { value: 0 }; + var includeSubdomains = { value: false }; + var error = "no error"; + if (header != null) { + try { + var uri = Services.io.newURI("https://" + host.name, null, null); + gSTSService.processStsHeader(uri, header, maxAge, includeSubdomains); + } + catch (e) { + dump("ERROR: could not process header '" + header + "' from " + hostname + + ": " + e + "\n"); + error = e; + } + } + else { + if (status == 0) { + error = "could not connect to host"; + } else { + error = "did not receive HSTS header"; + } + } + + return { hostname: hostname, + maxAge: maxAge.value, + includeSubdomains: includeSubdomains.value, + error: error }; +} + +function RedirectStopper() {}; + +RedirectStopper.prototype = { + // nsIChannelEventSink + asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) { + throw Cr.NS_ERROR_ENTITY_CHANGED; + }, + + getInterface: function(iid) { + return this.QueryInterface(iid); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]) +}; + +function getHSTSStatus(host, resultList) { + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + var inResultList = false; + var uri = "https://" + host.name + "/"; + req.open("GET", uri, true); + req.channel.notificationCallbacks = new RedirectStopper(); + req.onreadystatechange = function(event) { + if (!inResultList && req.readyState == 4) { + inResultList = true; + var header = req.getResponseHeader("strict-transport-security"); + resultList.push(processStsHeader(host.name, header, req.status)); + } + }; + + try { + req.send(); + } + catch (e) { + dump("ERROR: exception making request to " + host.name + ": " + e + "\n"); + } +} + +function compareHSTSStatus(a, b) { + return (a.hostname > b.hostname ? 1 : (a.hostname < b.hostname ? -1 : 0)); +} + +function writeTo(string, fos) { + fos.write(string, string.length); +} + +function output(sortedStatuses) { + try { + var file = FileUtils.getFile("CurWorkD", [OUTPUT]); + var errorFile = FileUtils.getFile("CurWorkD", [ERROR_OUTPUT]); + var fos = FileUtils.openSafeFileOutputStream(file); + var eos = FileUtils.openSafeFileOutputStream(errorFile); + writeTo(PREFIX, fos); + for (var status of hstsStatuses) { + if (status.maxAge >= MINIMUM_REQUIRED_MAX_AGE) { + writeTo(" { \"" + status.hostname + "\", " + + (status.includeSubdomains ? "true" : "false") + " },\n", fos); + dump("INFO: " + status.hostname + " ON the preload list\n"); + } + else { + dump("INFO: " + status.hostname + " NOT ON the preload list\n"); + if (status.maxAge != 0) { + status.error = "max-age too low: " + status.maxAge; + } + writeTo(status.hostname + ": " + status.error + "\n", eos); + } + } + writeTo(POSTFIX, fos); + FileUtils.closeSafeFileOutputStream(fos); + FileUtils.closeSafeFileOutputStream(eos); + } + catch (e) { + dump("ERROR: problem writing output to '" + OUTPUT + "': " + e + "\n"); + } +} + +// The idea is the output list will be the same size as the input list +// when we've received all responses (or timed out). +// Since all events are processed on the main thread, and since event +// handlers are not preemptible, there shouldn't be any concurrency issues. +function waitForResponses(inputList, outputList) { + // From + var threadManager = Cc["@mozilla.org/thread-manager;1"] + .getService(Ci.nsIThreadManager); + var mainThread = threadManager.currentThread; + while (inputList.length != outputList.length) { + mainThread.processNextEvent(true); + } + while (mainThread.hasPendingEvents()) { + mainThread.processNextEvent(true); + } +} + +// **************************************************************************** +// This is where the action happens: +// disable the current preload list so it won't interfere with requests we make +Services.prefs.setBoolPref("network.stricttransportsecurity.preloadlist", false); +// download and parse the raw json file from the Chromium source +var rawdata = download(); +// get just the hosts with mode: "force-https" +var hosts = getHosts(rawdata); +// spin off a request to each host +var hstsStatuses = []; +for (var host of hosts) { + getHSTSStatus(host, hstsStatuses); +} +// wait for those responses to come back +waitForResponses(hosts, hstsStatuses); +// sort the hosts alphabetically +hstsStatuses.sort(compareHSTSStatus); +// write the results to a file (this is where we filter out hosts that we +// either couldn't connect to, didn't receive an HSTS header from, couldn't +// parse the header, or had a header with too short a max-age) +output(hstsStatuses); +// **************************************************************************** diff --git a/security/manager/tools/getHSTSPreloadList.py b/security/manager/tools/getHSTSPreloadList.py deleted file mode 100644 index 87d6921a1976..000000000000 --- a/security/manager/tools/getHSTSPreloadList.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/python - -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import sys, subprocess, json, argparse - -SOURCE = "https://src.chromium.org/viewvc/chrome/trunk/src/net/base/transport_security_state_static.json" -OUTPUT = "nsSTSPreloadList.inc" -PREFIX = """/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/*****************************************************************************/ -/* This is an automatically generated file. If you're not */ -/* nsStrictTransportSecurityService.cpp, you shouldn't be #including it. */ -/*****************************************************************************/ - -#include - -class nsSTSPreload -{ - public: - const char *mHost; - const bool mIncludeSubdomains; -}; - -static const nsSTSPreload kSTSPreloadList[] = { -""" -POSTFIX = """}; -""" - -def filterComments(stream): - lines = [] - for line in stream: - # each line still has '\n' at the end, so if find returns -1, - # the newline gets chopped off like we want - # (and otherwise, comments are filtered out like we want) - lines.append(line[0:line.find("//")]) - return "".join(lines) - -def readFile(source): - if source != "-": - f = open(source, 'r') - else: - f = sys.stdin - return filterComments(f) - -def download(source): - download = subprocess.Popen(["wget", "-O", "-", source], stdout = subprocess.PIPE, stderr = subprocess.PIPE) - contents = filterComments(download.stdout) - download.wait() - if download.returncode != 0: - raise Exception() - return contents - -def output(filename, jsonblob): - if filename != "-": - outstream = open(filename, 'w') - else: - outstream = sys.stdout - if not 'entries' in jsonblob: - raise Exception() - else: - outstream.write(PREFIX) - # use a dictionary to prevent duplicates - lines = {} - for entry in jsonblob['entries']: - if 'name' in entry and 'mode' in entry and entry['mode'] == "force-https": - line = " { \"" + entry['name'] + "\", " - if 'include_subdomains' in entry and entry['include_subdomains']: - line = line + "true },\n" - else: - line = line + "false },\n" - lines[line] = True - # The data must be sorted by domain name because we do a binary search to - # determine if a host is in the preload list. - keys = lines.keys() - keys.sort() - for line in keys: - outstream.write(line) - outstream.write(POSTFIX); - outstream.close() - -def main(): - parser = argparse.ArgumentParser(description="Download Chrome's STS preload list and format it for Firefox") - parser.add_argument("-s", "--source", default=SOURCE, help="Specify source for input list (can be a file, url, or '-' for stdin)") - parser.add_argument("-o", "--output", default=OUTPUT, help="Specify output file ('-' for stdout)") - args = parser.parse_args() - contents = None - try: - contents = readFile(args.source) - except: - pass - if not contents: - try: - contents = download(args.source) - except: - print >> sys.stderr, "Could not read source '%s'" % args.source - return 1 - try: - jsonblob = json.loads(contents) - except: - print >> sys.stderr, "Could not parse contents of file '%s'" % args.source - return 1 - try: - output(args.output, jsonblob) - except: - print >> sys.stderr, "Could not write to '%s'" % args.output - return 1 - return 0 - -if __name__ == "__main__": - sys.exit(main()) -- 2.11.4.GIT