bug 1564481 - reset HSTS/HPKP state to factory settings rather than storing knockout entries for preloaded sites r=jcj r=KevinJacobs

As originally implemented, nsISiteSecurityService.removeState allowed direct
access to remove HSTS state. It also provided the implementation for when the
browser encountered an HSTS header with "max-age=0". In bug 775370, it was
updated to store an entry that would override preloaded information when
processing such headers. However, this meant that the semantics of the direct
access API had changed. Preloaded information could be overridden if a user
invoked the "forget about this site" feature. This change fixes the public API
(and renames it to "resetState") so it actually behaves as its consumers expect.

Reviewers: jcj!, KevinJacobs!

Tags: #secure-revision

Bug #: 1564481

Differential Revision: https://phabricator.services.mozilla.com/D38108

--HG--
extra : rebase_source : 8dd5460d3fd3c0ce92746cc83fae220d6e2a83cf
extra : amend_source : 171ebb015e9f9ae775f0caa22e161d41970f3d51
This commit is contained in:
Dana Keeler 2019-07-11 13:48:28 -07:00
Родитель 18c49c3c24
Коммит 18e9f3ba80
16 изменённых файлов: 220 добавлений и 58 удалений

Просмотреть файл

@ -43,7 +43,7 @@ function test() {
Services.prefs.clearUserPref(kpkpEnforcementPref);
Services.prefs.clearUserPref(khpkpPinninEnablePref);
let uri = Services.io.newURI("https://" + kPinningDomain);
gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
gSSService.resetState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
});
whenNewTabLoaded(window, loadPinningPage);
}

Просмотреть файл

@ -14,7 +14,7 @@ add_task(async function test_star_redirect() {
let sss = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
sss.removeState(
sss.resetState(
Ci.nsISiteSecurityService.HEADER_HSTS,
NetUtil.newURI("http://example.com/"),
0

Просмотреть файл

@ -54,7 +54,7 @@ function startTest() {
.getService(Ci.nsIIOService);
for (let {url} of TEST_CASES) {
let uri = gIOService.newURI(url);
gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
gSSService.resetState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
}
});

Просмотреть файл

@ -48,7 +48,7 @@ function startTest()
.getService(Ci.nsIIOService);
let uri = gIOService.newURI(TEST_CASES[0].url);
gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0);
gSSService.resetState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0);
});
info("Test detection of HTTP Strict Transport Security.");

Просмотреть файл

