/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 "IntegrityPolicyWAICT.h" #include "IntegrityPolicy.h" #include "WAICTUtils.h" #include "mozilla/Logging.h" #include "mozilla/StaticPrefs_security.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/IntegrityViolationReportBody.h" #include "mozilla/dom/ReportingUtils.h" #include "mozilla/dom/WindowGlobalChild.h" #include "mozilla/net/SFVService.h" #include "nsCSPParser.h" #include "nsContentUtils.h" #include "nsIScriptError.h" using namespace mozilla; namespace mozilla::dom { using mozilla::waict::gWaictLog; NS_IMPL_ISUPPORTS(IntegrityPolicyWAICT, nsIStreamLoaderObserver) IntegrityPolicyWAICT::IntegrityPolicyWAICT(Document* aDocument) : mDocument(do_GetWeakReference(aDocument)), mDocumentURI(aDocument->GetDocumentURI()), mPrincipal(aDocument->NodePrincipal()) {} IntegrityPolicyWAICT::~IntegrityPolicyWAICT() { if (mPromise) { ResolvePromiseInvalidManifest(); } } RefPtr IntegrityPolicyWAICT::WaitForManifestLoad() { MOZ_ASSERT(!mManifestURL.IsEmpty()); return mPromise; } bool IntegrityPolicyWAICT::MaybeCheckResourceIntegrity( nsIURI* aURI, IntegrityPolicy::DestinationType aDestination, const nsACString& aHash) { MOZ_LOG_FMT( gWaictLog, LogLevel::Debug, "IntegrityPolicyWAICT::MaybeCheckResourceIntegrity aURI = {} aHash = {}", aURI->GetSpecOrDefault().get(), nsCString(aHash).get()); // If manifest failed to load/validate, decision depends on mode if (!mManifestValid) { ReportViolation(aURI, aDestination, IntegrityViolationReason::Invalid_manifest); if (mEnforce) { MOZ_LOG_FMT( gWaictLog, LogLevel::Warning, "IntegrityPolicyWAICT::MaybeCheckResourceIntegrity: Manifest not " "valid, enforce mode - blocking"); return false; } MOZ_LOG_FMT( gWaictLog, LogLevel::Info, "IntegrityPolicyWAICT::MaybeCheckResourceIntegrity: Manifest not " "valid, audit mode - proceeding"); return true; } if (!mHashes.IsEmpty()) { nsAutoCString spec; nsresult rv = aURI->GetSpec(spec); if (NS_SUCCEEDED(rv)) { if (auto hashValue = mHashes.Lookup(spec)) { if (*hashValue != aHash) { MOZ_LOG_FMT(gWaictLog, LogLevel::Warning, "IntegrityPolicyWAICT::MaybeCheckResourceIntegrity: " "Wrong hash for URL " "({} != {})", *hashValue, nsCString(aHash)); nsCString spec = aURI->GetSpecOrDefault(); nsTArray params = {NS_ConvertUTF8toUTF16(spec), NS_ConvertUTF8toUTF16(*hashValue), NS_ConvertUTF8toUTF16(aHash)}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTHashMismatch", params); ReportViolation(aURI, aDestination, IntegrityViolationReason::No_manifest_match); return !mEnforce; } MOZ_LOG_FMT( gWaictLog, LogLevel::Info, "IntegrityPolicyWAICT::MaybeCheckResourceIntegrity: Correct hash " "(URL-based)"); return true; } } } if (!mAnyHashes.IsEmpty()) { if (mAnyHashes.Contains(aHash)) { MOZ_LOG_FMT(gWaictLog, LogLevel::Info, "IntegrityPolicyWAICT::MaybeCheckResourceIntegrity: Hash " "found in any_hashes"); return true; } } MOZ_LOG_FMT(gWaictLog, LogLevel::Debug, "IntegrityPolicyWAICT::MaybeCheckResourceIntegrity: Hash not " "found in either " "lookup"); nsCString spec = aURI->GetSpecOrDefault(); nsTArray params = {NS_ConvertUTF8toUTF16(spec), NS_ConvertUTF8toUTF16(aHash)}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTResourceNotInManifest", params); ReportViolation(aURI, aDestination, IntegrityViolationReason::Missing_from_manifest); return !mEnforce; } /* static */ already_AddRefed IntegrityPolicyWAICT::Create( Document* aDocument, const nsACString& aHeader) { if (!StaticPrefs::security_waict_enabled()) { return nullptr; } if (aHeader.IsEmpty()) { return nullptr; } RefPtr policy = new IntegrityPolicyWAICT(aDocument); // We can't propagate the error here, because we would never flush // the console messages. if (NS_SUCCEEDED(policy->ParseHeader(aHeader))) { policy->FetchManifest(); } return policy.forget(); } nsresult IntegrityPolicyWAICT::ParseHeader(const nsACString& aHeader) { nsCOMPtr sfv = net::GetSFVService(); if (!sfv) { return NS_ERROR_FAILURE; } nsCOMPtr dict; nsresult rv = sfv->ParseDictionary(aHeader, getter_AddRefs(dict)); if (NS_FAILED(rv)) { nsTArray params = {NS_ConvertUTF8toUTF16(aHeader)}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTHeaderParseError", params); return rv; } auto destinationsResult = IntegrityPolicy::ParseDestinations(dict, /* aIsWAICT */ true); if (destinationsResult.isErr()) { nsTArray params = {NS_ConvertUTF8toUTF16(aHeader), u"destinations"_ns}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTHeaderFieldParseError", params); return destinationsResult.unwrapErr(); } mDestinations = destinationsResult.unwrap(); auto endpointsResult = IntegrityPolicy::ParseEndpoints(dict); if (endpointsResult.isErr()) { nsTArray params = {NS_ConvertUTF8toUTF16(aHeader), u"endpoints"_ns}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTHeaderFieldParseError", params); return endpointsResult.unwrapErr(); } mEndpoints = endpointsResult.unwrap(); auto maxAgeResult = waict::ParseMaxAge(dict); if (maxAgeResult.isErr()) { nsTArray params = {NS_ConvertUTF8toUTF16(aHeader), u"max-age"_ns}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTHeaderFieldParseError", params); return rv; } mMaxAge = maxAgeResult.unwrap(); auto modeResult = waict::ParseMode(dict); if (modeResult.isErr()) { nsTArray params = {NS_ConvertUTF8toUTF16(aHeader), u"mode"_ns}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTHeaderFieldParseError", params); return rv; } mEnforce = modeResult.unwrap() == waict::WaictMode::Enforce; // Make sure this is the last step. We use the existence of the manifest URL // as a trigger to activate WAICT. auto manifestURLResult = waict::ParseManifest(dict); if (manifestURLResult.isErr()) { nsTArray params = {NS_ConvertUTF8toUTF16(aHeader)}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTHeaderManifestParseError", params); return rv; } mManifestURL = manifestURLResult.unwrap(); return NS_OK; } static bool ValidateHashValue(const nsACString& aHash) { if (!nsCSPParser::isValidBase64Value(NS_ConvertUTF8toUTF16((aHash)))) { return false; } if (aHash.Length() != 43 && aHash.Length() != 44) { return false; } if (aHash.Length() == 44 && aHash[43] != '=') { return false; } if (aHash.Length() == 43 && aHash.Contains('=')) { return false; } return true; } IntegrityPolicyWAICT::ManifestValidationStatus IntegrityPolicyWAICT::ValidateManifest(const nsACString& aManifestJSON, WAICTManifest& aOutManifest, IntegrityPolicyWAICT* aPolicy) { if (aManifestJSON.IsEmpty() || !aOutManifest.Init(aManifestJSON)) { if (aPolicy) { aPolicy->ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTManifestJSONParseError", {}); } return ManifestValidationStatus::InvalidJSON; } bool hasHashes = aOutManifest.mHashes.WasPassed() && !aOutManifest.mHashes.Value().Entries().IsEmpty(); bool hasAnyHashes = aOutManifest.mAny_hashes.WasPassed() && !aOutManifest.mAny_hashes.Value().IsEmpty(); if (!hasHashes && !hasAnyHashes) { return ManifestValidationStatus::MissingHashes; } if (hasHashes) { for (const auto& entry : aOutManifest.mHashes.Value().Entries()) { if (entry.mKey.IsEmpty() || !ValidateHashValue(entry.mValue)) { if (aPolicy) { nsTArray params = {NS_ConvertUTF8toUTF16(entry.mKey), NS_ConvertUTF8toUTF16(entry.mValue)}; aPolicy->ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTManifestInvalidHash", params); } return ManifestValidationStatus::InvalidHashFormat; } } } if (hasAnyHashes) { for (const auto& hash : aOutManifest.mAny_hashes.Value()) { if (!ValidateHashValue(hash)) { if (aPolicy) { nsTArray params = {NS_ConvertUTF8toUTF16(hash)}; aPolicy->ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTManifestInvalidAnyHash", params); } return ManifestValidationStatus::InvalidHashFormat; } } } return ManifestValidationStatus::OK; } NS_IMETHODIMP IntegrityPolicyWAICT::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* context, nsresult aStatus, uint32_t aDataLen, const uint8_t* aData) { MOZ_LOG_FMT(gWaictLog, LogLevel::Debug, "IntegrityPolicyWAICT::OnStreamComplete: dataLen = {}", aDataLen); if (NS_FAILED(aStatus)) { ResolvePromiseInvalidManifest(); return NS_OK; } nsDependentCSubstring data(reinterpret_cast(aData), aDataLen); WAICTManifest manifest; ManifestValidationStatus status = ValidateManifest(data, manifest, this); if (status != ManifestValidationStatus::OK) { MOZ_LOG_FMT(gWaictLog, LogLevel::Warning, "Failed to validate WAICT manifest, error= {}", static_cast(status)); ResolvePromiseInvalidManifest(); return NS_OK; } MOZ_LOG_FMT(gWaictLog, LogLevel::Debug, "Manifest validation successful"); if (StaticPrefs::security_waict_downgrade_protection_enable() && mEnforce && mDocumentURI) { if (RefPtr doc = do_QueryReferent(mDocument)) { if (WindowGlobalChild* wgc = doc->GetWindowGlobalChild()) { wgc->SendSetSiteIntegrityProtected(WrapNotNull(mDocumentURI), mMaxAge); } } } if (manifest.mHashes.WasPassed()) { MOZ_ASSERT(mHashes.IsEmpty()); nsCOMPtr uri; nsAutoCString spec; for (const auto& entry : manifest.mHashes.Value().Entries()) { if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), entry.mKey, nullptr, mDocumentURI)) || NS_FAILED(uri->GetSpec(spec))) { nsTArray params = {NS_ConvertUTF8toUTF16(entry.mKey)}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTManifestInvalidURL", params); ResolvePromiseInvalidManifest(); return NS_OK; } mHashes.InsertOrUpdate(spec, entry.mValue); } } if (manifest.mAny_hashes.WasPassed()) { MOZ_ASSERT(mAnyHashes.IsEmpty()); for (const auto& hash : manifest.mAny_hashes.Value()) { mAnyHashes.Insert(hash); } } mManifestValid = true; mPromise->Resolve(true, __func__); return NS_OK; } void IntegrityPolicyWAICT::ResolvePromiseInvalidManifest() { mManifestValid = false; mPromise->Resolve(true, __func__); } void IntegrityPolicyWAICT::FetchManifest() { MOZ_LOG_FMT(gWaictLog, LogLevel::Debug, "IntegrityPolicyWAICT::FetchManifest: mManifestURL={}", mManifestURL.get()); MOZ_ASSERT(!mPromise); mPromise = MakeRefPtr(__func__); nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), mManifestURL, nullptr, mDocumentURI); if (NS_FAILED(rv)) { nsTArray params = {NS_ConvertUTF8toUTF16(mManifestURL)}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTManifestFetchURLParseError", params); ResolvePromiseInvalidManifest(); return; } nsCOMPtr loader; rv = NS_NewStreamLoader( getter_AddRefs(loader), uri, this, mPrincipal, nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, nsIContentPolicy::TYPE_OTHER, /* aLoadGroup */ nullptr, /* aCallbacks */ nullptr, /* aLoadFlags */ nsIRequest::LOAD_BACKGROUND); if (NS_FAILED(rv)) { nsTArray params = {NS_ConvertUTF8toUTF16(mManifestURL)}; ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns, "WAICTManifestFetchError", params); ResolvePromiseInvalidManifest(); } } void IntegrityPolicyWAICT::FlushConsoleMessages() { mQueueUpMessages = false; RefPtr doc = do_QueryReferent(mDocument); if (NS_WARN_IF(!doc)) { mConsoleMsgQueue.Clear(); return; } for (const auto& elem : mConsoleMsgQueue) { nsContentUtils::ReportToConsole(elem.mErrorFlags, elem.mCategory, doc, nsContentUtils::eSECURITY_PROPERTIES, elem.mMessageName.get(), elem.mParams); } mConsoleMsgQueue.Clear(); } void IntegrityPolicyWAICT::ReportMessage(uint32_t aErrorFlags, const nsACString& aCategory, const char* aMessageName, const nsTArray& aParams) { if (mQueueUpMessages) { ConsoleMsgQueueElem& elem = *mConsoleMsgQueue.AppendElement(); elem.mErrorFlags = aErrorFlags; elem.mCategory = aCategory; elem.mMessageName = nsCString(aMessageName); elem.mParams = aParams.Clone(); return; } nsCOMPtr doc = do_QueryReferent(mDocument); if (NS_WARN_IF(!doc)) { return; } nsContentUtils::ReportToConsole(aErrorFlags, aCategory, doc, nsContentUtils::eSECURITY_PROPERTIES, aMessageName, aParams); } void IntegrityPolicyWAICT::ReportViolation( nsIURI* aURI, IntegrityPolicy::DestinationType aDestination, IntegrityViolationReason aReason) const { nsCOMPtr doc = do_QueryReferent(mDocument); if (!doc) { return; } nsPIDOMWindowInner* window = doc->GetInnerWindow(); if (NS_WARN_IF(!window)) { return; } nsCOMPtr global = window->AsGlobal(); if (NS_WARN_IF(!mDocumentURI)) { return; } nsAutoCString documentURL; ReportingUtils::StripURL(mDocumentURI, documentURL); NS_ConvertUTF8toUTF16 documentURLUTF16(documentURL); nsAutoCString blockedURL; ReportingUtils::StripURL(aURI, 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; } for (const nsCString& endpoint : mEndpoints) { RefPtr body = new IntegrityViolationReportBody(global, documentURL, blockedURL, destination, !mEnforce, Nullable(aReason)); ReportingUtils::Report(global, nsGkAtoms::integrity_violation, NS_ConvertUTF8toUTF16(endpoint), documentURLUTF16, body); } } } // namespace mozilla::dom