/* 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 "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(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(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"); nsCOMPtr dataStorageManager( do_GetService("@mozilla.org/security/datastoragemanager;1")); if (!dataStorageManager) { return NS_ERROR_FAILURE; } nsresult rv = dataStorageManager->Get(nsIDataStorageManager::SiteSecurityServiceState, getter_AddRefs(mSiteStateStorage)); if (NS_FAILED(rv)) { return rv; } if (!mSiteStateStorage) { return NS_ERROR_FAILURE; } return NS_OK; } nsresult nsSiteSecurityService::GetHost(nsIURI* aURI, nsACString& aResult) { nsCOMPtr 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 NormalizePartitionKey(nsString& partitionKey) { // If present, the partitionKey will be of the form // "(,[,port>])" (where "" will be "https" or "http" // and "", if present, will be a port number). This normalizes the // scheme to "https" and strips the port so that a domain noted as HSTS will // be HSTS regardless of scheme and port, as per the RFC. Tokenizer16 tokenizer(partitionKey, nullptr, u".-_"); if (!tokenizer.CheckChar(u'(')) { return; } nsString scheme; if (!(tokenizer.ReadWord(scheme))) { return; } if (!tokenizer.CheckChar(u',')) { return; } nsString host; if (!tokenizer.ReadWord(host)) { return; } partitionKey.Assign(u"(https,"); partitionKey.Append(host); partitionKey.Append(u")"); } // Uses the previous format of storage key. Only to be used for migrating old // entries. static void GetOldStorageKey(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); } static void GetStorageKey(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; NormalizePartitionKey(originAttributesNoUserContext.mPartitionKey); nsAutoCString originAttributesSuffix; originAttributesNoUserContext.CreateSuffix(originAttributesSuffix); storageKey.Append(originAttributesSuffix); } // 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; nsIDataStorage::DataType storageType = isPrivate ? nsIDataStorage::DataType::Private : nsIDataStorage::DataType::Persistent; SSSLOG(("SSS: storing HSTS site entry for %s", hostname.get())); nsAutoCString value; nsresult rv = GetWithMigration(hostname, aOriginAttributes, storageType, value); // If this fails for a reason other than nothing by that key exists, // propagate the failure. if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) { return rv; } // This is an entirely new entry. if (rv == NS_ERROR_NOT_AVAILABLE) { nsAutoCString storageKey; GetStorageKey(hostname, aOriginAttributes, storageKey); return mSiteStateStorage->Put(storageKey, stateString, storageType); } // Otherwise, 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". SiteHSTSState curSiteState(hostname, aOriginAttributes, value); if (curSiteState.mHSTSState != siteState.mHSTSState || curSiteState.mHSTSIncludeSubdomains != siteState.mHSTSIncludeSubdomains || AbsoluteDifference(curSiteState.mHSTSExpireTime, siteState.mHSTSExpireTime) > sOneDayInMilliseconds) { rv = PutWithMigration(hostname, aOriginAttributes, storageType, stateString); 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; nsIDataStorage::DataType storageType = isPrivate ? nsIDataStorage::DataType::Private : nsIDataStorage::DataType::Persistent; 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 = PutWithMigration(aHost, aOriginAttributes, storageType, stateString); NS_ENSURE_SUCCESS(rv, rv); } else { SSSLOG(("SSS: removing entry for %s", aHost.get())); RemoveWithMigration(aHost, aOriginAttributes, storageType); } return NS_OK; } NS_IMETHODIMP nsSiteSecurityService::ResetState(nsIURI* aURI, JS::Handle 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; } OriginAttributes normalizedOriginAttributes(aOriginAttributes); NormalizePartitionKey(normalizedOriginAttributes.mPartitionKey); if (aScope == ResetStateBy::ExactDomain) { ResetStateForExactDomain(hostname, normalizedOriginAttributes); return NS_OK; } nsTArray> items; rv = mSiteStateStorage->GetAll(items); if (NS_FAILED(rv)) { return rv; } for (const auto& item : items) { static const nsLiteralCString kHPKPKeySuffix = ":HPKP"_ns; nsAutoCString key; rv = item->GetKey(key); if (NS_FAILED(rv)) { return rv; } nsAutoCString value; rv = item->GetValue(value); if (NS_FAILED(rv)) { return rv; } if (StringEndsWith(key, kHPKPKeySuffix)) { (void)mSiteStateStorage->Remove(key, nsIDataStorage::DataType::Persistent); continue; } size_t suffixLength = StringEndsWith(key, kHSTSKeySuffix) ? kHSTSKeySuffix.Length() : 0; nsCString origin(StringHead(key, key.Length() - suffixLength)); nsAutoCString itemHostname; OriginAttributes itemOriginAttributes; if (!itemOriginAttributes.PopulateFromOrigin(origin, itemHostname)) { continue; } bool hasRootDomain = false; nsresult rv = net::HasRootDomain(itemHostname, hostname, &hasRootDomain); if (NS_FAILED(rv)) { continue; } 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) { bool isPrivate = aOriginAttributes.mPrivateBrowsingId > 0; nsIDataStorage::DataType storageType = isPrivate ? nsIDataStorage::DataType::Private : nsIDataStorage::DataType::Persistent; RemoveWithMigration(aHostname, aOriginAttributes, 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 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* 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 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; } nsresult nsSiteSecurityService::GetWithMigration( const nsACString& aHostname, const OriginAttributes& aOriginAttributes, nsIDataStorage::DataType aDataStorageType, nsACString& aValue) { // First see if this entry exists and has already been migrated. nsAutoCString storageKey; GetStorageKey(aHostname, aOriginAttributes, storageKey); nsresult rv = mSiteStateStorage->Get(storageKey, aDataStorageType, aValue); if (NS_SUCCEEDED(rv)) { return NS_OK; } if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) { return rv; } // Otherwise, it potentially needs to be migrated, if it's persistent data. if (aDataStorageType != nsIDataStorage::DataType::Persistent) { return NS_ERROR_NOT_AVAILABLE; } nsAutoCString oldStorageKey; GetOldStorageKey(aHostname, aOriginAttributes, oldStorageKey); rv = mSiteStateStorage->Get(oldStorageKey, nsIDataStorage::DataType::Persistent, aValue); if (NS_FAILED(rv)) { return rv; } // If there was a value, remove the old entry, insert a new one with the new // key, and return the value. rv = mSiteStateStorage->Remove(oldStorageKey, nsIDataStorage::DataType::Persistent); if (NS_FAILED(rv)) { return rv; } return mSiteStateStorage->Put(storageKey, aValue, nsIDataStorage::DataType::Persistent); } nsresult nsSiteSecurityService::PutWithMigration( const nsACString& aHostname, const OriginAttributes& aOriginAttributes, nsIDataStorage::DataType aDataStorageType, const nsACString& aStateString) { // Only persistent data needs migrating. if (aDataStorageType == nsIDataStorage::DataType::Persistent) { // Since the intention is to overwrite the previously-stored data anyway, // the old entry can be removed. nsAutoCString oldStorageKey; GetOldStorageKey(aHostname, aOriginAttributes, oldStorageKey); nsresult rv = mSiteStateStorage->Remove( oldStorageKey, nsIDataStorage::DataType::Persistent); if (NS_FAILED(rv)) { return rv; } } nsAutoCString storageKey; GetStorageKey(aHostname, aOriginAttributes, storageKey); return mSiteStateStorage->Put(storageKey, aStateString, aDataStorageType); } nsresult nsSiteSecurityService::RemoveWithMigration( const nsACString& aHostname, const OriginAttributes& aOriginAttributes, nsIDataStorage::DataType aDataStorageType) { // Only persistent data needs migrating. if (aDataStorageType == nsIDataStorage::DataType::Persistent) { nsAutoCString oldStorageKey; GetOldStorageKey(aHostname, aOriginAttributes, oldStorageKey); nsresult rv = mSiteStateStorage->Remove( oldStorageKey, nsIDataStorage::DataType::Persistent); if (NS_FAILED(rv)) { return rv; } } nsAutoCString storageKey; GetStorageKey(aHostname, aOriginAttributes, storageKey); return mSiteStateStorage->Remove(storageKey, aDataStorageType); } // 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. nsresult nsSiteSecurityService::HostMatchesHSTSEntry( const nsAutoCString& aHost, bool aRequireIncludeSubdomains, const OriginAttributes& aOriginAttributes, bool& aHostMatchesHSTSEntry) { aHostMatchesHSTSEntry = false; // 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; nsIDataStorage::DataType storageType = isPrivate ? nsIDataStorage::DataType::Private : nsIDataStorage::DataType::Persistent; SSSLOG(("Seeking HSTS entry for %s", aHost.get())); nsAutoCString value; nsresult rv = GetWithMigration(aHost, aOriginAttributes, storageType, value); // If this fails for a reason other than nothing by that key exists, // propagate the failure. if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) { return rv; } bool checkPreloadList = true; // If something by that key does exist, decode and process that information. if (NS_SUCCEEDED(rv)) { 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) { aHostMatchesHSTSEntry = aRequireIncludeSubdomains ? siteState.mHSTSIncludeSubdomains : true; return NS_OK; } } if (expired) { SSSLOG( ("Entry %s is expired - checking for preload state", aHost.get())); if (!GetPreloadStatus(aHost)) { SSSLOG(("No static preload - removing expired entry")); nsAutoCString storageKey; GetStorageKey(aHost, aOriginAttributes, storageKey); rv = mSiteStateStorage->Remove(storageKey, storageType); if (NS_FAILED(rv)) { return rv; } } } return NS_OK; } checkPreloadList = false; } bool includeSubdomains = false; // Finally look in the static preload list. if (checkPreloadList && GetPreloadStatus(aHost, &includeSubdomains)) { SSSLOG(("%s is a preloaded HSTS host", aHost.get())); aHostMatchesHSTSEntry = aRequireIncludeSubdomains ? includeSubdomains : true; } return NS_OK; } 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. bool hostMatchesHSTSEntry = false; nsresult rv = HostMatchesHSTSEntry(host, false, aOriginAttributes, hostMatchesHSTSEntry); if (NS_FAILED(rv)) { return rv; } if (hostMatchesHSTSEntry) { *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); hostMatchesHSTSEntry = false; rv = HostMatchesHSTSEntry(superdomainString, true, aOriginAttributes, hostMatchesHSTSEntry); if (NS_FAILED(rv)) { return rv; } if (hostMatchesHSTSEntry) { *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; }