Differential Revision: https://phabricator.services.mozilla.com/D289049
377 lines
14 KiB
C++
377 lines
14 KiB
C++
/* 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<void*>(aLoadInfo));
|
||
|
||
*aDecision = nsIContentPolicy::ACCEPT;
|
||
|
||
if (!StaticPrefs::security_integrity_policy_enabled()) {
|
||
LOG("ShouldLoad: [{}] Integrity policy is disabled",
|
||
static_cast<void*>(aLoadInfo));
|
||
return NS_OK;
|
||
}
|
||
|
||
if (!aContentLocation) {
|
||
LOG("ShouldLoad: [{}] No content location", static_cast<void*>(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<void*>(aLoadInfo),
|
||
static_cast<uint8_t>(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<void*>(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<RequestMode> 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<void*>(aLoadInfo), aContentLocation->GetSpecOrDefault(),
|
||
static_cast<uint8_t>(*destination), static_cast<uint8_t>(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<void*>(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<void*>(aLoadInfo));
|
||
return false;
|
||
}
|
||
|
||
// We only support integrity policy for documents so far.
|
||
nsCOMPtr<nsIPolicyContainer> policyContainer =
|
||
aLoadInfo->GetPolicyContainer();
|
||
if (!policyContainer) {
|
||
LOG("ShouldLoad: [{}] No policy container", static_cast<void*>(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<IntegrityPolicy> 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<void*>(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<nsString, 1> 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<nsISupports> loadingContext = aLoadInfo->GetLoadingContext();
|
||
RefPtr<Document> doc;
|
||
if (nsCOMPtr<nsINode> node = do_QueryInterface(loadingContext)) {
|
||
doc = node->OwnerDoc();
|
||
} else if (nsCOMPtr<nsPIDOMWindowOuter> window =
|
||
do_QueryInterface(loadingContext)) {
|
||
doc = window->GetDoc();
|
||
}
|
||
|
||
if (NS_WARN_IF(!doc)) {
|
||
return;
|
||
}
|
||
|
||
nsPIDOMWindowInner* window = doc->GetInnerWindow();
|
||
if (NS_WARN_IF(!window)) {
|
||
return;
|
||
}
|
||
nsCOMPtr<nsIGlobalObject> 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<nsIURI> 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<nsCString> enforcementEndpoints;
|
||
nsTArray<nsCString> 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<IntegrityViolationReportBody> 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<IntegrityViolationReportBody> 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
|