Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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::WAICTManifestLoadedPromise>
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<nsString> 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<nsString> 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> IntegrityPolicyWAICT::Create(
Document* aDocument, const nsACString& aHeader) {
if (!StaticPrefs::security_waict_enabled()) {
return nullptr;
}
if (aHeader.IsEmpty()) {
return nullptr;
}
RefPtr<IntegrityPolicyWAICT> 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<nsISFVService> sfv = net::GetSFVService();
if (!sfv) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsISFVDictionary> dict;
nsresult rv = sfv->ParseDictionary(aHeader, getter_AddRefs(dict));
if (NS_FAILED(rv)) {
nsTArray<nsString> params = {NS_ConvertUTF8toUTF16(aHeader)};
ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns,
"WAICTHeaderParseError", params);
return rv;
}
auto destinationsResult =
IntegrityPolicy::ParseDestinations(dict, /* aIsWAICT */ true);
if (destinationsResult.isErr()) {
nsTArray<nsString> 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<nsString> 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<nsString> 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<nsString> 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<nsString> 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<nsString> 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<nsString> 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<const char*>(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<uint8_t>(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<Document> doc = do_QueryReferent(mDocument)) {
if (WindowGlobalChild* wgc = doc->GetWindowGlobalChild()) {
wgc->SendSetSiteIntegrityProtected(WrapNotNull(mDocumentURI), mMaxAge);
}
}
}
if (manifest.mHashes.WasPassed()) {
MOZ_ASSERT(mHashes.IsEmpty());
nsCOMPtr<nsIURI> 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<nsString> 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<WAICTManifestLoadedPromise::Private>(__func__);
nsCOMPtr<nsIURI> uri;
nsresult rv =
NS_NewURI(getter_AddRefs(uri), mManifestURL, nullptr, mDocumentURI);
if (NS_FAILED(rv)) {
nsTArray<nsString> params = {NS_ConvertUTF8toUTF16(mManifestURL)};
ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns,
"WAICTManifestFetchURLParseError", params);
ResolvePromiseInvalidManifest();
return;
}
nsCOMPtr<nsIStreamLoader> 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<nsString> params = {NS_ConvertUTF8toUTF16(mManifestURL)};
ReportMessage(nsIScriptError::errorFlag, "WAICT"_ns,
"WAICTManifestFetchError", params);
ResolvePromiseInvalidManifest();
}
}
void IntegrityPolicyWAICT::FlushConsoleMessages() {
mQueueUpMessages = false;
RefPtr<Document> 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<nsString>& aParams) {
if (mQueueUpMessages) {
ConsoleMsgQueueElem& elem = *mConsoleMsgQueue.AppendElement();
elem.mErrorFlags = aErrorFlags;
elem.mCategory = aCategory;
elem.mMessageName = nsCString(aMessageName);
elem.mParams = aParams.Clone();
return;
}
nsCOMPtr<Document> 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<Document> doc = do_QueryReferent(mDocument);
if (!doc) {
return;
}
nsPIDOMWindowInner* window = doc->GetInnerWindow();
if (NS_WARN_IF(!window)) {
return;
}
nsCOMPtr<nsIGlobalObject> 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<IntegrityViolationReportBody> body =
new IntegrityViolationReportBody(global, documentURL, blockedURL,
destination, !mEnforce,
Nullable(aReason));
ReportingUtils::Report(global, nsGkAtoms::integrity_violation,
NS_ConvertUTF8toUTF16(endpoint), documentURLUTF16,
body);
}
}
} // namespace mozilla::dom