@ -160,10 +160,13 @@ interface nsISiteSecurityService : nsISupports
[optional] out uint32_t aFailureResult);
/**
* Given a header type, removes state relating to that header of a host,
* Given a header type, resets state relating to that header of a host,
* including the includeSubdomains state that would affect subdomains.
* This essentially removes the state for the domain tree rooted at this
* host.
* host. If any preloaded information is present for that host, that
* information will then be used instead of any other previously existing
* state.
*
* @param aType the type of security state in question
* @param aURI the URI of the target host
* @param aFlags options for this request as defined in nsISocketProvider:
@ -175,10 +178,10 @@ interface nsISiteSecurityService : nsISupports
* happens).
*/
[implicit_jscontext, optional_argc, must_use]
void removeState(in uint32_t aType,
in nsIURI aURI,
in uint32_t aFlags,
[optional] in jsval aOriginAttributes);
void resetState(in uint32_t aType,
in nsIURI aURI,
in uint32_t aFlags,
[optional] in jsval aOriginAttributes);
/**
* Checks whether or not the URI's hostname has a given security state set.

Просмотреть файл

@ -557,11 +557,12 @@ nsresult nsSiteSecurityService::SetHSTSState(
SecurityPropertySource aSource, const OriginAttributes& aOriginAttributes) {
nsAutoCString hostname(aHost);
bool isPreload = (aSource == SourcePreload);
// If max-age is zero, that's an indication to immediately remove the
// security state, so here's a shortcut.
if (!maxage) {
return RemoveStateInternal(aType, hostname, flags, isPreload,
aOriginAttributes);
// If max-age is zero, the host is no longer considered HSTS. If the host was
// preloaded, we store an entry indicating that this host is not HSTS, causing
// the preloaded information to be ignored.
if (maxage == 0) {
return MarkHostAsNotHSTS(aType, hostname, flags, isPreload,
aOriginAttributes);
}
MOZ_ASSERT(
@ -608,28 +609,17 @@ nsresult nsSiteSecurityService::SetHSTSState(
return NS_OK;
}
nsresult nsSiteSecurityService::RemoveStateInternal(
uint32_t aType, nsIURI* aURI, uint32_t aFlags,
const OriginAttributes& aOriginAttributes) {
nsAutoCString hostname;
GetHost(aURI, hostname);
return RemoveStateInternal(aType, hostname, aFlags, false, aOriginAttributes);
}
nsresult nsSiteSecurityService::RemoveStateInternal(
// Helper function to mark a host as not HSTS. In the general case, we can just
// remove the HSTS state. However, for preloaded entries, we have to store an
// entry that indicates this host is not HSTS to prevent the implementation
// using the preloaded information.
nsresult nsSiteSecurityService::MarkHostAsNotHSTS(
uint32_t aType, const nsAutoCString& aHost, uint32_t aFlags,
bool aIsPreload, const OriginAttributes& aOriginAttributes) {
// Child processes are not allowed direct access to this.
if (!XRE_IsParentProcess()) {
MOZ_CRASH(
"Child process: no direct access to "
"nsISiteSecurityService::RemoveStateInternal");
// This only applies to HSTS.
if (aType != nsISiteSecurityService::HEADER_HSTS) {
return NS_ERROR_INVALID_ARG;
}
// Only HSTS is supported at the moment.
NS_ENSURE_TRUE(aType == nsISiteSecurityService::HEADER_HSTS ||
aType == nsISiteSecurityService::HEADER_HPKP,
NS_ERROR_NOT_IMPLEMENTED);
if (aIsPreload && aOriginAttributes != OriginAttributes()) {
return NS_ERROR_INVALID_ARG;
}
@ -638,7 +628,6 @@ nsresult nsSiteSecurityService::RemoveStateInternal(
mozilla::DataStorageType storageType = isPrivate
? mozilla::DataStorage_Private
: mozilla::DataStorage_Persistent;
// If this host is in the preload list, we have to store a knockout entry.
nsAutoCString storageKey;
SetStorageKey(aHost, aType, aOriginAttributes, storageKey);
@ -675,10 +664,18 @@ nsresult nsSiteSecurityService::RemoveStateInternal(
}
NS_IMETHODIMP
nsSiteSecurityService::RemoveState(uint32_t aType, nsIURI* aURI,
uint32_t aFlags,
JS::HandleValue aOriginAttributes,
JSContext* aCx, uint8_t aArgc) {
nsSiteSecurityService::ResetState(uint32_t aType, nsIURI* aURI, uint32_t aFlags,
JS::HandleValue aOriginAttributes,
JSContext* aCx, uint8_t aArgc) {
if (!XRE_IsParentProcess()) {
MOZ_CRASH(
"Child process: no direct access to "
"nsISiteSecurityService::ResetState");
}
if (!aURI) {
return NS_ERROR_INVALID_ARG;
}
OriginAttributes originAttributes;
if (aArgc > 0) {
// OriginAttributes were passed in.
@ -687,7 +684,39 @@ nsSiteSecurityService::RemoveState(uint32_t aType, nsIURI* aURI,
return NS_ERROR_INVALID_ARG;
}
}
return RemoveStateInternal(aType, aURI, aFlags, originAttributes);
return ResetStateInternal(aType, aURI, aFlags, originAttributes);
}
// Helper function to reset stored state of the given type for the host
// identified by the given URI. If there is preloaded information for the host,
// that information will be used for future queries. C.f. MarkHostAsNotHSTS,
// which will store a knockout entry for preloaded HSTS hosts that have sent a
// header with max-age=0 (meaning preloaded information will then not be used
// for that host).
nsresult nsSiteSecurityService::ResetStateInternal(
uint32_t aType, nsIURI* aURI, uint32_t aFlags,
const OriginAttributes& aOriginAttributes) {
if (!aURI) {
return NS_ERROR_INVALID_ARG;
}
if (aType != nsISiteSecurityService::HEADER_HSTS &&
aType != nsISiteSecurityService::HEADER_HPKP) {
return NS_ERROR_INVALID_ARG;
}
nsAutoCString hostname;
nsresult rv = GetHost(aURI, hostname);
if (NS_FAILED(rv)) {
return rv;
}
nsAutoCString storageKey;
SetStorageKey(hostname, aType, aOriginAttributes, storageKey);
bool isPrivate = aFlags & nsISocketProvider::NO_PERMANENT_STORAGE;
mozilla::DataStorageType storageType = isPrivate
? mozilla::DataStorage_Private
: mozilla::DataStorage_Persistent;
mSiteStateStorage->Remove(storageKey, storageType);
return NS_OK;
}
static bool HostIsIPAddress(const nsCString& hostname) {
@ -1062,9 +1091,12 @@ nsresult nsSiteSecurityService::ProcessPKPHeader(
return NS_ERROR_FAILURE;
}
// if maxAge == 0 we must delete all state, for now no hole-punching
// If maxAge == 0, we remove dynamic HPKP state for this host. Due to
// architectural constraints, if this host was preloaded, any future lookups
// will use the preloaded state (i.e. we can't store a "this host is not HPKP"
// entry like we can for HSTS).
if (maxAge == 0) {
return RemoveStateInternal(aType, aSourceURI, aFlags, aOriginAttributes);
return ResetStateInternal(aType, aSourceURI, aFlags, aOriginAttributes);
}
// clamp maxAge to the maximum set by pref

Просмотреть файл

@ -191,11 +191,11 @@ class nsSiteSecurityService : public nsISiteSecurityService,
nsresult SetHPKPState(const char* aHost, SiteHPKPState& entry, uint32_t flags,
bool aIsPreload,
const OriginAttributes& aOriginAttributes);
nsresult RemoveStateInternal(uint32_t aType, nsIURI* aURI, uint32_t aFlags,
const OriginAttributes& aOriginAttributes);
nsresult RemoveStateInternal(uint32_t aType, const nsAutoCString& aHost,
uint32_t aFlags, bool aIsPreload,
const OriginAttributes& aOriginAttributes);
nsresult MarkHostAsNotHSTS(uint32_t aType, const nsAutoCString& aHost,
uint32_t aFlags, bool aIsPreload,
const OriginAttributes& aOriginAttributes);
nsresult ResetStateInternal(uint32_t aType, nsIURI* aURI, uint32_t aFlags,
const OriginAttributes& aOriginAttributes);
bool HostHasHSTSEntry(const nsAutoCString& aHost,
bool aRequireIncludeSubdomains, uint32_t aFlags,
const OriginAttributes& aOriginAttributes,

Просмотреть файл

@ -93,7 +93,7 @@ function test() {
aWin.close();
});
uri = Services.io.newURI("http://localhost");
gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0);
gSSService.resetState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0);
});
// test first when on private mode

Просмотреть файл

@ -69,7 +69,7 @@ function add_tests() {
);
// Clear accumulated state.
ssservice.removeState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
ssservice.resetState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
Services.prefs.clearUserPref(
"security.cert_pinning.process_headers_from_non_builtin_roots"
);

Просмотреть файл

@ -55,7 +55,7 @@ function checkPassValidPin(pinValue, settingPin, expectedMaxAge) {
// setup preconditions for the test, if setting ensure there is no previous
// state, if removing ensure there is a valid pin in place.
if (settingPin) {
gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
gSSService.resetState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
} else {
// add a known valid pin!
let validPinValue = "max-age=5000;" + VALID_PIN1 + BACKUP_PIN1;

Просмотреть файл

@ -83,7 +83,7 @@ function doTest(originAttributes1, originAttributes2, shouldShare) {
if (!shouldShare) {
// Remove originAttributes2 from the storage.
sss.removeState(type, uri, 0, originAttributes2);
sss.resetState(type, uri, 0, originAttributes2);
ok(
sss.isSecureURI(type, uri, 0, originAttributes1),
"URI should still be secure given original origin attributes"
@ -91,7 +91,7 @@ function doTest(originAttributes1, originAttributes2, shouldShare) {
}
// Remove originAttributes1 from the storage.
sss.removeState(type, uri, 0, originAttributes1);
sss.resetState(type, uri, 0, originAttributes1);
ok(
!sss.isSecureURI(type, uri, 0, originAttributes1),
"URI should be not be secure after removeState"
@ -161,7 +161,7 @@ function testInvalidOriginAttributes(originAttributes) {
originAttributes
),
() => sss.isSecureURI(type, uri, 0, originAttributes),
() => sss.removeState(type, uri, 0, originAttributes),
() => sss.resetState(type, uri, 0, originAttributes),
];
for (let callback of callbacks) {

Просмотреть файл

@ -0,0 +1,126 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
// 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/.
"use strict";
// Tests that resetting HSTS/HPKP state in the way the "forget about this site"
// functionality does works as expected for preloaded and non-preloaded sites.
do_get_profile();
var gCertDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
const ROOT_CERT = addCertFromFile(gCertDB, "bad_certs/test-ca.pem", "CTu,,");
var gSSService = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
function run_test() {
Services.prefs.setBoolPref(
"security.cert_pinning.process_headers_from_non_builtin_roots",
true
);
test_removeState(Ci.nsISiteSecurityService.HEADER_HSTS, 0);
test_removeState(
Ci.nsISiteSecurityService.HEADER_HSTS,
Ci.nsISocketProvider.NO_PERMANENT_STORAGE
);
test_removeState(Ci.nsISiteSecurityService.HEADER_HPKP, 0);
test_removeState(
Ci.nsISiteSecurityService.HEADER_HPKP,
Ci.nsISocketProvider.NO_PERMANENT_STORAGE
);
}
function test_removeState(type, flags) {
info(`running test_removeState(type=${type}, flags=${flags})`);
const NON_ISSUED_KEY_HASH = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
const PINNING_ROOT_KEY_HASH = "VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=";
const PINNING_HEADERS = `pin-sha256="${NON_ISSUED_KEY_HASH}"; pin-sha256="${PINNING_ROOT_KEY_HASH}"`;
let headerAddendum =
type == Ci.nsISiteSecurityService.HEADER_HPKP ? PINNING_HEADERS : "";
let secInfo = new FakeTransportSecurityInfo(
constructCertFromFile("bad_certs/default-ee.pem")
);
// Simulate visiting a non-preloaded site by processing an HSTS or HPKP header
// (depending on which type we were given), check that the HSTS/HPKP bit gets
// set, simulate "forget about this site" (call removeState), and then check
// that the HSTS/HPKP bit isn't set.
let notPreloadedURI = Services.io.newURI("https://not-preloaded.example.com");
ok(!gSSService.isSecureURI(type, notPreloadedURI, flags));
gSSService.processHeader(
type,
notPreloadedURI,
"max-age=1000;" + headerAddendum,
secInfo,
flags,
Ci.nsISiteSecurityService.SOURCE_ORGANIC_REQUEST
);
ok(gSSService.isSecureURI(type, notPreloadedURI, flags));
gSSService.resetState(type, notPreloadedURI, flags);
ok(!gSSService.isSecureURI(type, notPreloadedURI, flags));
// Simulate visiting a non-preloaded site that unsets HSTS/HPKP by processing
// an HSTS/HPKP header with "max-age=0", check that the HSTS/HPKP bit isn't
// set, simulate "forget about this site" (call removeState), and then check
// that the HSTS/HPKP bit isn't set.
gSSService.processHeader(
type,
notPreloadedURI,
"max-age=0;" + headerAddendum,
secInfo,
flags,
Ci.nsISiteSecurityService.SOURCE_ORGANIC_REQUEST
);
ok(!gSSService.isSecureURI(type, notPreloadedURI, flags));
gSSService.resetState(type, notPreloadedURI, flags);
ok(!gSSService.isSecureURI(type, notPreloadedURI, flags));
// Simulate visiting a preloaded site by processing an HSTS/HPKP header, check
// that the HSTS/HPKP bit is still set, simulate "forget about this site"
// (call removeState), and then check that the HSTS/HPKP bit is still set.
let preloadedHost =
type == Ci.nsISiteSecurityService.HEADER_HPKP
? "include-subdomains.pinning.example.com"
: "includesubdomains.preloaded.test";
let preloadedURI = Services.io.newURI(`https://${preloadedHost}`);
ok(gSSService.isSecureURI(type, preloadedURI, flags));
gSSService.processHeader(
type,
preloadedURI,
"max-age=1000;" + headerAddendum,
secInfo,
flags,
Ci.nsISiteSecurityService.SOURCE_ORGANIC_REQUEST
);
ok(gSSService.isSecureURI(type, preloadedURI, flags));
gSSService.resetState(type, preloadedURI, flags);
ok(gSSService.isSecureURI(type, preloadedURI, flags));
// Simulate visiting a preloaded site that unsets HSTS/HPKP by processing an
// HSTS/HPKP header with "max-age=0", check that the HSTS/HPKP bit is what we
// expect (see below), simulate "forget about this site" (call removeState),
// and then check that the HSTS/HPKP bit is set.
gSSService.processHeader(
type,
preloadedURI,
"max-age=0;" + headerAddendum,
secInfo,
flags,
Ci.nsISiteSecurityService.SOURCE_ORGANIC_REQUEST
);
// Due to architectural constraints, encountering a "max-age=0" header for a
// preloaded HPKP site does not mark that site as not HPKP (whereas with HSTS,
// it does).
if (type == Ci.nsISiteSecurityService.HEADER_HPKP) {
ok(gSSService.isSecureURI(type, preloadedURI, flags));
} else {
ok(!gSSService.isSecureURI(type, preloadedURI, flags));
}
gSSService.resetState(type, preloadedURI, flags);
ok(gSSService.isSecureURI(type, preloadedURI, flags));
}

Просмотреть файл

@ -29,7 +29,7 @@ function run_test() {
ok(SSService.isSecureURI(Ci.nsISiteSecurityService.HEADER_HSTS, uri1, 0));
ok(SSService.isSecureURI(Ci.nsISiteSecurityService.HEADER_HSTS, uri2, 0));
SSService.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0);
SSService.resetState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0);
ok(!SSService.isSecureURI(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0));
ok(!SSService.isSecureURI(Ci.nsISiteSecurityService.HEADER_HSTS, uri1, 0));
ok(!SSService.isSecureURI(Ci.nsISiteSecurityService.HEADER_HSTS, uri2, 0));

Просмотреть файл

@ -199,6 +199,7 @@ skip-if = toolkit == 'android'
[test_sss_readstate_empty.js]
[test_sss_readstate_garbage.js]
[test_sss_readstate_huge.js]
[test_sss_resetState.js]
[test_sss_savestate.js]
[test_sss_sanitizeOnShutdown.js]
firefox-appdir = browser

Просмотреть файл

@ -705,7 +705,7 @@ class SpecialPowersAPIParent extends JSWindowActorParent {
let sss = Cc["@mozilla.org/ssservice;1"].getService(
Ci.nsISiteSecurityService
);
sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, flags);
sss.resetState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, flags);
return undefined;
}

Просмотреть файл

@ -920,14 +920,14 @@ const SecuritySettingsCleaner = {
Ci.nsISiteSecurityService.HEADER_HSTS,
Ci.nsISiteSecurityService.HEADER_HPKP,
]) {
// Also remove HSTS/HPKP/OMS information for subdomains by enumerating
// Also remove HSTS/HPKP information for subdomains by enumerating
// the information in the site security service.
for (let entry of sss.enumerate(type)) {
let hostname = entry.hostname;
if (Services.eTLD.hasRootDomain(hostname, aHost)) {
// This uri is used as a key to remove the state.
// This uri is used as a key to reset the state.
let uri = Services.io.newURI("https://" + hostname);
sss.removeState(type, uri, 0, entry.originAttributes);
sss.resetState(type, uri, 0, entry.originAttributes);
}
}
}