//------------------------------------------------------------ // Functions to overlay the permission manager calls in case // we're in private browsing mode. //------------------------------------------------------------ nsresult nsStrictTransportSecurityService::AddPermission(nsIURI *aURI, const char *aType, uint32_t aPermission, uint32_t aExpireType, int64_t aExpireTime) { // Private mode doesn't address user-set (EXPIRE_NEVER) permissions: let // those be stored persistently. if (!mInPrivateMode || aExpireType == nsIPermissionManager::EXPIRE_NEVER) { // Not in private mode, or manually-set permission nsCOMPtr<nsIPrincipal> principal; nsresult rv = GetPrincipalForURI(aURI, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); return mPermMgr->AddFromPrincipal(principal, aType, aPermission, aExpireType, aExpireTime); } nsAutoCString host; nsresult rv = GetHost(aURI, host); NS_ENSURE_SUCCESS(rv, rv); STSLOG(("AddPermission for entry for %s", host.get())); // Update in mPrivateModeHostTable only, so any changes will be rolled // back when exiting private mode. // Note: EXPIRE_NEVER permissions should trump anything that shows up in // the HTTP header, so if there's an EXPIRE_NEVER permission already // don't store anything new. // Currently there's no way to get the type of expiry out of the // permission manager, but that's okay since there's nothing that stores // EXPIRE_NEVER permissions. // PutEntry returns an existing entry if there already is one, or it // creates a new one if there isn't. nsSTSHostEntry* entry = mPrivateModeHostTable.PutEntry(host.get()); if (!entry) { return NS_ERROR_OUT_OF_MEMORY; } STSLOG(("Created private mode entry for %s", host.get())); // AddPermission() will be called twice if the STS header encountered has // includeSubdomains (first for the main permission and second for the // subdomains permission). If AddPermission() gets called a second time // with the STS_SUBDOMAIN_PERMISSION, we just have to flip that bit in // the nsSTSHostEntry. if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) { entry->mIncludeSubdomains = true; } else if (strcmp(aType, STS_PERMISSION) == 0) { entry->mStsPermission = aPermission; } // Also refresh the expiration time. entry->SetExpireTime(aExpireTime); return NS_OK; }
//------------------------------------------------------------ // Functions to overlay the permission manager calls in case // we're in private browsing mode. //------------------------------------------------------------ nsresult nsStrictTransportSecurityService::AddPermission(nsIURI *aURI, const char *aType, PRUint32 aPermission, PRUint32 aExpireType, PRInt64 aExpireTime) { // Private mode doesn't address user-set (EXPIRE_NEVER) permissions: let // those be stored persistently. if (!mInPrivateMode || aExpireType == nsIPermissionManager::EXPIRE_NEVER) { // Not in private mode, or manually-set permission return mPermMgr->Add(aURI, aType, aPermission, aExpireType, aExpireTime); } nsCAutoString host; nsresult rv = GetHost(aURI, host); NS_ENSURE_SUCCESS(rv, rv); STSLOG(("AddPermission for entry for for %s", host.get())); // Update in mPrivateModeHostTable only, so any changes will be rolled // back when exiting private mode. // Note: EXPIRE_NEVER permissions should trump anything that shows up in // the HTTP header, so if there's an EXPIRE_NEVER permission already // don't store anything new. // Currently there's no way to get the type of expiry out of the // permission manager, but that's okay since there's nothing that stores // EXPIRE_NEVER permissions. // PutEntry returns an existing entry if there already is one, or it // creates a new one if there isn't. nsSTSHostEntry* entry = mPrivateModeHostTable.PutEntry(host.get()); STSLOG(("Created private mode entry for for %s", host.get())); // AddPermission() will be called twice if the STS header encountered has // includeSubdomains (first for the main permission and second for the // subdomains permission). If AddPermission() gets called a second time // with the STS_SUBDOMAIN_PERMISSION, we just have to flip that bit in // the nsSTSHostEntry. if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) { entry->mIncludeSubdomains = true; } // for the case where PutEntry() returned an existing host entry, make // sure it's not set as deleted (which might have happened in the past). entry->mDeleted = false; // Also refresh the expiration time. entry->mExpireTime = aExpireTime; return NS_OK; }
nsresult nsStrictTransportSecurityService::SetStsState(nsIURI* aSourceURI, int64_t maxage, bool includeSubdomains) { // If max-age is zero, that's an indication to immediately remove the // permissions, so here's a shortcut. if (!maxage) return RemoveStsState(aSourceURI); // Expire time is millis from now. Since STS max-age is in seconds, and // PR_Now() is in micros, must equalize the units at milliseconds. int64_t expiretime = (PR_Now() / PR_USEC_PER_MSEC) + (maxage * PR_MSEC_PER_SEC); // record entry for this host with max-age in the permissions manager STSLOG(("STS: maxage permission SET, adding permission\n")); nsresult rv = AddPermission(aSourceURI, STS_PERMISSION, (uint32_t) STS_SET, (uint32_t) nsIPermissionManager::EXPIRE_TIME, expiretime); NS_ENSURE_SUCCESS(rv, rv); if (includeSubdomains) { // record entry for this host with include subdomains in the permissions manager STSLOG(("STS: subdomains permission SET, adding permission\n")); rv = AddPermission(aSourceURI, STS_SUBDOMAIN_PERMISSION, (uint32_t) STS_SET, (uint32_t) nsIPermissionManager::EXPIRE_TIME, expiretime); NS_ENSURE_SUCCESS(rv, rv); } else { // !includeSubdomains nsAutoCString hostname; rv = GetHost(aSourceURI, hostname); NS_ENSURE_SUCCESS(rv, rv); STSLOG(("STS: subdomains permission UNSET, removing any existing ones\n")); rv = RemovePermission(hostname, STS_SUBDOMAIN_PERMISSION); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; }
NS_IMETHODIMP nsStrictTransportSecurityService::RemoveStsState(nsIURI* aURI) { // Should be called on the main thread (or via proxy) since the permission // manager is used and it's not threadsafe. NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED); nsAutoCString hostname; nsresult rv = GetHost(aURI, hostname); NS_ENSURE_SUCCESS(rv, rv); rv = RemovePermission(hostname, STS_PERMISSION); NS_ENSURE_SUCCESS(rv, rv); STSLOG(("STS: deleted maxage permission\n")); rv = RemovePermission(hostname, STS_SUBDOMAIN_PERMISSION); NS_ENSURE_SUCCESS(rv, rv); STSLOG(("STS: deleted subdomains permission\n")); return NS_OK; }
nsresult nsStrictTransportSecurityService::RemovePermission(const nsCString &aHost, const char *aType) { if (!mInPrivateMode) { // Not in private mode: remove permissions persistently. return mPermMgr->Remove(aHost, aType); } // Make changes in mPrivateModeHostTable only, so any changes will be // rolled back when exiting private mode. nsSTSHostEntry* entry = mPrivateModeHostTable.GetEntry(aHost.get()); // Build up an nsIURI for use with the permission manager. nsCOMPtr<nsIURI> uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("http://") + aHost); NS_ENSURE_SUCCESS(rv, rv); // Check to see if there's STS data stored for this host in the // permission manager (probably set outside private mode). PRUint32 permmgrValue; rv = mPermMgr->TestExactPermission(uri, aType, &permmgrValue); NS_ENSURE_SUCCESS(rv, rv); // If there is STS data in the permission manager, store a "deleted" mask // for the permission in mPrivateModeHostTable (either update // mPrivateModeHostTable to have the deleted mask, or add one). // This is because we don't want removals that happen in private mode to // be reflected when private mode is exited -- but while in private mode // we still want the effect of the removal. if (permmgrValue != nsIPermissionManager::UNKNOWN_ACTION) { // if there's no entry in mPrivateModeHostTable, we have to make one. if (!entry) { entry = mPrivateModeHostTable.PutEntry(aHost.get()); STSLOG(("Created private mode deleted mask for for %s", aHost.get())); } entry->mDeleted = true; entry->mIncludeSubdomains = false; return NS_OK; } // Otherwise, permission doesn't exist in the real permission manager, so // there's nothing to "pretend" to delete. I'ts ok to delete any copy in // mPrivateModeHostTable. if (entry) mPrivateModeHostTable.RawRemoveEntry(entry); return NS_OK; }
nsresult nsStrictTransportSecurityService::RemovePermission(const nsCString &aHost, const char *aType) { // Build up a principal for use with the permission manager. // normalize all URIs with https:// nsCOMPtr<nsIURI> uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://") + aHost); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<nsIPrincipal> principal; rv = GetPrincipalForURI(uri, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); if (!mInPrivateMode) { // Not in private mode: remove permissions persistently. // This means setting the permission to STS_KNOCKOUT in case // this host is on the preload list (so we can override it). return mPermMgr->AddFromPrincipal(principal, aType, STS_KNOCKOUT, nsIPermissionManager::EXPIRE_NEVER, 0); } // Make changes in mPrivateModeHostTable only, so any changes will be // rolled back when exiting private mode. nsSTSHostEntry* entry = mPrivateModeHostTable.GetEntry(aHost.get()); if (!entry) { entry = mPrivateModeHostTable.PutEntry(aHost.get()); if (!entry) { return NS_ERROR_OUT_OF_MEMORY; } STSLOG(("Created private mode deleted mask for %s", aHost.get())); } if (strcmp(aType, STS_PERMISSION) == 0) { entry->mStsPermission = STS_KNOCKOUT; } else if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) { entry->mIncludeSubdomains = false; } return NS_OK; }
NS_IMETHODIMP nsStrictTransportSecurityService::IsStsURI(nsIURI* aURI, bool* aResult) { // Should be called on the main thread (or via proxy) since the permission // manager is used and it's not threadsafe. NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED); // set default in case if we can't find any STS information *aResult = false; nsAutoCString host; nsresult rv = GetHost(aURI, host); NS_ENSURE_SUCCESS(rv, rv); const nsSTSPreload *preload = nullptr; nsSTSHostEntry *pbEntry = nullptr; if (mInPrivateMode) { pbEntry = mPrivateModeHostTable.GetEntry(host.get()); } nsCOMPtr<nsIPrincipal> principal; rv = GetPrincipalForURI(aURI, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); uint32_t permMgrPermission; rv = mPermMgr->TestExactPermissionFromPrincipal(principal, STS_PERMISSION, &permMgrPermission); NS_ENSURE_SUCCESS(rv, rv); // First check the exact host. This involves first checking for an entry in // the private browsing table. If that entry exists, we don't want to check // in either the permission manager or the preload list. We only want to use // the stored permission 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 sts status of this host". if (pbEntry && pbEntry->mStsPermission != STS_UNSET) { STSLOG(("Found private browsing table entry for %s", host.get())); if (!pbEntry->IsExpired() && pbEntry->mStsPermission == STS_SET) { *aResult = true; return NS_OK; } } // Next we look in the permission manager. Same story here regarding // knockout entries. else if (permMgrPermission != STS_UNSET) { STSLOG(("Found permission manager entry for %s", host.get())); if (permMgrPermission == STS_SET) { *aResult = true; return NS_OK; } } // Finally look in the preloaded list. This is the exact host, // so if an entry exists at all, this host is sts. else if (GetPreloadListEntry(host.get())) { STSLOG(("%s is a preloaded STS host", host.get())); *aResult = true; return NS_OK; } // Used for testing permissions as we walk up the domain tree. nsCOMPtr<nsIURI> domainWalkURI; nsCOMPtr<nsIPrincipal> domainWalkPrincipal; const char *subdomain; STSLOG(("no HSTS data for %s found, walking up domain", host.get())); uint32_t offset = 0; for (offset = host.FindChar('.', offset) + 1; offset > 0; offset = host.FindChar('.', offset) + 1) { subdomain = host.get() + offset; // If we get an empty string, don't continue. if (strlen(subdomain) < 1) { break; } if (mInPrivateMode) { pbEntry = mPrivateModeHostTable.GetEntry(subdomain); } // normalize all URIs with https:// rv = NS_NewURI(getter_AddRefs(domainWalkURI), NS_LITERAL_CSTRING("https://") + Substring(host, offset)); NS_ENSURE_SUCCESS(rv, rv); rv = GetPrincipalForURI(domainWalkURI, getter_AddRefs(domainWalkPrincipal)); NS_ENSURE_SUCCESS(rv, rv); rv = mPermMgr->TestExactPermissionFromPrincipal(domainWalkPrincipal, STS_PERMISSION, &permMgrPermission); NS_ENSURE_SUCCESS(rv, rv); // Do the same thing as with the exact host, except now we're looking at // ancestor domains of the original host. So, we have to look at the // include subdomains permissions (although we still have to check for the // STS_PERMISSION first to check that this is an sts host and not a // knockout entry - and again, if it is a knockout entry, we stop looking // for data on it and skip to the next higher up ancestor domain). if (pbEntry && pbEntry->mStsPermission != STS_UNSET) { STSLOG(("Found private browsing table entry for %s", subdomain)); if (!pbEntry->IsExpired() && pbEntry->mStsPermission == STS_SET) { *aResult = pbEntry->mIncludeSubdomains; break; } } else if (permMgrPermission != STS_UNSET) { STSLOG(("Found permission manager entry for %s", subdomain)); if (permMgrPermission == STS_SET) { uint32_t subdomainPermission; rv = mPermMgr->TestExactPermissionFromPrincipal(domainWalkPrincipal, STS_SUBDOMAIN_PERMISSION, &subdomainPermission); NS_ENSURE_SUCCESS(rv, rv); *aResult = (subdomainPermission == STS_SET); break; } } // This is an ancestor, so if we get a match, we have to check if the // preloaded entry includes subdomains. else if ((preload = GetPreloadListEntry(subdomain)) != nullptr) { if (preload->mIncludeSubdomains) { STSLOG(("%s is a preloaded STS host", subdomain)); *aResult = true; break; } } STSLOG(("no HSTS data for %s found, walking up domain", subdomain)); } // Use whatever we ended up with, which defaults to false. return NS_OK; }
nsresult nsStrictTransportSecurityService::ProcessStsHeaderMutating(nsIURI* aSourceURI, char* aHeader, uint64_t *aMaxAge, bool *aIncludeSubdomains) { STSLOG(("STS: ProcessStrictTransportHeader(%s)\n", aHeader)); // "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" ] const char* directive; bool foundMaxAge = false; bool foundUnrecognizedTokens = false; bool includeSubdomains = false; int64_t maxAge = 0; NS_NAMED_LITERAL_CSTRING(max_age_var, "max-age"); NS_NAMED_LITERAL_CSTRING(include_subd_var, "includesubdomains"); while ((directive = NS_strtok(";", &aHeader))) { //skip leading whitespace directive = NS_strspnp(" \t", directive); STS_PARSER_FAIL_IF(!(*directive), ("error removing initial whitespace\n.")); if (!PL_strncasecmp(directive, max_age_var.get(), max_age_var.Length())) { // skip directive name directive += max_age_var.Length(); // skip leading whitespace directive = NS_strspnp(" \t", directive); STS_PARSER_FAIL_IF(*directive != '=', ("No equal sign found in max-age directive\n")); // skip over the equal sign STS_PARSER_FAIL_IF(*(++directive) == '\0', ("No delta-seconds present\n")); // obtain the delta-seconds value STS_PARSER_FAIL_IF(PR_sscanf(directive, "%lld", &maxAge) != 1, ("Could not convert delta-seconds\n")); STSLOG(("STS: ProcessStrictTransportHeader() STS found maxage %lld\n", maxAge)); foundMaxAge = true; // skip max-age value and trailing whitespace directive = NS_strspnp("0123456789 \t", directive); // log unknown tokens, but don't fail (for forwards compatibility) if (*directive != '\0') { foundUnrecognizedTokens = true; STSLOG(("Extra stuff in max-age after delta-seconds: %s \n", directive)); } } else if (!PL_strncasecmp(directive, include_subd_var.get(), include_subd_var.Length())) { directive += include_subd_var.Length(); // only record "includesubdomains" if it is a token by itself... for // example, don't set includeSubdomains = true if the directive is // "includesubdomainsFooBar". if (*directive == '\0' || *directive =='\t' || *directive == ' ') { includeSubdomains = true; STSLOG(("STS: ProcessStrictTransportHeader: obtained subdomains status\n")); // skip trailing whitespace directive = NS_strspnp(" \t", directive); if (*directive != '\0') { foundUnrecognizedTokens = true; STSLOG(("Extra stuff after includesubdomains: %s\n", directive)); } } else { foundUnrecognizedTokens = true; STSLOG(("Unrecognized directive in header: %s\n", directive)); } } else { // log unknown directives, but don't fail (for backwards compatibility) foundUnrecognizedTokens = true; STSLOG(("Unrecognized directive in header: %s\n", directive)); } } // after processing all the directives, make sure we came across max-age // somewhere. STS_PARSER_FAIL_IF(!foundMaxAge, ("Parse ERROR: couldn't locate max-age token\n")); // record the successfully parsed header data. SetStsState(aSourceURI, maxAge, includeSubdomains); if (aMaxAge != nullptr) { *aMaxAge = (uint64_t)maxAge; } if (aIncludeSubdomains != nullptr) { *aIncludeSubdomains = includeSubdomains; } return foundUnrecognizedTokens ? NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA : NS_OK; }
nsresult nsStrictTransportSecurityService::TestPermission(nsIURI *aURI, const char *aType, PRUint32 *aPermission, bool testExact) { // set default for if we can't find any STS information *aPermission = nsIPermissionManager::UNKNOWN_ACTION; if (!mInPrivateMode) { // if not in private mode, just delegate to the permission manager. if (testExact) return mPermMgr->TestExactPermission(aURI, aType, aPermission); else return mPermMgr->TestPermission(aURI, aType, aPermission); } nsCAutoString host; nsresult rv = GetHost(aURI, host); if (NS_FAILED(rv)) return NS_OK; nsSTSHostEntry *entry; PRUint32 actualExactPermission; PRUint32 offset = 0; PRInt64 now = PR_Now() / 1000; // Used for testing permissions as we walk up the domain tree. nsCOMPtr<nsIURI> domainWalkURI; // In parallel, loop over private mode cache and also the real permission // manager--ignoring any masked as "deleted" in the local cache. We have // to do this here since the most specific permission in *either* the // permission manager or mPrivateModeHostTable should be used. do { entry = mPrivateModeHostTable.GetEntry(host.get() + offset); STSLOG(("Checking PM Table entry and permmgr for %s", host.get()+offset)); // flag as deleted any entries encountered that have expired. We only // flag the nsSTSHostEntry because there could be some data in the // permission manager that -- if not in private mode -- would have been // overwritten by newly encountered STS data. if (entry && (now > entry->mExpireTime)) { STSLOG(("Deleting expired PM Table entry for %s", host.get()+offset)); entry->mDeleted = true; entry->mIncludeSubdomains = false; } rv = NS_NewURI(getter_AddRefs(domainWalkURI), NS_LITERAL_CSTRING("http://") + Substring(host, offset)); NS_ENSURE_SUCCESS(rv, rv); rv = mPermMgr->TestExactPermission(domainWalkURI, aType, &actualExactPermission); NS_ENSURE_SUCCESS(rv, rv); // There are three cases as we walk up the hostname testing // permissions: // 1. There's no entry in mPrivateModeHostTable for this host; rely // on data in the permission manager if (!entry) { if (actualExactPermission != nsIPermissionManager::UNKNOWN_ACTION) { // no cached data but a permission in the permission manager so use // it and stop looking. *aPermission = actualExactPermission; STSLOG(("no PM Table entry for %s, using permmgr", host.get()+offset)); break; } } // 2. There's a "deleted" mask in mPrivateModeHostTable for this host // or we're looking for includeSubdomain information and it's not set: // any data in the permission manager must be ignored, since the // permission would have been deleted if not in private mode. else if (entry->mDeleted || (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0 && !entry->mIncludeSubdomains)) { STSLOG(("no entry at all for %s, walking up", host.get()+offset)); // keep looking } // 3. There's a non-deleted entry in mPrivateModeHostTable for this // host, so it should be used. else { // All STS permissions' values are ALLOW_ACTION or they are not // known (as in, not set or turned off). *aPermission = nsIPermissionManager::ALLOW_ACTION; STSLOG(("PM Table entry for %s: forcing", host.get()+offset)); break; } // Don't continue walking up the host segments if the test was for an // exact match only. if (testExact) break; STSLOG(("no PM Table entry or permmgr data for %s, walking up domain", host.get()+offset)); // walk up the host segments offset = host.FindChar('.', offset) + 1; } while (offset > 0); // Use whatever we ended up with, which defaults to UNKNOWN_ACTION. return NS_OK; }