diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /security/manager/ssl/nsSiteSecurityService.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'security/manager/ssl/nsSiteSecurityService.cpp')
-rw-r--r-- | security/manager/ssl/nsSiteSecurityService.cpp | 830 |
1 files changed, 830 insertions, 0 deletions
diff --git a/security/manager/ssl/nsSiteSecurityService.cpp b/security/manager/ssl/nsSiteSecurityService.cpp new file mode 100644 index 0000000000..66cc8981b7 --- /dev/null +++ b/security/manager/ssl/nsSiteSecurityService.cpp @@ -0,0 +1,830 @@ +/* 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/. */ + +#include "nsSiteSecurityService.h" + +#include "PublicKeyPinningService.h" +#include "mozilla/Assertions.h" +#include "mozilla/Base64.h" +#include "mozilla/LinkedList.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/dom/PContent.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsArrayEnumerator.h" +#include "nsCOMArray.h" +#include "nsIScriptSecurityManager.h" +#include "nsISocketProvider.h" +#include "nsIURI.h" +#include "nsNSSComponent.h" +#include "nsNetUtil.h" +#include "nsPromiseFlatString.h" +#include "nsReadableUtils.h" +#include "nsSecurityHeaderParser.h" +#include "nsThreadUtils.h" +#include "nsVariant.h" +#include "nsXULAppAPI.h" +#include "prnetdb.h" + +// A note about the preload list: +// When a site specifically disables HSTS by sending a header with +// 'max-age: 0', we keep a "knockout" value that means "we have no information +// regarding the HSTS state of this host" (any ancestor of "this host" can still +// influence its HSTS status via include subdomains, however). +// This prevents the preload list from overriding the site's current +// desired HSTS status. +#include "nsSTSPreloadListGenerated.inc" + +using namespace mozilla; +using namespace mozilla::psm; + +static LazyLogModule gSSSLog("nsSSService"); + +#define SSSLOG(args) MOZ_LOG(gSSSLog, mozilla::LogLevel::Debug, args) + +static const nsLiteralCString kHSTSKeySuffix = ":HSTS"_ns; + +//////////////////////////////////////////////////////////////////////////////// + +namespace { + +class SSSTokenizer final : public Tokenizer { + public: + explicit SSSTokenizer(const nsACString& source) : Tokenizer(source) {} + + [[nodiscard]] bool ReadBool(/*out*/ bool& value) { + uint8_t rawValue; + if (!ReadInteger(&rawValue)) { + return false; + } + + if (rawValue != 0 && rawValue != 1) { + return false; + } + + value = (rawValue == 1); + return true; + } + + [[nodiscard]] bool ReadState(/*out*/ SecurityPropertyState& state) { + uint32_t rawValue; + if (!ReadInteger(&rawValue)) { + return false; + } + + state = static_cast<SecurityPropertyState>(rawValue); + switch (state) { + case SecurityPropertyKnockout: + case SecurityPropertySet: + case SecurityPropertyUnset: + break; + default: + return false; + } + + return true; + } +}; + +// Parses a state string like "1500918564034,1,1" into its constituent parts. +bool ParseHSTSState(const nsCString& stateString, + /*out*/ PRTime& expireTime, + /*out*/ SecurityPropertyState& state, + /*out*/ bool& includeSubdomains) { + SSSTokenizer tokenizer(stateString); + SSSLOG(("Parsing state from %s", stateString.get())); + + if (!tokenizer.ReadInteger(&expireTime)) { + return false; + } + + if (!tokenizer.CheckChar(',')) { + return false; + } + + if (!tokenizer.ReadState(state)) { + return false; + } + + if (!tokenizer.CheckChar(',')) { + return false; + } + + if (!tokenizer.ReadBool(includeSubdomains)) { + return false; + } + + if (tokenizer.CheckChar(',')) { + // Read now-unused "source" field. + uint32_t unused; + if (!tokenizer.ReadInteger(&unused)) { + return false; + } + } + + return tokenizer.CheckEOF(); +} + +} // namespace + +SiteHSTSState::SiteHSTSState(const nsCString& aHost, + const OriginAttributes& aOriginAttributes, + const nsCString& aStateString) + : mHostname(aHost), + mOriginAttributes(aOriginAttributes), + mHSTSExpireTime(0), + mHSTSState(SecurityPropertyUnset), + mHSTSIncludeSubdomains(false) { + bool valid = ParseHSTSState(aStateString, mHSTSExpireTime, mHSTSState, + mHSTSIncludeSubdomains); + if (!valid) { + SSSLOG(("%s is not a valid SiteHSTSState", aStateString.get())); + mHSTSExpireTime = 0; + mHSTSState = SecurityPropertyUnset; + mHSTSIncludeSubdomains = false; + } +} + +SiteHSTSState::SiteHSTSState(const nsCString& aHost, + const OriginAttributes& aOriginAttributes, + PRTime aHSTSExpireTime, + SecurityPropertyState aHSTSState, + bool aHSTSIncludeSubdomains) + + : mHostname(aHost), + mOriginAttributes(aOriginAttributes), + mHSTSExpireTime(aHSTSExpireTime), + mHSTSState(aHSTSState), + mHSTSIncludeSubdomains(aHSTSIncludeSubdomains) {} + +void SiteHSTSState::ToString(nsCString& aString) { + aString.Truncate(); + aString.AppendInt(mHSTSExpireTime); + aString.Append(','); + aString.AppendInt(mHSTSState); + aString.Append(','); + aString.AppendInt(static_cast<uint32_t>(mHSTSIncludeSubdomains)); +} + +nsSiteSecurityService::nsSiteSecurityService() + : mUsePreloadList(true), mPreloadListTimeOffset(0), mDafsa(kDafsa) {} + +nsSiteSecurityService::~nsSiteSecurityService() = default; + +NS_IMPL_ISUPPORTS(nsSiteSecurityService, nsIObserver, nsISiteSecurityService) + +nsresult nsSiteSecurityService::Init() { + // Don't access Preferences off the main thread. + if (!NS_IsMainThread()) { + MOZ_ASSERT_UNREACHABLE("nsSiteSecurityService initialized off main thread"); + return NS_ERROR_NOT_SAME_THREAD; + } + + mUsePreloadList = mozilla::Preferences::GetBool( + "network.stricttransportsecurity.preloadlist", true); + mozilla::Preferences::AddStrongObserver( + this, "network.stricttransportsecurity.preloadlist"); + mPreloadListTimeOffset = + mozilla::Preferences::GetInt("test.currentTimeOffsetSeconds", 0); + mozilla::Preferences::AddStrongObserver(this, + "test.currentTimeOffsetSeconds"); + mSiteStateStorage = + mozilla::DataStorage::Get(DataStorageClass::SiteSecurityServiceState); + nsresult rv = mSiteStateStorage->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult nsSiteSecurityService::GetHost(nsIURI* aURI, nsACString& aResult) { + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(aURI); + if (!innerURI) { + return NS_ERROR_FAILURE; + } + + nsAutoCString host; + nsresult rv = innerURI->GetAsciiHost(host); + if (NS_FAILED(rv)) { + return rv; + } + + aResult.Assign(PublicKeyPinningService::CanonicalizeHostname(host.get())); + if (aResult.IsEmpty()) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +static void SetStorageKey(const nsACString& hostname, + const OriginAttributes& aOriginAttributes, + /*out*/ nsAutoCString& storageKey) { + storageKey = hostname; + + // Don't isolate by userContextId. + OriginAttributes originAttributesNoUserContext = aOriginAttributes; + originAttributesNoUserContext.mUserContextId = + nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID; + nsAutoCString originAttributesSuffix; + originAttributesNoUserContext.CreateSuffix(originAttributesSuffix); + storageKey.Append(originAttributesSuffix); + storageKey.Append(kHSTSKeySuffix); +} + +// Expire times are in millis. Since Headers max-age is in seconds, and +// PR_Now() is in micros, normalize the units at milliseconds. +static int64_t ExpireTimeFromMaxAge(uint64_t maxAge) { + return (PR_Now() / PR_USEC_PER_MSEC) + ((int64_t)maxAge * PR_MSEC_PER_SEC); +} + +inline uint64_t AbsoluteDifference(int64_t a, int64_t b) { + if (a <= b) { + return b - a; + } + return a - b; +} + +const uint64_t sOneDayInMilliseconds = 24 * 60 * 60 * 1000; + +nsresult nsSiteSecurityService::SetHSTSState( + const char* aHost, int64_t maxage, bool includeSubdomains, + SecurityPropertyState aHSTSState, + const OriginAttributes& aOriginAttributes) { + nsAutoCString hostname(aHost); + // 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(hostname, aOriginAttributes); + } + + MOZ_ASSERT(aHSTSState == SecurityPropertySet, + "HSTS State must be SecurityPropertySet"); + + int64_t expiretime = ExpireTimeFromMaxAge(maxage); + SiteHSTSState siteState(hostname, aOriginAttributes, expiretime, aHSTSState, + includeSubdomains); + nsAutoCString stateString; + siteState.ToString(stateString); + SSSLOG(("SSS: setting state for %s", hostname.get())); + bool isPrivate = aOriginAttributes.mPrivateBrowsingId > 0; + mozilla::DataStorageType storageType = isPrivate + ? mozilla::DataStorage_Private + : mozilla::DataStorage_Persistent; + nsAutoCString storageKey; + SetStorageKey(hostname, aOriginAttributes, storageKey); + SSSLOG(("SSS: storing HSTS site entry for %s", hostname.get())); + nsCString value = mSiteStateStorage->Get(storageKey, storageType); + SiteHSTSState curSiteState(hostname, aOriginAttributes, value); + // Only update the backing storage if the currently-stored state is + // different. In the case of expiration time, "different" means "is different + // by more than a day". + if (curSiteState.mHSTSState != siteState.mHSTSState || + curSiteState.mHSTSIncludeSubdomains != siteState.mHSTSIncludeSubdomains || + AbsoluteDifference(curSiteState.mHSTSExpireTime, + siteState.mHSTSExpireTime) > sOneDayInMilliseconds) { + nsresult rv = mSiteStateStorage->Put(storageKey, stateString, storageType); + if (NS_FAILED(rv)) { + return rv; + } + } + + return NS_OK; +} + +// 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( + const nsAutoCString& aHost, const OriginAttributes& aOriginAttributes) { + bool isPrivate = aOriginAttributes.mPrivateBrowsingId > 0; + mozilla::DataStorageType storageType = isPrivate + ? mozilla::DataStorage_Private + : mozilla::DataStorage_Persistent; + nsAutoCString storageKey; + SetStorageKey(aHost, aOriginAttributes, storageKey); + + if (GetPreloadStatus(aHost)) { + SSSLOG(("SSS: storing knockout entry for %s", aHost.get())); + SiteHSTSState siteState(aHost, aOriginAttributes, 0, + SecurityPropertyKnockout, false); + nsAutoCString stateString; + siteState.ToString(stateString); + nsresult rv = mSiteStateStorage->Put(storageKey, stateString, storageType); + NS_ENSURE_SUCCESS(rv, rv); + } else { + SSSLOG(("SSS: removing entry for %s", aHost.get())); + mSiteStateStorage->Remove(storageKey, storageType); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsSiteSecurityService::ResetState(nsIURI* aURI, + JS::Handle<JS::Value> aOriginAttributes, + nsISiteSecurityService::ResetStateBy aScope, + JSContext* aCx, uint8_t aArgc) { + if (!aURI) { + return NS_ERROR_INVALID_ARG; + } + + OriginAttributes originAttributes; + if (aArgc > 0) { + // OriginAttributes were passed in. + if (!aOriginAttributes.isObject() || + !originAttributes.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + } + nsISiteSecurityService::ResetStateBy scope = + nsISiteSecurityService::ResetStateBy::ExactDomain; + if (aArgc > 1) { + // ResetStateBy scope was passed in + scope = aScope; + } + + return ResetStateInternal(aURI, originAttributes, scope); +} + +// 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( + nsIURI* aURI, const OriginAttributes& aOriginAttributes, + nsISiteSecurityService::ResetStateBy aScope) { + if (!aURI) { + return NS_ERROR_INVALID_ARG; + } + nsAutoCString hostname; + nsresult rv = GetHost(aURI, hostname); + if (NS_FAILED(rv)) { + return rv; + } + + if (aScope == ResetStateBy::ExactDomain) { + ResetStateForExactDomain(hostname, aOriginAttributes); + return NS_OK; + } + + nsTArray<DataStorageItem> items; + mSiteStateStorage->GetAll(&items); + for (const auto& item : items) { + if (!StringEndsWith(item.key, kHSTSKeySuffix)) { + // The key does not end with correct suffix, so is not the type we want. + continue; + } + nsCString origin( + StringHead(item.key, item.key.Length() - kHSTSKeySuffix.Length())); + nsAutoCString itemHostname; + OriginAttributes itemOriginAttributes; + if (!itemOriginAttributes.PopulateFromOrigin(origin, itemHostname)) { + return NS_ERROR_FAILURE; + } + bool hasRootDomain = false; + nsresult rv = net::HasRootDomain(itemHostname, hostname, &hasRootDomain); + if (NS_FAILED(rv)) { + return rv; + } + if (hasRootDomain) { + ResetStateForExactDomain(itemHostname, itemOriginAttributes); + } else if (aScope == ResetStateBy::BaseDomain) { + mozilla::dom::PartitionKeyPatternDictionary partitionKeyPattern; + partitionKeyPattern.mBaseDomain.Construct( + NS_ConvertUTF8toUTF16(hostname)); + OriginAttributesPattern originAttributesPattern; + originAttributesPattern.mPartitionKeyPattern.Construct( + partitionKeyPattern); + if (originAttributesPattern.Matches(itemOriginAttributes)) { + ResetStateForExactDomain(itemHostname, itemOriginAttributes); + } + } + } + return NS_OK; +} + +void nsSiteSecurityService::ResetStateForExactDomain( + const nsCString& aHostname, const OriginAttributes& aOriginAttributes) { + nsAutoCString storageKey; + SetStorageKey(aHostname, aOriginAttributes, storageKey); + bool isPrivate = aOriginAttributes.mPrivateBrowsingId > 0; + mozilla::DataStorageType storageType = isPrivate + ? mozilla::DataStorage_Private + : mozilla::DataStorage_Persistent; + mSiteStateStorage->Remove(storageKey, storageType); +} + +bool nsSiteSecurityService::HostIsIPAddress(const nsCString& hostname) { + PRNetAddr hostAddr; + PRErrorCode prv = PR_StringToNetAddr(hostname.get(), &hostAddr); + return (prv == PR_SUCCESS); +} + +NS_IMETHODIMP +nsSiteSecurityService::ProcessHeaderScriptable( + nsIURI* aSourceURI, const nsACString& aHeader, + JS::Handle<JS::Value> aOriginAttributes, uint64_t* aMaxAge, + bool* aIncludeSubdomains, uint32_t* aFailureResult, JSContext* aCx, + uint8_t aArgc) { + OriginAttributes originAttributes; + if (aArgc > 0) { + if (!aOriginAttributes.isObject() || + !originAttributes.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + } + return ProcessHeader(aSourceURI, aHeader, originAttributes, aMaxAge, + aIncludeSubdomains, aFailureResult); +} + +NS_IMETHODIMP +nsSiteSecurityService::ProcessHeader(nsIURI* aSourceURI, + const nsACString& aHeader, + const OriginAttributes& aOriginAttributes, + uint64_t* aMaxAge, + bool* aIncludeSubdomains, + uint32_t* aFailureResult) { + if (aFailureResult) { + *aFailureResult = nsISiteSecurityService::ERROR_UNKNOWN; + } + return ProcessHeaderInternal(aSourceURI, PromiseFlatCString(aHeader), + aOriginAttributes, aMaxAge, aIncludeSubdomains, + aFailureResult); +} + +nsresult nsSiteSecurityService::ProcessHeaderInternal( + nsIURI* aSourceURI, const nsCString& aHeader, + const OriginAttributes& aOriginAttributes, uint64_t* aMaxAge, + bool* aIncludeSubdomains, uint32_t* aFailureResult) { + if (aFailureResult) { + *aFailureResult = nsISiteSecurityService::ERROR_UNKNOWN; + } + if (aMaxAge != nullptr) { + *aMaxAge = 0; + } + + if (aIncludeSubdomains != nullptr) { + *aIncludeSubdomains = false; + } + + nsAutoCString host; + nsresult rv = GetHost(aSourceURI, host); + NS_ENSURE_SUCCESS(rv, rv); + if (HostIsIPAddress(host)) { + /* Don't process headers if a site is accessed by IP address. */ + return NS_OK; + } + + return ProcessSTSHeader(aSourceURI, aHeader, aOriginAttributes, aMaxAge, + aIncludeSubdomains, aFailureResult); +} + +static uint32_t ParseSSSHeaders(const nsCString& aHeader, + bool& foundIncludeSubdomains, bool& foundMaxAge, + bool& foundUnrecognizedDirective, + uint64_t& maxAge) { + // "Strict-Transport-Security" ":" OWS + // STS-d *( OWS ";" OWS STS-d OWS) + // + // ; STS directive + // STS-d = maxAge / includeSubDomains + // + // maxAge = "max-age" "=" delta-seconds v-ext + // + // includeSubDomains = [ "includeSubDomains" ] + // + // The order of the directives is not significant. + // All directives must appear only once. + // Directive names are case-insensitive. + // The entire header is invalid if a directive not conforming to the + // syntax is encountered. + // Unrecognized directives (that are otherwise syntactically valid) are + // ignored, and the rest of the header is parsed as normal. + + constexpr auto max_age_var = "max-age"_ns; + constexpr auto include_subd_var = "includesubdomains"_ns; + + nsSecurityHeaderParser parser(aHeader); + nsresult rv = parser.Parse(); + if (NS_FAILED(rv)) { + SSSLOG(("SSS: could not parse header")); + return nsISiteSecurityService::ERROR_COULD_NOT_PARSE_HEADER; + } + mozilla::LinkedList<nsSecurityHeaderDirective>* directives = + parser.GetDirectives(); + + for (nsSecurityHeaderDirective* directive = directives->getFirst(); + directive != nullptr; directive = directive->getNext()) { + SSSLOG(("SSS: found directive %s\n", directive->mName.get())); + if (directive->mName.EqualsIgnoreCase(max_age_var)) { + if (foundMaxAge) { + SSSLOG(("SSS: found two max-age directives")); + return nsISiteSecurityService::ERROR_MULTIPLE_MAX_AGES; + } + + SSSLOG(("SSS: found max-age directive")); + foundMaxAge = true; + + Tokenizer tokenizer(directive->mValue); + if (!tokenizer.ReadInteger(&maxAge)) { + SSSLOG(("SSS: could not parse delta-seconds")); + return nsISiteSecurityService::ERROR_INVALID_MAX_AGE; + } + + if (!tokenizer.CheckEOF()) { + SSSLOG(("SSS: invalid value for max-age directive")); + return nsISiteSecurityService::ERROR_INVALID_MAX_AGE; + } + + SSSLOG(("SSS: parsed delta-seconds: %" PRIu64, maxAge)); + } else if (directive->mName.EqualsIgnoreCase(include_subd_var)) { + if (foundIncludeSubdomains) { + SSSLOG(("SSS: found two includeSubdomains directives")); + return nsISiteSecurityService::ERROR_MULTIPLE_INCLUDE_SUBDOMAINS; + } + + SSSLOG(("SSS: found includeSubdomains directive")); + foundIncludeSubdomains = true; + + if (directive->mValue.Length() != 0) { + SSSLOG(("SSS: includeSubdomains directive unexpectedly had value '%s'", + directive->mValue.get())); + return nsISiteSecurityService::ERROR_INVALID_INCLUDE_SUBDOMAINS; + } + } else { + SSSLOG(("SSS: ignoring unrecognized directive '%s'", + directive->mName.get())); + foundUnrecognizedDirective = true; + } + } + return nsISiteSecurityService::Success; +} + +// 100 years is wildly longer than anyone will ever need. +const uint64_t sMaxMaxAgeInSeconds = UINT64_C(60 * 60 * 24 * 365 * 100); + +nsresult nsSiteSecurityService::ProcessSTSHeader( + nsIURI* aSourceURI, const nsCString& aHeader, + const OriginAttributes& aOriginAttributes, uint64_t* aMaxAge, + bool* aIncludeSubdomains, uint32_t* aFailureResult) { + if (aFailureResult) { + *aFailureResult = nsISiteSecurityService::ERROR_UNKNOWN; + } + SSSLOG(("SSS: processing HSTS header '%s'", aHeader.get())); + + bool foundMaxAge = false; + bool foundIncludeSubdomains = false; + bool foundUnrecognizedDirective = false; + uint64_t maxAge = 0; + + uint32_t sssrv = ParseSSSHeaders(aHeader, foundIncludeSubdomains, foundMaxAge, + foundUnrecognizedDirective, maxAge); + if (sssrv != nsISiteSecurityService::Success) { + if (aFailureResult) { + *aFailureResult = sssrv; + } + return NS_ERROR_FAILURE; + } + + // after processing all the directives, make sure we came across max-age + // somewhere. + if (!foundMaxAge) { + SSSLOG(("SSS: did not encounter required max-age directive")); + if (aFailureResult) { + *aFailureResult = nsISiteSecurityService::ERROR_NO_MAX_AGE; + } + return NS_ERROR_FAILURE; + } + + // Cap the specified max-age. + if (maxAge > sMaxMaxAgeInSeconds) { + maxAge = sMaxMaxAgeInSeconds; + } + + nsAutoCString hostname; + nsresult rv = GetHost(aSourceURI, hostname); + NS_ENSURE_SUCCESS(rv, rv); + + // record the successfully parsed header data. + rv = SetHSTSState(hostname.get(), maxAge, foundIncludeSubdomains, + SecurityPropertySet, aOriginAttributes); + if (NS_FAILED(rv)) { + SSSLOG(("SSS: failed to set STS state")); + if (aFailureResult) { + *aFailureResult = nsISiteSecurityService::ERROR_COULD_NOT_SAVE_STATE; + } + return rv; + } + + if (aMaxAge != nullptr) { + *aMaxAge = maxAge; + } + + if (aIncludeSubdomains != nullptr) { + *aIncludeSubdomains = foundIncludeSubdomains; + } + + return foundUnrecognizedDirective ? NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA + : NS_OK; +} + +NS_IMETHODIMP +nsSiteSecurityService::IsSecureURIScriptable( + nsIURI* aURI, JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx, + uint8_t aArgc, bool* aResult) { + OriginAttributes originAttributes; + if (aArgc > 0) { + if (!aOriginAttributes.isObject() || + !originAttributes.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + } + return IsSecureURI(aURI, originAttributes, aResult); +} + +NS_IMETHODIMP +nsSiteSecurityService::IsSecureURI(nsIURI* aURI, + const OriginAttributes& aOriginAttributes, + bool* aResult) { + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG(aResult); + + nsAutoCString hostname; + nsresult rv = GetHost(aURI, hostname); + NS_ENSURE_SUCCESS(rv, rv); + /* An IP address never qualifies as a secure URI. */ + if (HostIsIPAddress(hostname)) { + *aResult = false; + return NS_OK; + } + + return IsSecureHost(hostname, aOriginAttributes, aResult); +} + +// Checks if the given host is in the preload list. +// +// @param aHost The host to match. Only does exact host matching. +// @param aIncludeSubdomains Out, optional. Indicates whether or not to include +// subdomains. Only set if the host is matched and this function returns +// true. +// +// @return True if the host is matched, false otherwise. +bool nsSiteSecurityService::GetPreloadStatus(const nsACString& aHost, + bool* aIncludeSubdomains) const { + const int kIncludeSubdomains = 1; + bool found = false; + + PRTime currentTime = PR_Now() + (mPreloadListTimeOffset * PR_USEC_PER_SEC); + if (mUsePreloadList && currentTime < gPreloadListExpirationTime) { + int result = mDafsa.Lookup(aHost); + found = (result != mozilla::Dafsa::kKeyNotFound); + if (found && aIncludeSubdomains) { + *aIncludeSubdomains = (result == kIncludeSubdomains); + } + } + + return found; +} + +// Determines whether or not there is a matching HSTS entry for the given host. +// If aRequireIncludeSubdomains is set, then for there to be a matching HSTS +// entry, it must assert includeSubdomains. +bool nsSiteSecurityService::HostMatchesHSTSEntry( + const nsAutoCString& aHost, bool aRequireIncludeSubdomains, + const OriginAttributes& aOriginAttributes) { + // First we check for an entry in site security storage. If that entry exists, + // we don't want to check in the preload lists. We only want to use the + // stored value if it is not a knockout entry, however. + // Additionally, if it is a knockout entry, we want to stop looking for data + // on the host, because the knockout entry indicates "we have no information + // regarding the security status of this host". + bool isPrivate = aOriginAttributes.mPrivateBrowsingId > 0; + mozilla::DataStorageType storageType = isPrivate + ? mozilla::DataStorage_Private + : mozilla::DataStorage_Persistent; + nsAutoCString storageKey; + SSSLOG(("Seeking HSTS entry for %s", aHost.get())); + SetStorageKey(aHost, aOriginAttributes, storageKey); + nsCString value = mSiteStateStorage->Get(storageKey, storageType); + SiteHSTSState siteState(aHost, aOriginAttributes, value); + if (siteState.mHSTSState != SecurityPropertyUnset) { + SSSLOG(("Found HSTS entry for %s", aHost.get())); + bool expired = siteState.IsExpired(); + if (!expired) { + SSSLOG(("Entry for %s is not expired", aHost.get())); + if (siteState.mHSTSState == SecurityPropertySet) { + return aRequireIncludeSubdomains ? siteState.mHSTSIncludeSubdomains + : true; + } + } + + if (expired) { + SSSLOG(("Entry %s is expired - checking for preload state", aHost.get())); + if (!GetPreloadStatus(aHost)) { + SSSLOG(("No static preload - removing expired entry")); + mSiteStateStorage->Remove(storageKey, storageType); + } + } + return false; + } + + bool includeSubdomains = false; + + // Finally look in the static preload list. + if (siteState.mHSTSState == SecurityPropertyUnset && + GetPreloadStatus(aHost, &includeSubdomains)) { + SSSLOG(("%s is a preloaded HSTS host", aHost.get())); + return aRequireIncludeSubdomains ? includeSubdomains : true; + } + + return false; +} + +nsresult nsSiteSecurityService::IsSecureHost( + const nsACString& aHost, const OriginAttributes& aOriginAttributes, + bool* aResult) { + NS_ENSURE_ARG(aResult); + *aResult = false; + + /* An IP address never qualifies as a secure URI. */ + const nsCString& flatHost = PromiseFlatCString(aHost); + if (HostIsIPAddress(flatHost)) { + return NS_OK; + } + + nsAutoCString host( + PublicKeyPinningService::CanonicalizeHostname(flatHost.get())); + + // First check the exact host. + if (HostMatchesHSTSEntry(host, false, aOriginAttributes)) { + *aResult = true; + return NS_OK; + } + + SSSLOG(("%s not congruent match for any known HSTS host", host.get())); + const char* superdomain; + + uint32_t offset = 0; + for (offset = host.FindChar('.', offset) + 1; offset > 0; + offset = host.FindChar('.', offset) + 1) { + superdomain = host.get() + offset; + + // If we get an empty string, don't continue. + if (strlen(superdomain) < 1) { + break; + } + + // Do the same thing as with the exact host except now we're looking at + // ancestor domains of the original host and, therefore, we have to require + // that the entry asserts includeSubdomains. + nsAutoCString superdomainString(superdomain); + if (HostMatchesHSTSEntry(superdomainString, true, aOriginAttributes)) { + *aResult = true; + return NS_OK; + } + + SSSLOG( + ("superdomain %s not known HSTS host (or includeSubdomains not set), " + "walking up domain", + superdomain)); + } + + // If we get here, there was no congruent match, and no superdomain matched + // while asserting includeSubdomains, so this host is not HSTS. + *aResult = false; + return NS_OK; +} + +NS_IMETHODIMP +nsSiteSecurityService::ClearAll() { return mSiteStateStorage->Clear(); } + +//------------------------------------------------------------ +// nsSiteSecurityService::nsIObserver +//------------------------------------------------------------ + +NS_IMETHODIMP +nsSiteSecurityService::Observe(nsISupports* /*subject*/, const char* topic, + const char16_t* /*data*/) { + // Don't access Preferences off the main thread. + if (!NS_IsMainThread()) { + MOZ_ASSERT_UNREACHABLE("Preferences accessed off main thread"); + return NS_ERROR_NOT_SAME_THREAD; + } + + if (strcmp(topic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) { + mUsePreloadList = mozilla::Preferences::GetBool( + "network.stricttransportsecurity.preloadlist", true); + mPreloadListTimeOffset = + mozilla::Preferences::GetInt("test.currentTimeOffsetSeconds", 0); + } + + return NS_OK; +} |