1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 #include "nsSiteSecurityService.h"
7 #include "PublicKeyPinningService.h"
8 #include "mozilla/Assertions.h"
9 #include "mozilla/Base64.h"
10 #include "mozilla/LinkedList.h"
11 #include "mozilla/Logging.h"
12 #include "mozilla/Preferences.h"
13 #include "mozilla/Tokenizer.h"
14 #include "mozilla/dom/PContent.h"
15 #include "mozilla/dom/ToJSValue.h"
16 #include "nsCOMArray.h"
17 #include "nsIScriptSecurityManager.h"
18 #include "nsISocketProvider.h"
20 #include "nsNSSComponent.h"
21 #include "nsNetUtil.h"
22 #include "nsPromiseFlatString.h"
23 #include "nsReadableUtils.h"
24 #include "nsSecurityHeaderParser.h"
25 #include "nsThreadUtils.h"
26 #include "nsVariant.h"
27 #include "nsXULAppAPI.h"
30 // A note about the preload list:
31 // When a site specifically disables HSTS by sending a header with
32 // 'max-age: 0', we keep a "knockout" value that means "we have no information
33 // regarding the HSTS state of this host" (any ancestor of "this host" can still
34 // influence its HSTS status via include subdomains, however).
35 // This prevents the preload list from overriding the site's current
36 // desired HSTS status.
37 #include "nsSTSPreloadListGenerated.inc"
39 using namespace mozilla
;
40 using namespace mozilla::psm
;
42 static LazyLogModule
gSSSLog("nsSSService");
44 #define SSSLOG(args) MOZ_LOG(gSSSLog, mozilla::LogLevel::Debug, args)
46 static const nsLiteralCString kHSTSKeySuffix
= ":HSTS"_ns
;
48 ////////////////////////////////////////////////////////////////////////////////
52 class SSSTokenizer final
: public Tokenizer
{
54 explicit SSSTokenizer(const nsACString
& source
) : Tokenizer(source
) {}
56 [[nodiscard
]] bool ReadBool(/*out*/ bool& value
) {
58 if (!ReadInteger(&rawValue
)) {
62 if (rawValue
!= 0 && rawValue
!= 1) {
66 value
= (rawValue
== 1);
70 [[nodiscard
]] bool ReadState(/*out*/ SecurityPropertyState
& state
) {
72 if (!ReadInteger(&rawValue
)) {
76 state
= static_cast<SecurityPropertyState
>(rawValue
);
78 case SecurityPropertyKnockout
:
79 case SecurityPropertySet
:
80 case SecurityPropertyUnset
:
90 // Parses a state string like "1500918564034,1,1" into its constituent parts.
91 bool ParseHSTSState(const nsCString
& stateString
,
92 /*out*/ PRTime
& expireTime
,
93 /*out*/ SecurityPropertyState
& state
,
94 /*out*/ bool& includeSubdomains
) {
95 SSSTokenizer
tokenizer(stateString
);
96 SSSLOG(("Parsing state from %s", stateString
.get()));
98 if (!tokenizer
.ReadInteger(&expireTime
)) {
102 if (!tokenizer
.CheckChar(',')) {
106 if (!tokenizer
.ReadState(state
)) {
110 if (!tokenizer
.CheckChar(',')) {
114 if (!tokenizer
.ReadBool(includeSubdomains
)) {
118 if (tokenizer
.CheckChar(',')) {
119 // Read now-unused "source" field.
121 if (!tokenizer
.ReadInteger(&unused
)) {
126 return tokenizer
.CheckEOF();
131 SiteHSTSState::SiteHSTSState(const nsCString
& aHost
,
132 const OriginAttributes
& aOriginAttributes
,
133 const nsCString
& aStateString
)
135 mOriginAttributes(aOriginAttributes
),
137 mHSTSState(SecurityPropertyUnset
),
138 mHSTSIncludeSubdomains(false) {
139 bool valid
= ParseHSTSState(aStateString
, mHSTSExpireTime
, mHSTSState
,
140 mHSTSIncludeSubdomains
);
142 SSSLOG(("%s is not a valid SiteHSTSState", aStateString
.get()));
144 mHSTSState
= SecurityPropertyUnset
;
145 mHSTSIncludeSubdomains
= false;
149 SiteHSTSState::SiteHSTSState(const nsCString
& aHost
,
150 const OriginAttributes
& aOriginAttributes
,
151 PRTime aHSTSExpireTime
,
152 SecurityPropertyState aHSTSState
,
153 bool aHSTSIncludeSubdomains
)
156 mOriginAttributes(aOriginAttributes
),
157 mHSTSExpireTime(aHSTSExpireTime
),
158 mHSTSState(aHSTSState
),
159 mHSTSIncludeSubdomains(aHSTSIncludeSubdomains
) {}
161 void SiteHSTSState::ToString(nsCString
& aString
) {
163 aString
.AppendInt(mHSTSExpireTime
);
165 aString
.AppendInt(mHSTSState
);
167 aString
.AppendInt(static_cast<uint32_t>(mHSTSIncludeSubdomains
));
170 nsSiteSecurityService::nsSiteSecurityService()
171 : mUsePreloadList(true), mPreloadListTimeOffset(0), mDafsa(kDafsa
) {}
173 nsSiteSecurityService::~nsSiteSecurityService() = default;
175 NS_IMPL_ISUPPORTS(nsSiteSecurityService
, nsIObserver
, nsISiteSecurityService
)
177 nsresult
nsSiteSecurityService::Init() {
178 // Don't access Preferences off the main thread.
179 if (!NS_IsMainThread()) {
180 MOZ_ASSERT_UNREACHABLE("nsSiteSecurityService initialized off main thread");
181 return NS_ERROR_NOT_SAME_THREAD
;
184 mUsePreloadList
= mozilla::Preferences::GetBool(
185 "network.stricttransportsecurity.preloadlist", true);
186 mozilla::Preferences::AddStrongObserver(
187 this, "network.stricttransportsecurity.preloadlist");
188 mPreloadListTimeOffset
=
189 mozilla::Preferences::GetInt("test.currentTimeOffsetSeconds", 0);
190 mozilla::Preferences::AddStrongObserver(this,
191 "test.currentTimeOffsetSeconds");
192 nsCOMPtr
<nsIDataStorageManager
> dataStorageManager(
193 do_GetService("@mozilla.org/security/datastoragemanager;1"));
194 if (!dataStorageManager
) {
195 return NS_ERROR_FAILURE
;
198 dataStorageManager
->Get(nsIDataStorageManager::SiteSecurityServiceState
,
199 getter_AddRefs(mSiteStateStorage
));
203 if (!mSiteStateStorage
) {
204 return NS_ERROR_FAILURE
;
210 nsresult
nsSiteSecurityService::GetHost(nsIURI
* aURI
, nsACString
& aResult
) {
211 nsCOMPtr
<nsIURI
> innerURI
= NS_GetInnermostURI(aURI
);
213 return NS_ERROR_FAILURE
;
217 nsresult rv
= innerURI
->GetAsciiHost(host
);
222 aResult
.Assign(PublicKeyPinningService::CanonicalizeHostname(host
.get()));
223 if (aResult
.IsEmpty()) {
224 return NS_ERROR_UNEXPECTED
;
230 static void NormalizePartitionKey(nsString
& partitionKey
) {
231 // If present, the partitionKey will be of the form
232 // "(<scheme>,<domain>[,port>])" (where "<scheme>" will be "https" or "http"
233 // and "<port>", if present, will be a port number). This normalizes the
234 // scheme to "https" and strips the port so that a domain noted as HSTS will
235 // be HSTS regardless of scheme and port, as per the RFC.
236 Tokenizer16
tokenizer(partitionKey
, nullptr, u
".-_");
237 if (!tokenizer
.CheckChar(u
'(')) {
241 if (!(tokenizer
.ReadWord(scheme
))) {
244 if (!tokenizer
.CheckChar(u
',')) {
248 if (!tokenizer
.ReadWord(host
)) {
251 partitionKey
.Assign(u
"(https,");
252 partitionKey
.Append(host
);
253 partitionKey
.Append(u
")");
256 // Uses the previous format of storage key. Only to be used for migrating old
258 static void GetOldStorageKey(const nsACString
& hostname
,
259 const OriginAttributes
& aOriginAttributes
,
260 /*out*/ nsAutoCString
& storageKey
) {
261 storageKey
= hostname
;
263 // Don't isolate by userContextId.
264 OriginAttributes originAttributesNoUserContext
= aOriginAttributes
;
265 originAttributesNoUserContext
.mUserContextId
=
266 nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID
;
267 nsAutoCString originAttributesSuffix
;
268 originAttributesNoUserContext
.CreateSuffix(originAttributesSuffix
);
269 storageKey
.Append(originAttributesSuffix
);
270 storageKey
.Append(kHSTSKeySuffix
);
273 static void GetStorageKey(const nsACString
& hostname
,
274 const OriginAttributes
& aOriginAttributes
,
275 /*out*/ nsAutoCString
& storageKey
) {
276 storageKey
= hostname
;
278 // Don't isolate by userContextId.
279 OriginAttributes originAttributesNoUserContext
= aOriginAttributes
;
280 originAttributesNoUserContext
.mUserContextId
=
281 nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID
;
282 NormalizePartitionKey(originAttributesNoUserContext
.mPartitionKey
);
283 nsAutoCString originAttributesSuffix
;
284 originAttributesNoUserContext
.CreateSuffix(originAttributesSuffix
);
285 storageKey
.Append(originAttributesSuffix
);
288 // Expire times are in millis. Since Headers max-age is in seconds, and
289 // PR_Now() is in micros, normalize the units at milliseconds.
290 static int64_t ExpireTimeFromMaxAge(uint64_t maxAge
) {
291 return (PR_Now() / PR_USEC_PER_MSEC
) + ((int64_t)maxAge
* PR_MSEC_PER_SEC
);
294 inline uint64_t AbsoluteDifference(int64_t a
, int64_t b
) {
301 const uint64_t sOneDayInMilliseconds
= 24 * 60 * 60 * 1000;
303 nsresult
nsSiteSecurityService::SetHSTSState(
304 const char* aHost
, int64_t maxage
, bool includeSubdomains
,
305 SecurityPropertyState aHSTSState
,
306 const OriginAttributes
& aOriginAttributes
) {
307 nsAutoCString
hostname(aHost
);
308 // If max-age is zero, the host is no longer considered HSTS. If the host was
309 // preloaded, we store an entry indicating that this host is not HSTS, causing
310 // the preloaded information to be ignored.
312 return MarkHostAsNotHSTS(hostname
, aOriginAttributes
);
315 MOZ_ASSERT(aHSTSState
== SecurityPropertySet
,
316 "HSTS State must be SecurityPropertySet");
318 int64_t expiretime
= ExpireTimeFromMaxAge(maxage
);
319 SiteHSTSState
siteState(hostname
, aOriginAttributes
, expiretime
, aHSTSState
,
321 nsAutoCString stateString
;
322 siteState
.ToString(stateString
);
323 SSSLOG(("SSS: setting state for %s", hostname
.get()));
324 bool isPrivate
= aOriginAttributes
.IsPrivateBrowsing();
325 nsIDataStorage::DataType storageType
=
326 isPrivate
? nsIDataStorage::DataType::Private
327 : nsIDataStorage::DataType::Persistent
;
328 SSSLOG(("SSS: storing HSTS site entry for %s", hostname
.get()));
331 GetWithMigration(hostname
, aOriginAttributes
, storageType
, value
);
332 // If this fails for a reason other than nothing by that key exists,
333 // propagate the failure.
334 if (NS_FAILED(rv
) && rv
!= NS_ERROR_NOT_AVAILABLE
) {
337 // This is an entirely new entry.
338 if (rv
== NS_ERROR_NOT_AVAILABLE
) {
339 nsAutoCString storageKey
;
340 GetStorageKey(hostname
, aOriginAttributes
, storageKey
);
341 return mSiteStateStorage
->Put(storageKey
, stateString
, storageType
);
343 // Otherwise, only update the backing storage if the currently-stored state
344 // is different. In the case of expiration time, "different" means "is
345 // different by more than a day".
346 SiteHSTSState
curSiteState(hostname
, aOriginAttributes
, value
);
347 if (curSiteState
.mHSTSState
!= siteState
.mHSTSState
||
348 curSiteState
.mHSTSIncludeSubdomains
!= siteState
.mHSTSIncludeSubdomains
||
349 AbsoluteDifference(curSiteState
.mHSTSExpireTime
,
350 siteState
.mHSTSExpireTime
) > sOneDayInMilliseconds
) {
352 PutWithMigration(hostname
, aOriginAttributes
, storageType
, stateString
);
361 // Helper function to mark a host as not HSTS. In the general case, we can just
362 // remove the HSTS state. However, for preloaded entries, we have to store an
363 // entry that indicates this host is not HSTS to prevent the implementation
364 // using the preloaded information.
365 nsresult
nsSiteSecurityService::MarkHostAsNotHSTS(
366 const nsAutoCString
& aHost
, const OriginAttributes
& aOriginAttributes
) {
367 bool isPrivate
= aOriginAttributes
.IsPrivateBrowsing();
368 nsIDataStorage::DataType storageType
=
369 isPrivate
? nsIDataStorage::DataType::Private
370 : nsIDataStorage::DataType::Persistent
;
371 if (GetPreloadStatus(aHost
)) {
372 SSSLOG(("SSS: storing knockout entry for %s", aHost
.get()));
373 SiteHSTSState
siteState(aHost
, aOriginAttributes
, 0,
374 SecurityPropertyKnockout
, false);
375 nsAutoCString stateString
;
376 siteState
.ToString(stateString
);
378 PutWithMigration(aHost
, aOriginAttributes
, storageType
, stateString
);
379 NS_ENSURE_SUCCESS(rv
, rv
);
381 SSSLOG(("SSS: removing entry for %s", aHost
.get()));
382 RemoveWithMigration(aHost
, aOriginAttributes
, storageType
);
389 nsSiteSecurityService::ResetState(nsIURI
* aURI
,
390 JS::Handle
<JS::Value
> aOriginAttributes
,
391 nsISiteSecurityService::ResetStateBy aScope
,
392 JSContext
* aCx
, uint8_t aArgc
) {
394 return NS_ERROR_INVALID_ARG
;
397 OriginAttributes originAttributes
;
399 // OriginAttributes were passed in.
400 if (!aOriginAttributes
.isObject() ||
401 !originAttributes
.Init(aCx
, aOriginAttributes
)) {
402 return NS_ERROR_INVALID_ARG
;
405 nsISiteSecurityService::ResetStateBy scope
=
406 nsISiteSecurityService::ResetStateBy::ExactDomain
;
408 // ResetStateBy scope was passed in
412 return ResetStateInternal(aURI
, originAttributes
, scope
);
415 // Helper function to reset stored state of the given type for the host
416 // identified by the given URI. If there is preloaded information for the host,
417 // that information will be used for future queries. C.f. MarkHostAsNotHSTS,
418 // which will store a knockout entry for preloaded HSTS hosts that have sent a
419 // header with max-age=0 (meaning preloaded information will then not be used
421 nsresult
nsSiteSecurityService::ResetStateInternal(
422 nsIURI
* aURI
, const OriginAttributes
& aOriginAttributes
,
423 nsISiteSecurityService::ResetStateBy aScope
) {
425 return NS_ERROR_INVALID_ARG
;
427 nsAutoCString hostname
;
428 nsresult rv
= GetHost(aURI
, hostname
);
433 OriginAttributes
normalizedOriginAttributes(aOriginAttributes
);
434 NormalizePartitionKey(normalizedOriginAttributes
.mPartitionKey
);
436 if (aScope
== ResetStateBy::ExactDomain
) {
437 ResetStateForExactDomain(hostname
, normalizedOriginAttributes
);
441 nsTArray
<RefPtr
<nsIDataStorageItem
>> items
;
442 rv
= mSiteStateStorage
->GetAll(items
);
446 for (const auto& item
: items
) {
447 static const nsLiteralCString kHPKPKeySuffix
= ":HPKP"_ns
;
449 rv
= item
->GetKey(key
);
454 rv
= item
->GetValue(value
);
458 if (StringEndsWith(key
, kHPKPKeySuffix
)) {
459 (void)mSiteStateStorage
->Remove(key
,
460 nsIDataStorage::DataType::Persistent
);
463 size_t suffixLength
=
464 StringEndsWith(key
, kHSTSKeySuffix
) ? kHSTSKeySuffix
.Length() : 0;
465 nsCString
origin(StringHead(key
, key
.Length() - suffixLength
));
466 nsAutoCString itemHostname
;
467 OriginAttributes itemOriginAttributes
;
468 if (!itemOriginAttributes
.PopulateFromOrigin(origin
, itemHostname
)) {
471 bool hasRootDomain
= false;
472 nsresult rv
= net::HasRootDomain(itemHostname
, hostname
, &hasRootDomain
);
477 ResetStateForExactDomain(itemHostname
, itemOriginAttributes
);
478 } else if (aScope
== ResetStateBy::BaseDomain
) {
479 mozilla::dom::PartitionKeyPatternDictionary partitionKeyPattern
;
480 partitionKeyPattern
.mBaseDomain
.Construct(
481 NS_ConvertUTF8toUTF16(hostname
));
482 OriginAttributesPattern originAttributesPattern
;
483 originAttributesPattern
.mPartitionKeyPattern
.Construct(
484 partitionKeyPattern
);
485 if (originAttributesPattern
.Matches(itemOriginAttributes
)) {
486 ResetStateForExactDomain(itemHostname
, itemOriginAttributes
);
493 void nsSiteSecurityService::ResetStateForExactDomain(
494 const nsCString
& aHostname
, const OriginAttributes
& aOriginAttributes
) {
495 bool isPrivate
= aOriginAttributes
.IsPrivateBrowsing();
496 nsIDataStorage::DataType storageType
=
497 isPrivate
? nsIDataStorage::DataType::Private
498 : nsIDataStorage::DataType::Persistent
;
499 RemoveWithMigration(aHostname
, aOriginAttributes
, storageType
);
502 bool nsSiteSecurityService::HostIsIPAddress(const nsCString
& hostname
) {
504 PRErrorCode prv
= PR_StringToNetAddr(hostname
.get(), &hostAddr
);
505 return (prv
== PR_SUCCESS
);
509 nsSiteSecurityService::ProcessHeaderScriptable(
510 nsIURI
* aSourceURI
, const nsACString
& aHeader
,
511 JS::Handle
<JS::Value
> aOriginAttributes
, uint64_t* aMaxAge
,
512 bool* aIncludeSubdomains
, uint32_t* aFailureResult
, JSContext
* aCx
,
514 OriginAttributes originAttributes
;
516 if (!aOriginAttributes
.isObject() ||
517 !originAttributes
.Init(aCx
, aOriginAttributes
)) {
518 return NS_ERROR_INVALID_ARG
;
521 return ProcessHeader(aSourceURI
, aHeader
, originAttributes
, aMaxAge
,
522 aIncludeSubdomains
, aFailureResult
);
526 nsSiteSecurityService::ProcessHeader(nsIURI
* aSourceURI
,
527 const nsACString
& aHeader
,
528 const OriginAttributes
& aOriginAttributes
,
530 bool* aIncludeSubdomains
,
531 uint32_t* aFailureResult
) {
532 if (aFailureResult
) {
533 *aFailureResult
= nsISiteSecurityService::ERROR_UNKNOWN
;
535 return ProcessHeaderInternal(aSourceURI
, PromiseFlatCString(aHeader
),
536 aOriginAttributes
, aMaxAge
, aIncludeSubdomains
,
540 nsresult
nsSiteSecurityService::ProcessHeaderInternal(
541 nsIURI
* aSourceURI
, const nsCString
& aHeader
,
542 const OriginAttributes
& aOriginAttributes
, uint64_t* aMaxAge
,
543 bool* aIncludeSubdomains
, uint32_t* aFailureResult
) {
544 if (aFailureResult
) {
545 *aFailureResult
= nsISiteSecurityService::ERROR_UNKNOWN
;
547 if (aMaxAge
!= nullptr) {
551 if (aIncludeSubdomains
!= nullptr) {
552 *aIncludeSubdomains
= false;
556 nsresult rv
= GetHost(aSourceURI
, host
);
557 NS_ENSURE_SUCCESS(rv
, rv
);
558 if (HostIsIPAddress(host
)) {
559 /* Don't process headers if a site is accessed by IP address. */
563 return ProcessSTSHeader(aSourceURI
, aHeader
, aOriginAttributes
, aMaxAge
,
564 aIncludeSubdomains
, aFailureResult
);
567 static uint32_t ParseSSSHeaders(const nsCString
& aHeader
,
568 bool& foundIncludeSubdomains
, bool& foundMaxAge
,
569 bool& foundUnrecognizedDirective
,
571 // "Strict-Transport-Security" ":" OWS
572 // STS-d *( OWS ";" OWS STS-d OWS)
575 // STS-d = maxAge / includeSubDomains
577 // maxAge = "max-age" "=" delta-seconds v-ext
579 // includeSubDomains = [ "includeSubDomains" ]
581 // The order of the directives is not significant.
582 // All directives must appear only once.
583 // Directive names are case-insensitive.
584 // The entire header is invalid if a directive not conforming to the
585 // syntax is encountered.
586 // Unrecognized directives (that are otherwise syntactically valid) are
587 // ignored, and the rest of the header is parsed as normal.
589 constexpr auto max_age_var
= "max-age"_ns
;
590 constexpr auto include_subd_var
= "includesubdomains"_ns
;
592 nsSecurityHeaderParser
parser(aHeader
);
593 nsresult rv
= parser
.Parse();
595 SSSLOG(("SSS: could not parse header"));
596 return nsISiteSecurityService::ERROR_COULD_NOT_PARSE_HEADER
;
598 mozilla::LinkedList
<nsSecurityHeaderDirective
>* directives
=
599 parser
.GetDirectives();
601 for (nsSecurityHeaderDirective
* directive
= directives
->getFirst();
602 directive
!= nullptr; directive
= directive
->getNext()) {
603 SSSLOG(("SSS: found directive %s\n", directive
->mName
.get()));
604 if (directive
->mName
.EqualsIgnoreCase(max_age_var
)) {
606 SSSLOG(("SSS: found two max-age directives"));
607 return nsISiteSecurityService::ERROR_MULTIPLE_MAX_AGES
;
610 SSSLOG(("SSS: found max-age directive"));
613 if (directive
->mValue
.isNothing()) {
614 SSSLOG(("SSS: max-age directive didn't include value"));
615 return nsISiteSecurityService::ERROR_INVALID_MAX_AGE
;
618 Tokenizer
tokenizer(*(directive
->mValue
));
619 if (!tokenizer
.ReadInteger(&maxAge
)) {
620 SSSLOG(("SSS: could not parse delta-seconds"));
621 return nsISiteSecurityService::ERROR_INVALID_MAX_AGE
;
624 if (!tokenizer
.CheckEOF()) {
625 SSSLOG(("SSS: invalid value for max-age directive"));
626 return nsISiteSecurityService::ERROR_INVALID_MAX_AGE
;
629 SSSLOG(("SSS: parsed delta-seconds: %" PRIu64
, maxAge
));
630 } else if (directive
->mName
.EqualsIgnoreCase(include_subd_var
)) {
631 if (foundIncludeSubdomains
) {
632 SSSLOG(("SSS: found two includeSubdomains directives"));
633 return nsISiteSecurityService::ERROR_MULTIPLE_INCLUDE_SUBDOMAINS
;
636 SSSLOG(("SSS: found includeSubdomains directive"));
637 foundIncludeSubdomains
= true;
639 if (directive
->mValue
.isSome()) {
640 SSSLOG(("SSS: includeSubdomains directive unexpectedly had value '%s'",
641 directive
->mValue
->get()));
642 return nsISiteSecurityService::ERROR_INVALID_INCLUDE_SUBDOMAINS
;
645 SSSLOG(("SSS: ignoring unrecognized directive '%s'",
646 directive
->mName
.get()));
647 foundUnrecognizedDirective
= true;
650 return nsISiteSecurityService::Success
;
653 // 100 years is wildly longer than anyone will ever need.
654 const uint64_t sMaxMaxAgeInSeconds
= UINT64_C(60 * 60 * 24 * 365 * 100);
656 nsresult
nsSiteSecurityService::ProcessSTSHeader(
657 nsIURI
* aSourceURI
, const nsCString
& aHeader
,
658 const OriginAttributes
& aOriginAttributes
, uint64_t* aMaxAge
,
659 bool* aIncludeSubdomains
, uint32_t* aFailureResult
) {
660 if (aFailureResult
) {
661 *aFailureResult
= nsISiteSecurityService::ERROR_UNKNOWN
;
663 SSSLOG(("SSS: processing HSTS header '%s'", aHeader
.get()));
665 bool foundMaxAge
= false;
666 bool foundIncludeSubdomains
= false;
667 bool foundUnrecognizedDirective
= false;
670 uint32_t sssrv
= ParseSSSHeaders(aHeader
, foundIncludeSubdomains
, foundMaxAge
,
671 foundUnrecognizedDirective
, maxAge
);
672 if (sssrv
!= nsISiteSecurityService::Success
) {
673 if (aFailureResult
) {
674 *aFailureResult
= sssrv
;
676 return NS_ERROR_FAILURE
;
679 // after processing all the directives, make sure we came across max-age
682 SSSLOG(("SSS: did not encounter required max-age directive"));
683 if (aFailureResult
) {
684 *aFailureResult
= nsISiteSecurityService::ERROR_NO_MAX_AGE
;
686 return NS_ERROR_FAILURE
;
689 // Cap the specified max-age.
690 if (maxAge
> sMaxMaxAgeInSeconds
) {
691 maxAge
= sMaxMaxAgeInSeconds
;
694 nsAutoCString hostname
;
695 nsresult rv
= GetHost(aSourceURI
, hostname
);
696 NS_ENSURE_SUCCESS(rv
, rv
);
698 // record the successfully parsed header data.
699 rv
= SetHSTSState(hostname
.get(), maxAge
, foundIncludeSubdomains
,
700 SecurityPropertySet
, aOriginAttributes
);
702 SSSLOG(("SSS: failed to set STS state"));
703 if (aFailureResult
) {
704 *aFailureResult
= nsISiteSecurityService::ERROR_COULD_NOT_SAVE_STATE
;
709 if (aMaxAge
!= nullptr) {
713 if (aIncludeSubdomains
!= nullptr) {
714 *aIncludeSubdomains
= foundIncludeSubdomains
;
717 return foundUnrecognizedDirective
? NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA
722 nsSiteSecurityService::IsSecureURIScriptable(
723 nsIURI
* aURI
, JS::Handle
<JS::Value
> aOriginAttributes
, JSContext
* aCx
,
724 uint8_t aArgc
, bool* aResult
) {
725 OriginAttributes originAttributes
;
727 if (!aOriginAttributes
.isObject() ||
728 !originAttributes
.Init(aCx
, aOriginAttributes
)) {
729 return NS_ERROR_INVALID_ARG
;
732 return IsSecureURI(aURI
, originAttributes
, aResult
);
736 nsSiteSecurityService::IsSecureURI(nsIURI
* aURI
,
737 const OriginAttributes
& aOriginAttributes
,
740 NS_ENSURE_ARG(aResult
);
742 nsAutoCString hostname
;
743 nsresult rv
= GetHost(aURI
, hostname
);
744 NS_ENSURE_SUCCESS(rv
, rv
);
745 /* An IP address never qualifies as a secure URI. */
746 if (HostIsIPAddress(hostname
)) {
751 return IsSecureHost(hostname
, aOriginAttributes
, aResult
);
754 // Checks if the given host is in the preload list.
756 // @param aHost The host to match. Only does exact host matching.
757 // @param aIncludeSubdomains Out, optional. Indicates whether or not to include
758 // subdomains. Only set if the host is matched and this function returns
761 // @return True if the host is matched, false otherwise.
762 bool nsSiteSecurityService::GetPreloadStatus(const nsACString
& aHost
,
763 bool* aIncludeSubdomains
) const {
764 const int kIncludeSubdomains
= 1;
767 PRTime currentTime
= PR_Now() + (mPreloadListTimeOffset
* PR_USEC_PER_SEC
);
768 if (mUsePreloadList
&& currentTime
< gPreloadListExpirationTime
) {
769 int result
= mDafsa
.Lookup(aHost
);
770 found
= (result
!= mozilla::Dafsa::kKeyNotFound
);
771 if (found
&& aIncludeSubdomains
) {
772 *aIncludeSubdomains
= (result
== kIncludeSubdomains
);
779 nsresult
nsSiteSecurityService::GetWithMigration(
780 const nsACString
& aHostname
, const OriginAttributes
& aOriginAttributes
,
781 nsIDataStorage::DataType aDataStorageType
, nsACString
& aValue
) {
782 // First see if this entry exists and has already been migrated.
783 nsAutoCString storageKey
;
784 GetStorageKey(aHostname
, aOriginAttributes
, storageKey
);
785 nsresult rv
= mSiteStateStorage
->Get(storageKey
, aDataStorageType
, aValue
);
786 if (NS_SUCCEEDED(rv
)) {
789 if (NS_FAILED(rv
) && rv
!= NS_ERROR_NOT_AVAILABLE
) {
792 // Otherwise, it potentially needs to be migrated, if it's persistent data.
793 if (aDataStorageType
!= nsIDataStorage::DataType::Persistent
) {
794 return NS_ERROR_NOT_AVAILABLE
;
796 nsAutoCString oldStorageKey
;
797 GetOldStorageKey(aHostname
, aOriginAttributes
, oldStorageKey
);
798 rv
= mSiteStateStorage
->Get(oldStorageKey
,
799 nsIDataStorage::DataType::Persistent
, aValue
);
803 // If there was a value, remove the old entry, insert a new one with the new
804 // key, and return the value.
805 rv
= mSiteStateStorage
->Remove(oldStorageKey
,
806 nsIDataStorage::DataType::Persistent
);
810 return mSiteStateStorage
->Put(storageKey
, aValue
,
811 nsIDataStorage::DataType::Persistent
);
814 nsresult
nsSiteSecurityService::PutWithMigration(
815 const nsACString
& aHostname
, const OriginAttributes
& aOriginAttributes
,
816 nsIDataStorage::DataType aDataStorageType
, const nsACString
& aStateString
) {
817 // Only persistent data needs migrating.
818 if (aDataStorageType
== nsIDataStorage::DataType::Persistent
) {
819 // Since the intention is to overwrite the previously-stored data anyway,
820 // the old entry can be removed.
821 nsAutoCString oldStorageKey
;
822 GetOldStorageKey(aHostname
, aOriginAttributes
, oldStorageKey
);
823 nsresult rv
= mSiteStateStorage
->Remove(
824 oldStorageKey
, nsIDataStorage::DataType::Persistent
);
830 nsAutoCString storageKey
;
831 GetStorageKey(aHostname
, aOriginAttributes
, storageKey
);
832 return mSiteStateStorage
->Put(storageKey
, aStateString
, aDataStorageType
);
835 nsresult
nsSiteSecurityService::RemoveWithMigration(
836 const nsACString
& aHostname
, const OriginAttributes
& aOriginAttributes
,
837 nsIDataStorage::DataType aDataStorageType
) {
838 // Only persistent data needs migrating.
839 if (aDataStorageType
== nsIDataStorage::DataType::Persistent
) {
840 nsAutoCString oldStorageKey
;
841 GetOldStorageKey(aHostname
, aOriginAttributes
, oldStorageKey
);
842 nsresult rv
= mSiteStateStorage
->Remove(
843 oldStorageKey
, nsIDataStorage::DataType::Persistent
);
849 nsAutoCString storageKey
;
850 GetStorageKey(aHostname
, aOriginAttributes
, storageKey
);
851 return mSiteStateStorage
->Remove(storageKey
, aDataStorageType
);
854 // Determines whether or not there is a matching HSTS entry for the given host.
855 // If aRequireIncludeSubdomains is set, then for there to be a matching HSTS
856 // entry, it must assert includeSubdomains.
857 nsresult
nsSiteSecurityService::HostMatchesHSTSEntry(
858 const nsAutoCString
& aHost
, bool aRequireIncludeSubdomains
,
859 const OriginAttributes
& aOriginAttributes
, bool& aHostMatchesHSTSEntry
) {
860 aHostMatchesHSTSEntry
= false;
861 // First we check for an entry in site security storage. If that entry exists,
862 // we don't want to check in the preload lists. We only want to use the
863 // stored value if it is not a knockout entry, however.
864 // Additionally, if it is a knockout entry, we want to stop looking for data
865 // on the host, because the knockout entry indicates "we have no information
866 // regarding the security status of this host".
867 bool isPrivate
= aOriginAttributes
.IsPrivateBrowsing();
868 nsIDataStorage::DataType storageType
=
869 isPrivate
? nsIDataStorage::DataType::Private
870 : nsIDataStorage::DataType::Persistent
;
871 SSSLOG(("Seeking HSTS entry for %s", aHost
.get()));
873 nsresult rv
= GetWithMigration(aHost
, aOriginAttributes
, storageType
, value
);
874 // If this fails for a reason other than nothing by that key exists,
875 // propagate the failure.
876 if (NS_FAILED(rv
) && rv
!= NS_ERROR_NOT_AVAILABLE
) {
879 bool checkPreloadList
= true;
880 // If something by that key does exist, decode and process that information.
881 if (NS_SUCCEEDED(rv
)) {
882 SiteHSTSState
siteState(aHost
, aOriginAttributes
, value
);
883 if (siteState
.mHSTSState
!= SecurityPropertyUnset
) {
884 SSSLOG(("Found HSTS entry for %s", aHost
.get()));
885 bool expired
= siteState
.IsExpired();
887 SSSLOG(("Entry for %s is not expired", aHost
.get()));
888 if (siteState
.mHSTSState
== SecurityPropertySet
) {
889 aHostMatchesHSTSEntry
= aRequireIncludeSubdomains
890 ? siteState
.mHSTSIncludeSubdomains
898 ("Entry %s is expired - checking for preload state", aHost
.get()));
899 if (!GetPreloadStatus(aHost
)) {
900 SSSLOG(("No static preload - removing expired entry"));
901 nsAutoCString storageKey
;
902 GetStorageKey(aHost
, aOriginAttributes
, storageKey
);
903 rv
= mSiteStateStorage
->Remove(storageKey
, storageType
);
911 checkPreloadList
= false;
914 bool includeSubdomains
= false;
915 // Finally look in the static preload list.
916 if (checkPreloadList
&& GetPreloadStatus(aHost
, &includeSubdomains
)) {
917 SSSLOG(("%s is a preloaded HSTS host", aHost
.get()));
918 aHostMatchesHSTSEntry
=
919 aRequireIncludeSubdomains
? includeSubdomains
: true;
925 nsresult
nsSiteSecurityService::IsSecureHost(
926 const nsACString
& aHost
, const OriginAttributes
& aOriginAttributes
,
928 NS_ENSURE_ARG(aResult
);
931 /* An IP address never qualifies as a secure URI. */
932 const nsCString
& flatHost
= PromiseFlatCString(aHost
);
933 if (HostIsIPAddress(flatHost
)) {
938 PublicKeyPinningService::CanonicalizeHostname(flatHost
.get()));
940 // First check the exact host.
941 bool hostMatchesHSTSEntry
= false;
942 nsresult rv
= HostMatchesHSTSEntry(host
, false, aOriginAttributes
,
943 hostMatchesHSTSEntry
);
947 if (hostMatchesHSTSEntry
) {
952 SSSLOG(("%s not congruent match for any known HSTS host", host
.get()));
953 const char* superdomain
;
956 for (offset
= host
.FindChar('.', offset
) + 1; offset
> 0;
957 offset
= host
.FindChar('.', offset
) + 1) {
958 superdomain
= host
.get() + offset
;
960 // If we get an empty string, don't continue.
961 if (strlen(superdomain
) < 1) {
965 // Do the same thing as with the exact host except now we're looking at
966 // ancestor domains of the original host and, therefore, we have to require
967 // that the entry asserts includeSubdomains.
968 nsAutoCString
superdomainString(superdomain
);
969 hostMatchesHSTSEntry
= false;
970 rv
= HostMatchesHSTSEntry(superdomainString
, true, aOriginAttributes
,
971 hostMatchesHSTSEntry
);
975 if (hostMatchesHSTSEntry
) {
981 ("superdomain %s not known HSTS host (or includeSubdomains not set), "
986 // If we get here, there was no congruent match, and no superdomain matched
987 // while asserting includeSubdomains, so this host is not HSTS.
993 nsSiteSecurityService::ClearAll() { return mSiteStateStorage
->Clear(); }
995 //------------------------------------------------------------
996 // nsSiteSecurityService::nsIObserver
997 //------------------------------------------------------------
1000 nsSiteSecurityService::Observe(nsISupports
* /*subject*/, const char* topic
,
1001 const char16_t
* /*data*/) {
1002 // Don't access Preferences off the main thread.
1003 if (!NS_IsMainThread()) {
1004 MOZ_ASSERT_UNREACHABLE("Preferences accessed off main thread");
1005 return NS_ERROR_NOT_SAME_THREAD
;
1008 if (strcmp(topic
, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID
) == 0) {
1009 mUsePreloadList
= mozilla::Preferences::GetBool(
1010 "network.stricttransportsecurity.preloadlist", true);
1011 mPreloadListTimeOffset
=
1012 mozilla::Preferences::GetInt("test.currentTimeOffsetSeconds", 0);