/* 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 "IntegrityPolicyService.h" #include "mozilla/BasePrincipal.h" #include "mozilla/Logging.h" #include "mozilla/StaticPrefs_security.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/IntegrityPolicy.h" #include "mozilla/dom/IntegrityViolationReportBody.h" #include "mozilla/dom/PolicyContainer.h" #include "mozilla/dom/ReportingUtils.h" #include "mozilla/dom/RequestBinding.h" #include "mozilla/dom/SRIMetadata.h" #include "mozilla/net/SFVService.h" #include "nsContentSecurityManager.h" #include "nsContentSecurityUtils.h" #include "nsContentUtils.h" #include "nsILoadInfo.h" #include "nsString.h" using namespace mozilla; static LazyLogModule sIntegrityPolicyServiceLogModule("IntegrityPolicy"); #define LOG(fmt, ...) \ MOZ_LOG_FMT(sIntegrityPolicyServiceLogModule, LogLevel::Debug, fmt, \ ##__VA_ARGS__) namespace mozilla::dom { IntegrityPolicyService::~IntegrityPolicyService() = default; /* nsIContentPolicy implementation */ NS_IMETHODIMP IntegrityPolicyService::ShouldLoad(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, int16_t* aDecision) { LOG("ShouldLoad: [{}] Entered ShouldLoad", static_cast(aLoadInfo)); *aDecision = nsIContentPolicy::ACCEPT; if (!StaticPrefs::security_integrity_policy_enabled()) { LOG("ShouldLoad: [{}] Integrity policy is disabled", static_cast(aLoadInfo)); return NS_OK; } if (!aContentLocation) { LOG("ShouldLoad: [{}] No content location", static_cast(aLoadInfo)); return NS_ERROR_FAILURE; } bool block = ShouldRequestBeBlocked(aContentLocation, aLoadInfo); *aDecision = block ? nsIContentPolicy::REJECT_SERVER : nsIContentPolicy::ACCEPT; return NS_OK; } NS_IMETHODIMP IntegrityPolicyService::ShouldProcess(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, int16_t* aDecision) { *aDecision = nsIContentPolicy::ACCEPT; return NS_OK; } // https://w3c.github.io/webappsec-subresource-integrity/#should-request-be-blocked-by-integrity-policy-section bool IntegrityPolicyService::ShouldRequestBeBlocked(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo) { // Efficiency check: if we don't care about this type, we can skip. auto destination = IntegrityPolicy::ContentTypeToDestinationType( aLoadInfo->InternalContentPolicyType()); if (destination.isNothing()) { LOG("ShouldLoad: [{}] Integrity policy doesn't handle this type={}", static_cast(aLoadInfo), static_cast(aLoadInfo->InternalContentPolicyType())); return false; } // Exempt addons from integrity policy checks. // Top level document loads have null LoadingPrincipal, but we don't apply // integrity policy to top level document loads right now. if (BasePrincipal::Cast(aLoadInfo->TriggeringPrincipal()) ->OverridesCSP(aLoadInfo->GetLoadingPrincipal())) { LOG("ShouldLoad: [{}] Got a request from an addon, allowing it.", static_cast(aLoadInfo)); return false; } // 2. Let parsedMetadata be the result of calling parse metadata with // request’s integrity metadata. // In our case, parsedMetadata is in loadInfo. Maybe maybeRequestMode; aLoadInfo->GetRequestMode(&maybeRequestMode); if (maybeRequestMode.isNothing()) { // We don't have a request mode set explicitly, get it from the secFlags. // Just make sure that we aren't trying to get it from a // nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK loadInfo. In those // cases, we have to set the requestMode explicitly. MOZ_ASSERT(aLoadInfo->GetSecurityFlags() != nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK); maybeRequestMode = Some(nsContentSecurityManager::SecurityModeToRequestMode( aLoadInfo->GetSecurityMode())); } RequestMode requestMode = *maybeRequestMode; if (MOZ_LOG_TEST(sIntegrityPolicyServiceLogModule, LogLevel::Debug)) { nsAutoString integrityMetadata; aLoadInfo->GetIntegrityMetadata(integrityMetadata); LOG("ShouldLoad: [{}] uri={} destination={} " "requestMode={} integrityMetadata={}", static_cast(aLoadInfo), aContentLocation->GetSpecOrDefault(), static_cast(*destination), static_cast(requestMode), NS_ConvertUTF16toUTF8(integrityMetadata).get()); } // 3. If parsedMetadata is not the empty set and request’s mode is either // "cors" or "same-origin", return "Allowed". if (requestMode == RequestMode::Cors || requestMode == RequestMode::Same_origin) { nsAutoString integrityMetadata; aLoadInfo->GetIntegrityMetadata(integrityMetadata); SRIMetadata outMetadata; dom::SRICheck::IntegrityMetadata(integrityMetadata, aContentLocation->GetSpecOrDefault(), nullptr, &outMetadata); if (outMetadata.IsValid()) { LOG("ShouldLoad: [{}] Allowed because we have valid a integrity.", static_cast(aLoadInfo)); return false; } } // 4. If request's url is local, return "Allowed". if (aContentLocation->SchemeIs("data") || aContentLocation->SchemeIs("blob") || aContentLocation->SchemeIs("about")) { LOG("ShouldLoad: [{}] Allowed because we have data or blob.", static_cast(aLoadInfo)); return false; } // We only support integrity policy for documents so far. nsCOMPtr policyContainer = aLoadInfo->GetPolicyContainer(); if (!policyContainer) { LOG("ShouldLoad: [{}] No policy container", static_cast(aLoadInfo)); return false; } // 5. Let policy be policyContainer’s integrity policy. // 6. Let reportPolicy be policyContainer’s report only integrity policy. // Our IntegrityPolicy struct contains both the enforcement and // report-only policies. RefPtr policy = IntegrityPolicy::Cast( PolicyContainer::Cast(policyContainer)->GetIntegrityPolicy()); if (!policy) { // 7. If both policy and reportPolicy are empty integrity policy structs, // return "Allowed". LOG("ShouldLoad: [{}] No integrity policy", static_cast(aLoadInfo)); return false; } // TODO: 8. Let global be request’s client’s global object. // TODO: 9. If global is not a Window nor a WorkerGlobalScope, return // "Allowed". // Steps 10-13 in policy->PolicyContains(...) bool contains = false; bool roContains = false; policy->PolicyContains(*destination, &contains, &roContains); // 14. If block is true or reportBlock is true, then report violation // with request, block, reportBlock, policy and reportPolicy. if (contains || roContains) { ReportToConsole(aContentLocation, aLoadInfo, *destination, contains, roContains); ReportViolation(aContentLocation, aLoadInfo, *destination, policy, contains, roContains); } // 15. If block is true, then return "Blocked"; otherwise "Allowed". return contains; } const char* GetReportMessageKey(bool aEnforcing, IntegrityPolicy::DestinationType aDestination) { // If we are not enforcing, we are reporting only. switch (aDestination) { case IntegrityPolicy::DestinationType::Script: return aEnforcing ? "IntegrityPolicyEnforceBlockedScript" : "IntegrityPolicyReportOnlyBlockedScript"; case IntegrityPolicy::DestinationType::Style: return aEnforcing ? "IntegrityPolicyEnforceBlockedStylesheet" : "IntegrityPolicyReportOnlyBlockedStylesheet"; default: MOZ_ASSERT_UNREACHABLE("Unhandled destination type"); return nullptr; } } void IntegrityPolicyService::ReportToConsole( nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, IntegrityPolicy::DestinationType aDestination, bool aEnforce, bool aReportOnly) const { if (nsContentUtils::IsPreloadType(aLoadInfo->InternalContentPolicyType())) { return; // Don't report for preloads. } const char* messageKey = GetReportMessageKey(aEnforce, aDestination); NS_ENSURE_TRUE_VOID(messageKey); // We just report to the console for now. We should use the reporting API // in the future. AutoTArray params = { NS_ConvertUTF8toUTF16(aContentLocation->GetSpecOrDefault())}; nsAutoString localizedMsg; nsresult rv = nsContentUtils::FormatLocalizedString( PropertiesFile::SECURITY_PROPERTIES, messageKey, params, localizedMsg); NS_ENSURE_SUCCESS_VOID(rv); uint64_t windowID = aLoadInfo->GetInnerWindowID(); nsContentUtils::ReportToConsoleByWindowID( localizedMsg, aEnforce ? nsIScriptError::errorFlag : nsIScriptError::warningFlag, "Security"_ns, windowID); } // https://w3c.github.io/webappsec-subresource-integrity/#report-violation void dom::IntegrityPolicyService::ReportViolation( nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, IntegrityPolicy::DestinationType aDestination, const IntegrityPolicy* aPolicy, bool aEnforce, bool aReportOnly) const { // 1. Assert: request’s client is not null. // 2. Let settingsObject be request’s client. // 3. Let global be settingsObject’s global object. nsCOMPtr loadingContext = aLoadInfo->GetLoadingContext(); RefPtr doc; if (nsCOMPtr node = do_QueryInterface(loadingContext)) { doc = node->OwnerDoc(); } else if (nsCOMPtr window = do_QueryInterface(loadingContext)) { doc = window->GetDoc(); } if (NS_WARN_IF(!doc)) { return; } nsPIDOMWindowInner* window = doc->GetInnerWindow(); if (NS_WARN_IF(!window)) { return; } nsCOMPtr global = window->AsGlobal(); // 4. Assert: global is a Window or a WorkerGlobalScope. // 5. Let url be null. // 6. If global is a Window, set url to global’s associated Document’s URL. nsCOMPtr uri = doc->GetDocumentURI(); // XXX Maybe not the right URL? // 7. If global is a WorkerGlobalScope, set url to global’s URL. // TODO(bug 1969279): Worker support. // 8. Assert: url is a URL. if (NS_WARN_IF(!uri)) { return; } // 9. Let documentURL be the result of strip URL for use in reports on url. nsAutoCString documentURL; ReportingUtils::StripURL(uri, documentURL); NS_ConvertUTF8toUTF16 documentURLUTF16(documentURL); // 10. Let blockedURL be the result of strip URL for use in reports on // request’s URL. nsAutoCString blockedURL; ReportingUtils::StripURL(aContentLocation, blockedURL); nsAutoCString destination; switch (aDestination) { case IntegrityPolicy::DestinationType::Script: destination = "script"_ns; break; case IntegrityPolicy::DestinationType::Style: destination = "style"_ns; break; case IntegrityPolicy::DestinationType::Image: destination = "image"_ns; break; } nsTArray enforcementEndpoints; nsTArray reportOnlyEndpoints; aPolicy->Endpoints(enforcementEndpoints, reportOnlyEndpoints); // 11. If block is true, for each endpoint in policy’s endpoints: if (aEnforce) { for (const nsCString& endpoint : enforcementEndpoints) { // 11.1. Let body be a new IntegrityViolationReportBody, initialized as // follows: // // documentURL // documentURL // blockedURL // blockedURL // destination // request’s destination // reportOnly // false RefPtr body = new IntegrityViolationReportBody(global, documentURL, blockedURL, destination, false); // 11.2. Generate and queue a report with the following arguments: // // context // settingsObject // type // "integrity-violation" // destination // endpoint // data // body ReportingUtils::Report(global, nsGkAtoms::integrity_violation, NS_ConvertUTF8toUTF16(endpoint), documentURLUTF16, body); } } // 11. If reportBlock is true, for each endpoint in reportPolicy’s endpoints: if (aReportOnly) { for (const nsCString& endpoint : reportOnlyEndpoints) { // 11.1. Let reportBody be a new IntegrityViolationReportBody, initialized // as follows: // // documentURL // documentURL // blockedURL // blockedURL // destination // request’s destination // reportOnly // true RefPtr reportBody = new IntegrityViolationReportBody(global, documentURL, blockedURL, destination, true); // 11.2. Generate and queue a report with the following arguments: // // context // settingsObject // type // "integrity-violation" // destination // endpoint // data // reportBody ReportingUtils::Report(global, nsGkAtoms::integrity_violation, NS_ConvertUTF8toUTF16(endpoint), documentURLUTF16, reportBody); } } } NS_IMPL_ISUPPORTS(IntegrityPolicyService, nsIContentPolicy) } // namespace mozilla::dom #undef LOG