mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 22:55:23 +00:00
1473 lines
54 KiB
C++
1473 lines
54 KiB
C++
/* -*- 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 "nsCOMPtr.h"
|
||
#include "nsContentPolicyUtils.h"
|
||
#include "nsContentUtils.h"
|
||
#include "nsCSPContext.h"
|
||
#include "nsCSPParser.h"
|
||
#include "nsCSPService.h"
|
||
#include "nsError.h"
|
||
#include "nsIAsyncVerifyRedirectCallback.h"
|
||
#include "nsIClassInfoImpl.h"
|
||
#include "nsIDocShell.h"
|
||
#include "nsIDocShellTreeItem.h"
|
||
#include "nsIDOMHTMLDocument.h"
|
||
#include "nsIDOMHTMLElement.h"
|
||
#include "nsIDOMNode.h"
|
||
#include "nsIHttpChannel.h"
|
||
#include "nsIInterfaceRequestor.h"
|
||
#include "nsIInterfaceRequestorUtils.h"
|
||
#include "nsIObjectInputStream.h"
|
||
#include "nsIObjectOutputStream.h"
|
||
#include "nsIObserver.h"
|
||
#include "nsIObserverService.h"
|
||
#include "nsIStringStream.h"
|
||
#include "nsIUploadChannel.h"
|
||
#include "nsIScriptError.h"
|
||
#include "nsIWebNavigation.h"
|
||
#include "nsMimeTypes.h"
|
||
#include "nsNetUtil.h"
|
||
#include "nsIContentPolicy.h"
|
||
#include "nsSupportsPrimitives.h"
|
||
#include "nsThreadUtils.h"
|
||
#include "nsString.h"
|
||
#include "nsScriptSecurityManager.h"
|
||
#include "nsStringStream.h"
|
||
#include "mozilla/Logging.h"
|
||
#include "mozilla/dom/CSPReportBinding.h"
|
||
#include "mozilla/dom/CSPDictionariesBinding.h"
|
||
#include "mozilla/net/ReferrerPolicy.h"
|
||
#include "nsINetworkInterceptController.h"
|
||
|
||
using namespace mozilla;
|
||
|
||
static LogModule*
|
||
GetCspContextLog()
|
||
{
|
||
static LazyLogModule gCspContextPRLog("CSPContext");
|
||
return gCspContextPRLog;
|
||
}
|
||
|
||
#define CSPCONTEXTLOG(args) MOZ_LOG(GetCspContextLog(), mozilla::LogLevel::Debug, args)
|
||
#define CSPCONTEXTLOGENABLED() MOZ_LOG_TEST(GetCspContextLog(), mozilla::LogLevel::Debug)
|
||
|
||
static const uint32_t CSP_CACHE_URI_CUTOFF_SIZE = 512;
|
||
|
||
/**
|
||
* Creates a key for use in the ShouldLoad cache.
|
||
* Looks like: <uri>!<nsIContentPolicy::LOAD_TYPE>
|
||
*/
|
||
nsresult
|
||
CreateCacheKey_Internal(nsIURI* aContentLocation,
|
||
nsContentPolicyType aContentType,
|
||
nsACString& outCacheKey)
|
||
{
|
||
if (!aContentLocation) {
|
||
return NS_ERROR_FAILURE;
|
||
}
|
||
|
||
bool isDataScheme = false;
|
||
nsresult rv = aContentLocation->SchemeIs("data", &isDataScheme);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
outCacheKey.Truncate();
|
||
if (aContentType != nsIContentPolicy::TYPE_SCRIPT && isDataScheme) {
|
||
// For non-script data: URI, use ("data:", aContentType) as the cache key.
|
||
outCacheKey.Append(NS_LITERAL_CSTRING("data:"));
|
||
outCacheKey.AppendInt(aContentType);
|
||
return NS_OK;
|
||
}
|
||
|
||
nsAutoCString spec;
|
||
rv = aContentLocation->GetSpec(spec);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// Don't cache for a URI longer than the cutoff size.
|
||
if (spec.Length() <= CSP_CACHE_URI_CUTOFF_SIZE) {
|
||
outCacheKey.Append(spec);
|
||
outCacheKey.Append(NS_LITERAL_CSTRING("!"));
|
||
outCacheKey.AppendInt(aContentType);
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
/* ===== nsIContentSecurityPolicy impl ====== */
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::ShouldLoad(nsContentPolicyType aContentType,
|
||
nsIURI* aContentLocation,
|
||
nsIURI* aRequestOrigin,
|
||
nsISupports* aRequestContext,
|
||
const nsACString& aMimeTypeGuess,
|
||
nsISupports* aExtra,
|
||
int16_t* outDecision)
|
||
{
|
||
if (CSPCONTEXTLOGENABLED()) {
|
||
nsAutoCString spec;
|
||
aContentLocation->GetSpec(spec);
|
||
CSPCONTEXTLOG(("nsCSPContext::ShouldLoad, aContentLocation: %s", spec.get()));
|
||
CSPCONTEXTLOG((">>>> aContentType: %d", aContentType));
|
||
}
|
||
|
||
bool isPreload = nsContentUtils::IsPreloadType(aContentType);
|
||
|
||
// Since we know whether we are dealing with a preload, we have to convert
|
||
// the internal policytype ot the external policy type before moving on.
|
||
// We still need to know if this is a worker so child-src can handle that
|
||
// case correctly.
|
||
aContentType = nsContentUtils::InternalContentPolicyTypeToExternalOrWorker(aContentType);
|
||
|
||
nsresult rv = NS_OK;
|
||
|
||
// This ShouldLoad function is called from nsCSPService::ShouldLoad,
|
||
// which already checked a number of things, including:
|
||
// * aContentLocation is not null; we can consume this without further checks
|
||
// * scheme is not a whitelisted scheme (about: chrome:, etc).
|
||
// * CSP is enabled
|
||
// * Content Type is not whitelisted (CSP Reports, TYPE_DOCUMENT, etc).
|
||
// * Fast Path for Apps
|
||
|
||
nsAutoCString cacheKey;
|
||
rv = CreateCacheKey_Internal(aContentLocation, aContentType, cacheKey);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
bool isCached = mShouldLoadCache.Get(cacheKey, outDecision);
|
||
if (isCached && cacheKey.Length() > 0) {
|
||
// this is cached, use the cached value.
|
||
return NS_OK;
|
||
}
|
||
|
||
// Default decision, CSP can revise it if there's a policy to enforce
|
||
*outDecision = nsIContentPolicy::ACCEPT;
|
||
|
||
// If the content type doesn't map to a CSP directive, there's nothing for
|
||
// CSP to do.
|
||
CSPDirective dir = CSP_ContentTypeToDirective(aContentType);
|
||
if (dir == nsIContentSecurityPolicy::NO_DIRECTIVE) {
|
||
return NS_OK;
|
||
}
|
||
|
||
nsAutoString nonce;
|
||
if (!isPreload) {
|
||
nsCOMPtr<nsIDOMHTMLElement> htmlElement = do_QueryInterface(aRequestContext);
|
||
if (htmlElement) {
|
||
rv = htmlElement->GetAttribute(NS_LITERAL_STRING("nonce"), nonce);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
}
|
||
}
|
||
|
||
// aExtra is only non-null if the channel got redirected.
|
||
bool wasRedirected = (aExtra != nullptr);
|
||
nsCOMPtr<nsIURI> originalURI = do_QueryInterface(aExtra);
|
||
|
||
bool permitted = permitsInternal(dir,
|
||
aContentLocation,
|
||
originalURI,
|
||
nonce,
|
||
wasRedirected,
|
||
isPreload,
|
||
false, // allow fallback to default-src
|
||
true, // send violation reports
|
||
true); // send blocked URI in violation reports
|
||
|
||
*outDecision = permitted ? nsIContentPolicy::ACCEPT
|
||
: nsIContentPolicy::REJECT_SERVER;
|
||
|
||
// Done looping, cache any relevant result
|
||
if (cacheKey.Length() > 0 && !isPreload) {
|
||
mShouldLoadCache.Put(cacheKey, *outDecision);
|
||
}
|
||
|
||
if (CSPCONTEXTLOGENABLED()) {
|
||
nsAutoCString spec;
|
||
aContentLocation->GetSpec(spec);
|
||
CSPCONTEXTLOG(("nsCSPContext::ShouldLoad, decision: %s, aContentLocation: %s", *outDecision > 0 ? "load" : "deny", spec.get()));
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
bool
|
||
nsCSPContext::permitsInternal(CSPDirective aDir,
|
||
nsIURI* aContentLocation,
|
||
nsIURI* aOriginalURI,
|
||
const nsAString& aNonce,
|
||
bool aWasRedirected,
|
||
bool aIsPreload,
|
||
bool aSpecific,
|
||
bool aSendViolationReports,
|
||
bool aSendContentLocationInViolationReports)
|
||
{
|
||
bool permits = true;
|
||
|
||
nsAutoString violatedDirective;
|
||
for (uint32_t p = 0; p < mPolicies.Length(); p++) {
|
||
|
||
// According to the W3C CSP spec, frame-ancestors checks are ignored for
|
||
// report-only policies (when "monitoring").
|
||
if (aDir == nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE &&
|
||
mPolicies[p]->getReportOnlyFlag()) {
|
||
continue;
|
||
}
|
||
|
||
if (!mPolicies[p]->permits(aDir,
|
||
aContentLocation,
|
||
aNonce,
|
||
aWasRedirected,
|
||
aSpecific,
|
||
violatedDirective)) {
|
||
// If the policy is violated and not report-only, reject the load and
|
||
// report to the console
|
||
if (!mPolicies[p]->getReportOnlyFlag()) {
|
||
CSPCONTEXTLOG(("nsCSPContext::permitsInternal, false"));
|
||
permits = false;
|
||
}
|
||
|
||
// Do not send a report or notify observers if this is a preload - the
|
||
// decision may be wrong due to the inability to get the nonce, and will
|
||
// incorrectly fail the unit tests.
|
||
if (!aIsPreload && aSendViolationReports) {
|
||
this->AsyncReportViolation((aSendContentLocationInViolationReports ?
|
||
aContentLocation : nullptr),
|
||
aOriginalURI, /* in case of redirect originalURI is not null */
|
||
violatedDirective,
|
||
p, /* policy index */
|
||
EmptyString(), /* no observer subject */
|
||
EmptyString(), /* no source file */
|
||
EmptyString(), /* no script sample */
|
||
0); /* no line number */
|
||
}
|
||
}
|
||
}
|
||
|
||
return permits;
|
||
}
|
||
|
||
|
||
|
||
/* ===== nsISupports implementation ========== */
|
||
|
||
NS_IMPL_CLASSINFO(nsCSPContext,
|
||
nullptr,
|
||
nsIClassInfo::MAIN_THREAD_ONLY,
|
||
NS_CSPCONTEXT_CID)
|
||
|
||
NS_IMPL_ISUPPORTS_CI(nsCSPContext,
|
||
nsIContentSecurityPolicy,
|
||
nsISerializable)
|
||
|
||
nsCSPContext::nsCSPContext()
|
||
: mInnerWindowID(0)
|
||
, mLoadingContext(nullptr)
|
||
, mLoadingPrincipal(nullptr)
|
||
, mQueueUpMessages(true)
|
||
{
|
||
CSPCONTEXTLOG(("nsCSPContext::nsCSPContext"));
|
||
}
|
||
|
||
nsCSPContext::~nsCSPContext()
|
||
{
|
||
CSPCONTEXTLOG(("nsCSPContext::~nsCSPContext"));
|
||
for (uint32_t i = 0; i < mPolicies.Length(); i++) {
|
||
delete mPolicies[i];
|
||
}
|
||
mShouldLoadCache.Clear();
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::GetPolicy(uint32_t aIndex, nsAString& outStr)
|
||
{
|
||
if (aIndex >= mPolicies.Length()) {
|
||
return NS_ERROR_ILLEGAL_VALUE;
|
||
}
|
||
mPolicies[aIndex]->toString(outStr);
|
||
return NS_OK;
|
||
}
|
||
|
||
const nsCSPPolicy*
|
||
nsCSPContext::GetPolicy(uint32_t aIndex)
|
||
{
|
||
if (aIndex >= mPolicies.Length()) {
|
||
return nullptr;
|
||
}
|
||
return mPolicies[aIndex];
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::GetPolicyCount(uint32_t *outPolicyCount)
|
||
{
|
||
*outPolicyCount = mPolicies.Length();
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::GetUpgradeInsecureRequests(bool *outUpgradeRequest)
|
||
{
|
||
*outUpgradeRequest = false;
|
||
for (uint32_t i = 0; i < mPolicies.Length(); i++) {
|
||
if (mPolicies[i]->hasDirective(nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)) {
|
||
*outUpgradeRequest = true;
|
||
return NS_OK;
|
||
}
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::GetBlockAllMixedContent(bool *outBlockAllMixedContent)
|
||
{
|
||
*outBlockAllMixedContent = false;
|
||
for (uint32_t i = 0; i < mPolicies.Length(); i++) {
|
||
if (mPolicies[i]->hasDirective(nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)) {
|
||
*outBlockAllMixedContent = true;
|
||
return NS_OK;
|
||
}
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::GetReferrerPolicy(uint32_t* outPolicy, bool* outIsSet)
|
||
{
|
||
*outIsSet = false;
|
||
*outPolicy = mozilla::net::RP_Default;
|
||
nsAutoString refpol;
|
||
mozilla::net::ReferrerPolicy previousPolicy = mozilla::net::RP_Default;
|
||
for (uint32_t i = 0; i < mPolicies.Length(); i++) {
|
||
mPolicies[i]->getReferrerPolicy(refpol);
|
||
// an empty string in refpol means it wasn't set (that's the default in
|
||
// nsCSPPolicy).
|
||
if (!refpol.IsEmpty()) {
|
||
// if there are two policies that specify a referrer policy, then they
|
||
// must agree or the employed policy is no-referrer.
|
||
uint32_t currentPolicy = mozilla::net::ReferrerPolicyFromString(refpol);
|
||
if (*outIsSet && previousPolicy != currentPolicy) {
|
||
*outPolicy = mozilla::net::RP_No_Referrer;
|
||
return NS_OK;
|
||
}
|
||
|
||
*outPolicy = currentPolicy;
|
||
*outIsSet = true;
|
||
}
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::AppendPolicy(const nsAString& aPolicyString,
|
||
bool aReportOnly,
|
||
bool aDeliveredViaMetaTag)
|
||
{
|
||
CSPCONTEXTLOG(("nsCSPContext::AppendPolicy: %s",
|
||
NS_ConvertUTF16toUTF8(aPolicyString).get()));
|
||
|
||
// Use the mSelfURI from setRequestContext, see bug 991474
|
||
NS_ASSERTION(mSelfURI, "mSelfURI required for AppendPolicy, but not set");
|
||
nsCSPPolicy* policy = nsCSPParser::parseContentSecurityPolicy(aPolicyString, mSelfURI,
|
||
aReportOnly, this,
|
||
aDeliveredViaMetaTag);
|
||
if (policy) {
|
||
mPolicies.AppendElement(policy);
|
||
// reset cache since effective policy changes
|
||
mShouldLoadCache.Clear();
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::GetAllowsEval(bool* outShouldReportViolation,
|
||
bool* outAllowsEval)
|
||
{
|
||
*outShouldReportViolation = false;
|
||
*outAllowsEval = true;
|
||
|
||
for (uint32_t i = 0; i < mPolicies.Length(); i++) {
|
||
if (!mPolicies[i]->allows(nsIContentPolicy::TYPE_SCRIPT,
|
||
CSP_UNSAFE_EVAL, EmptyString())) {
|
||
// policy is violated: must report the violation and allow the inline
|
||
// script if the policy is report-only.
|
||
*outShouldReportViolation = true;
|
||
if (!mPolicies[i]->getReportOnlyFlag()) {
|
||
*outAllowsEval = false;
|
||
}
|
||
}
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
// Helper function to report inline violations
|
||
void
|
||
nsCSPContext::reportInlineViolation(nsContentPolicyType aContentType,
|
||
const nsAString& aNonce,
|
||
const nsAString& aContent,
|
||
const nsAString& aViolatedDirective,
|
||
uint32_t aViolatedPolicyIndex, // TODO, use report only flag for that
|
||
uint32_t aLineNumber)
|
||
{
|
||
nsString observerSubject;
|
||
// if the nonce is non empty, then we report the nonce error, otherwise
|
||
// let's report the hash error; no need to report the unsafe-inline error
|
||
// anymore.
|
||
if (!aNonce.IsEmpty()) {
|
||
observerSubject = (aContentType == nsIContentPolicy::TYPE_SCRIPT)
|
||
? NS_LITERAL_STRING(SCRIPT_NONCE_VIOLATION_OBSERVER_TOPIC)
|
||
: NS_LITERAL_STRING(STYLE_NONCE_VIOLATION_OBSERVER_TOPIC);
|
||
}
|
||
else {
|
||
observerSubject = (aContentType == nsIContentPolicy::TYPE_SCRIPT)
|
||
? NS_LITERAL_STRING(SCRIPT_HASH_VIOLATION_OBSERVER_TOPIC)
|
||
: NS_LITERAL_STRING(STYLE_HASH_VIOLATION_OBSERVER_TOPIC);
|
||
}
|
||
|
||
nsCOMPtr<nsISupportsCString> selfICString(do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID));
|
||
if (selfICString) {
|
||
selfICString->SetData(nsDependentCString("self"));
|
||
}
|
||
nsCOMPtr<nsISupports> selfISupports(do_QueryInterface(selfICString));
|
||
|
||
// use selfURI as the sourceFile
|
||
nsAutoCString sourceFile;
|
||
if (mSelfURI) {
|
||
mSelfURI->GetSpec(sourceFile);
|
||
}
|
||
|
||
nsAutoString codeSample(aContent);
|
||
// cap the length of the script sample at 40 chars
|
||
if (codeSample.Length() > 40) {
|
||
codeSample.Truncate(40);
|
||
codeSample.AppendLiteral("...");
|
||
}
|
||
AsyncReportViolation(selfISupports, // aBlockedContentSource
|
||
mSelfURI, // aOriginalURI
|
||
aViolatedDirective, // aViolatedDirective
|
||
aViolatedPolicyIndex, // aViolatedPolicyIndex
|
||
observerSubject, // aObserverSubject
|
||
NS_ConvertUTF8toUTF16(sourceFile), // aSourceFile
|
||
codeSample, // aScriptSample
|
||
aLineNumber); // aLineNum
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::GetAllowsInline(nsContentPolicyType aContentType,
|
||
const nsAString& aNonce,
|
||
const nsAString& aContent,
|
||
uint32_t aLineNumber,
|
||
bool* outAllowsInline)
|
||
{
|
||
*outAllowsInline = true;
|
||
|
||
MOZ_ASSERT(aContentType == nsContentUtils::InternalContentPolicyTypeToExternal(aContentType),
|
||
"We should only see external content policy types here.");
|
||
|
||
if (aContentType != nsIContentPolicy::TYPE_SCRIPT &&
|
||
aContentType != nsIContentPolicy::TYPE_STYLESHEET) {
|
||
MOZ_ASSERT(false, "can only allow inline for script or style");
|
||
return NS_OK;
|
||
}
|
||
|
||
// always iterate all policies, otherwise we might not send out all reports
|
||
for (uint32_t i = 0; i < mPolicies.Length(); i++) {
|
||
bool allowed =
|
||
mPolicies[i]->allows(aContentType, CSP_UNSAFE_INLINE, EmptyString()) ||
|
||
mPolicies[i]->allows(aContentType, CSP_NONCE, aNonce) ||
|
||
mPolicies[i]->allows(aContentType, CSP_HASH, aContent);
|
||
|
||
if (!allowed) {
|
||
// policy is violoated: deny the load unless policy is report only and
|
||
// report the violation.
|
||
if (!mPolicies[i]->getReportOnlyFlag()) {
|
||
*outAllowsInline = false;
|
||
}
|
||
nsAutoString violatedDirective;
|
||
mPolicies[i]->getDirectiveStringForContentType(aContentType, violatedDirective);
|
||
reportInlineViolation(aContentType,
|
||
aNonce,
|
||
aContent,
|
||
violatedDirective,
|
||
i,
|
||
aLineNumber);
|
||
}
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
|
||
/**
|
||
* Reduces some code repetition for the various logging situations in
|
||
* LogViolationDetails.
|
||
*
|
||
* Call-sites for the eval/inline checks recieve two return values: allows
|
||
* and violates. Based on those, they must choose whether to call
|
||
* LogViolationDetails or not. Policies that are report-only allow the
|
||
* loads/compilations but violations should still be reported. Not all
|
||
* policies in this nsIContentSecurityPolicy instance will be violated,
|
||
* which is why we must check allows() again here.
|
||
*
|
||
* Note: This macro uses some parameters from its caller's context:
|
||
* p, mPolicies, this, aSourceFile, aScriptSample, aLineNum, selfISupports
|
||
*
|
||
* @param violationType: the VIOLATION_TYPE_* constant (partial symbol)
|
||
* such as INLINE_SCRIPT
|
||
* @param contentPolicyType: a constant from nsIContentPolicy such as TYPE_STYLESHEET
|
||
* @param nonceOrHash: for NONCE and HASH violations, it's the nonce or content
|
||
* string. For other violations, it is an empty string.
|
||
* @param keyword: the keyword corresponding to violation (UNSAFE_INLINE for most)
|
||
* @param observerTopic: the observer topic string to send with the CSP
|
||
* observer notifications.
|
||
*/
|
||
#define CASE_CHECK_AND_REPORT(violationType, contentPolicyType, nonceOrHash, \
|
||
keyword, observerTopic) \
|
||
case nsIContentSecurityPolicy::VIOLATION_TYPE_ ## violationType : \
|
||
PR_BEGIN_MACRO \
|
||
if (!mPolicies[p]->allows(nsIContentPolicy::TYPE_ ## contentPolicyType, \
|
||
keyword, nonceOrHash)) \
|
||
{ \
|
||
nsAutoString violatedDirective; \
|
||
mPolicies[p]->getDirectiveStringForContentType( \
|
||
nsIContentPolicy::TYPE_ ## contentPolicyType, \
|
||
violatedDirective); \
|
||
this->AsyncReportViolation(selfISupports, nullptr, violatedDirective, p, \
|
||
NS_LITERAL_STRING(observerTopic), \
|
||
aSourceFile, aScriptSample, aLineNum); \
|
||
} \
|
||
PR_END_MACRO; \
|
||
break
|
||
|
||
/**
|
||
* For each policy, log any violation on the Error Console and send a report
|
||
* if a report-uri is present in the policy
|
||
*
|
||
* @param aViolationType
|
||
* one of the VIOLATION_TYPE_* constants, e.g. inline-script or eval
|
||
* @param aSourceFile
|
||
* name of the source file containing the violation (if available)
|
||
* @param aContentSample
|
||
* sample of the violating content (to aid debugging)
|
||
* @param aLineNum
|
||
* source line number of the violation (if available)
|
||
* @param aNonce
|
||
* (optional) If this is a nonce violation, include the nonce so we can
|
||
* recheck to determine which policies were violated and send the
|
||
* appropriate reports.
|
||
* @param aContent
|
||
* (optional) If this is a hash violation, include contents of the inline
|
||
* resource in the question so we can recheck the hash in order to
|
||
* determine which policies were violated and send the appropriate
|
||
* reports.
|
||
*/
|
||
NS_IMETHODIMP
|
||
nsCSPContext::LogViolationDetails(uint16_t aViolationType,
|
||
const nsAString& aSourceFile,
|
||
const nsAString& aScriptSample,
|
||
int32_t aLineNum,
|
||
const nsAString& aNonce,
|
||
const nsAString& aContent)
|
||
{
|
||
for (uint32_t p = 0; p < mPolicies.Length(); p++) {
|
||
NS_ASSERTION(mPolicies[p], "null pointer in nsTArray<nsCSPPolicy>");
|
||
|
||
nsCOMPtr<nsISupportsCString> selfICString(do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID));
|
||
if (selfICString) {
|
||
selfICString->SetData(nsDependentCString("self"));
|
||
}
|
||
nsCOMPtr<nsISupports> selfISupports(do_QueryInterface(selfICString));
|
||
|
||
switch (aViolationType) {
|
||
CASE_CHECK_AND_REPORT(EVAL, SCRIPT, NS_LITERAL_STRING(""),
|
||
CSP_UNSAFE_EVAL, EVAL_VIOLATION_OBSERVER_TOPIC);
|
||
CASE_CHECK_AND_REPORT(INLINE_STYLE, STYLESHEET, NS_LITERAL_STRING(""),
|
||
CSP_UNSAFE_INLINE, INLINE_STYLE_VIOLATION_OBSERVER_TOPIC);
|
||
CASE_CHECK_AND_REPORT(INLINE_SCRIPT, SCRIPT, NS_LITERAL_STRING(""),
|
||
CSP_UNSAFE_INLINE, INLINE_SCRIPT_VIOLATION_OBSERVER_TOPIC);
|
||
CASE_CHECK_AND_REPORT(NONCE_SCRIPT, SCRIPT, aNonce,
|
||
CSP_UNSAFE_INLINE, SCRIPT_NONCE_VIOLATION_OBSERVER_TOPIC);
|
||
CASE_CHECK_AND_REPORT(NONCE_STYLE, STYLESHEET, aNonce,
|
||
CSP_UNSAFE_INLINE, STYLE_NONCE_VIOLATION_OBSERVER_TOPIC);
|
||
CASE_CHECK_AND_REPORT(HASH_SCRIPT, SCRIPT, aContent,
|
||
CSP_UNSAFE_INLINE, SCRIPT_HASH_VIOLATION_OBSERVER_TOPIC);
|
||
CASE_CHECK_AND_REPORT(HASH_STYLE, STYLESHEET, aContent,
|
||
CSP_UNSAFE_INLINE, STYLE_HASH_VIOLATION_OBSERVER_TOPIC);
|
||
|
||
default:
|
||
NS_ASSERTION(false, "LogViolationDetails with invalid type");
|
||
break;
|
||
}
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
#undef CASE_CHECK_AND_REPORT
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::SetRequestContext(nsIDOMDocument* aDOMDocument,
|
||
nsIPrincipal* aPrincipal)
|
||
{
|
||
NS_PRECONDITION(aDOMDocument || aPrincipal,
|
||
"Can't set context without doc or principal");
|
||
NS_ENSURE_ARG(aDOMDocument || aPrincipal);
|
||
|
||
if (aDOMDocument) {
|
||
nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDOMDocument);
|
||
mLoadingContext = do_GetWeakReference(doc);
|
||
mSelfURI = doc->GetDocumentURI();
|
||
mLoadingPrincipal = doc->NodePrincipal();
|
||
doc->GetReferrer(mReferrer);
|
||
mInnerWindowID = doc->InnerWindowID();
|
||
// the innerWindowID is not available for CSPs delivered through the
|
||
// header at the time setReqeustContext is called - let's queue up
|
||
// console messages until it becomes available, see flushConsoleMessages
|
||
mQueueUpMessages = !mInnerWindowID;
|
||
mCallingChannelLoadGroup = doc->GetDocumentLoadGroup();
|
||
|
||
// set the flag on the document for CSP telemetry
|
||
doc->SetHasCSP(true);
|
||
}
|
||
else {
|
||
NS_WARNING("No Document in SetRequestContext; can not query loadgroup; sending reports may fail.");
|
||
mLoadingPrincipal = aPrincipal;
|
||
mLoadingPrincipal->GetURI(getter_AddRefs(mSelfURI));
|
||
// if no document is available, then it also does not make sense to queue console messages
|
||
// sending messages to the browser conolse instead of the web console in that case.
|
||
mQueueUpMessages = false;
|
||
}
|
||
|
||
NS_ASSERTION(mSelfURI, "mSelfURI not available, can not translate 'self' into actual URI");
|
||
return NS_OK;
|
||
}
|
||
|
||
struct ConsoleMsgQueueElem {
|
||
nsXPIDLString mMsg;
|
||
nsString mSourceName;
|
||
nsString mSourceLine;
|
||
uint32_t mLineNumber;
|
||
uint32_t mColumnNumber;
|
||
uint32_t mSeverityFlag;
|
||
};
|
||
|
||
void
|
||
nsCSPContext::flushConsoleMessages()
|
||
{
|
||
// should flush messages even if doc is not available
|
||
nsCOMPtr<nsIDocument> doc = do_QueryReferent(mLoadingContext);
|
||
if (doc) {
|
||
mInnerWindowID = doc->InnerWindowID();
|
||
}
|
||
mQueueUpMessages = false;
|
||
|
||
for (uint32_t i = 0; i < mConsoleMsgQueue.Length(); i++) {
|
||
ConsoleMsgQueueElem &elem = mConsoleMsgQueue[i];
|
||
CSP_LogMessage(elem.mMsg, elem.mSourceName, elem.mSourceLine,
|
||
elem.mLineNumber, elem.mColumnNumber,
|
||
elem.mSeverityFlag, "CSP", mInnerWindowID);
|
||
}
|
||
mConsoleMsgQueue.Clear();
|
||
}
|
||
|
||
void
|
||
nsCSPContext::logToConsole(const char16_t* aName,
|
||
const char16_t** aParams,
|
||
uint32_t aParamsLength,
|
||
const nsAString& aSourceName,
|
||
const nsAString& aSourceLine,
|
||
uint32_t aLineNumber,
|
||
uint32_t aColumnNumber,
|
||
uint32_t aSeverityFlag)
|
||
{
|
||
// let's check if we have to queue up console messages
|
||
if (mQueueUpMessages) {
|
||
nsXPIDLString msg;
|
||
CSP_GetLocalizedStr(aName, aParams, aParamsLength, getter_Copies(msg));
|
||
ConsoleMsgQueueElem &elem = *mConsoleMsgQueue.AppendElement();
|
||
elem.mMsg = msg;
|
||
elem.mSourceName = PromiseFlatString(aSourceName);
|
||
elem.mSourceLine = PromiseFlatString(aSourceLine);
|
||
elem.mLineNumber = aLineNumber;
|
||
elem.mColumnNumber = aColumnNumber;
|
||
elem.mSeverityFlag = aSeverityFlag;
|
||
return;
|
||
}
|
||
CSP_LogLocalizedStr(aName, aParams, aParamsLength, aSourceName,
|
||
aSourceLine, aLineNumber, aColumnNumber,
|
||
aSeverityFlag, "CSP", mInnerWindowID);
|
||
}
|
||
|
||
/**
|
||
* Strip URI for reporting according to:
|
||
* http://www.w3.org/TR/CSP/#violation-reports
|
||
*
|
||
* @param aURI
|
||
* The uri to be stripped for reporting
|
||
* @param aProtectedResourcePrincipal
|
||
* The loadingPrincipal of the protected resource
|
||
* which is needed to enforce the SOP.
|
||
* @return ASCII serialization of the uri to be reported.
|
||
*/
|
||
void
|
||
StripURIForReporting(nsIURI* aURI,
|
||
nsIPrincipal* aProtectedResourcePrincipal,
|
||
nsACString& outStrippedURI)
|
||
{
|
||
// 1) If the origin of uri is a globally unique identifier (for example,
|
||
// aURI has a scheme of data, blob, or filesystem), then return the
|
||
// ASCII serialization of uri’s scheme.
|
||
bool isHttpOrFtp =
|
||
(NS_SUCCEEDED(aURI->SchemeIs("http", &isHttpOrFtp)) && isHttpOrFtp) ||
|
||
(NS_SUCCEEDED(aURI->SchemeIs("https", &isHttpOrFtp)) && isHttpOrFtp) ||
|
||
(NS_SUCCEEDED(aURI->SchemeIs("ftp", &isHttpOrFtp)) && isHttpOrFtp);
|
||
|
||
if (!isHttpOrFtp) {
|
||
// not strictly spec compliant, but what we really care about is
|
||
// http/https and also ftp. If it's not http/https or ftp, then treat aURI
|
||
// as if it's a globally unique identifier and just return the scheme.
|
||
aURI->GetScheme(outStrippedURI);
|
||
return;
|
||
}
|
||
|
||
// 2) If the origin of uri is not the same as the origin of the protected
|
||
// resource, then return the ASCII serialization of uri’s origin.
|
||
bool sameOrigin =
|
||
NS_SUCCEEDED(aProtectedResourcePrincipal->CheckMayLoad(aURI, false, false));
|
||
if (!sameOrigin) {
|
||
// cross origin redirects also fall into this category, see:
|
||
// http://www.w3.org/TR/CSP/#violation-reports
|
||
aURI->GetPrePath(outStrippedURI);
|
||
return;
|
||
}
|
||
|
||
// 3) Return uri, with any fragment component removed.
|
||
aURI->GetSpecIgnoringRef(outStrippedURI);
|
||
}
|
||
|
||
/**
|
||
* Sends CSP violation reports to all sources listed under report-uri.
|
||
*
|
||
* @param aBlockedContentSource
|
||
* Either a CSP Source (like 'self', as string) or nsIURI: the source
|
||
* of the violation.
|
||
* @param aOriginalUri
|
||
* The original URI if the blocked content is a redirect, else null
|
||
* @param aViolatedDirective
|
||
* the directive that was violated (string).
|
||
* @param aSourceFile
|
||
* name of the file containing the inline script violation
|
||
* @param aScriptSample
|
||
* a sample of the violating inline script
|
||
* @param aLineNum
|
||
* source line number of the violation (if available)
|
||
*/
|
||
nsresult
|
||
nsCSPContext::SendReports(nsISupports* aBlockedContentSource,
|
||
nsIURI* aOriginalURI,
|
||
nsAString& aViolatedDirective,
|
||
uint32_t aViolatedPolicyIndex,
|
||
nsAString& aSourceFile,
|
||
nsAString& aScriptSample,
|
||
uint32_t aLineNum)
|
||
{
|
||
NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1);
|
||
|
||
#ifdef MOZ_B2G
|
||
// load group information (on process-split necko implementations like b2g).
|
||
// (fix this in bug 1011086)
|
||
if (!mCallingChannelLoadGroup) {
|
||
NS_WARNING("Load group required but not present for report sending; cannot send CSP violation reports");
|
||
return NS_ERROR_FAILURE;
|
||
}
|
||
#endif
|
||
|
||
dom::CSPReport report;
|
||
nsresult rv;
|
||
|
||
// blocked-uri
|
||
if (aBlockedContentSource) {
|
||
nsAutoCString reportBlockedURI;
|
||
nsCOMPtr<nsIURI> uri = do_QueryInterface(aBlockedContentSource);
|
||
// could be a string or URI
|
||
if (uri) {
|
||
StripURIForReporting(uri, mLoadingPrincipal, reportBlockedURI);
|
||
} else {
|
||
nsCOMPtr<nsISupportsCString> cstr = do_QueryInterface(aBlockedContentSource);
|
||
if (cstr) {
|
||
cstr->GetData(reportBlockedURI);
|
||
}
|
||
}
|
||
if (reportBlockedURI.IsEmpty()) {
|
||
// this can happen for frame-ancestors violation where the violating
|
||
// ancestor is cross-origin.
|
||
NS_WARNING("No blocked URI (null aBlockedContentSource) for CSP violation report.");
|
||
}
|
||
report.mCsp_report.mBlocked_uri = NS_ConvertUTF8toUTF16(reportBlockedURI);
|
||
}
|
||
|
||
// document-uri
|
||
nsAutoCString reportDocumentURI;
|
||
StripURIForReporting(mSelfURI, mLoadingPrincipal, reportDocumentURI);
|
||
report.mCsp_report.mDocument_uri = NS_ConvertUTF8toUTF16(reportDocumentURI);
|
||
|
||
// original-policy
|
||
nsAutoString originalPolicy;
|
||
rv = this->GetPolicy(aViolatedPolicyIndex, originalPolicy);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
report.mCsp_report.mOriginal_policy = originalPolicy;
|
||
|
||
// referrer
|
||
if (!mReferrer.IsEmpty()) {
|
||
report.mCsp_report.mReferrer = mReferrer;
|
||
}
|
||
|
||
// violated-directive
|
||
report.mCsp_report.mViolated_directive = aViolatedDirective;
|
||
|
||
// source-file
|
||
if (!aSourceFile.IsEmpty()) {
|
||
// if aSourceFile is a URI, we have to make sure to strip fragments
|
||
nsCOMPtr<nsIURI> sourceURI;
|
||
NS_NewURI(getter_AddRefs(sourceURI), aSourceFile);
|
||
if (sourceURI) {
|
||
nsAutoCString spec;
|
||
sourceURI->GetSpecIgnoringRef(spec);
|
||
aSourceFile = NS_ConvertUTF8toUTF16(spec);
|
||
}
|
||
report.mCsp_report.mSource_file.Construct();
|
||
report.mCsp_report.mSource_file.Value() = aSourceFile;
|
||
}
|
||
|
||
// script-sample
|
||
if (!aScriptSample.IsEmpty()) {
|
||
report.mCsp_report.mScript_sample.Construct();
|
||
report.mCsp_report.mScript_sample.Value() = aScriptSample;
|
||
}
|
||
|
||
// line-number
|
||
if (aLineNum != 0) {
|
||
report.mCsp_report.mLine_number.Construct();
|
||
report.mCsp_report.mLine_number.Value() = aLineNum;
|
||
}
|
||
|
||
nsString csp_report;
|
||
if (!report.ToJSON(csp_report)) {
|
||
return NS_ERROR_FAILURE;
|
||
}
|
||
|
||
// ---------- Assembled, now send it to all the report URIs ----------- //
|
||
|
||
nsTArray<nsString> reportURIs;
|
||
mPolicies[aViolatedPolicyIndex]->getReportURIs(reportURIs);
|
||
|
||
|
||
nsCOMPtr<nsIDocument> doc = do_QueryReferent(mLoadingContext);
|
||
nsCOMPtr<nsIURI> reportURI;
|
||
nsCOMPtr<nsIChannel> reportChannel;
|
||
|
||
for (uint32_t r = 0; r < reportURIs.Length(); r++) {
|
||
nsAutoCString reportURICstring = NS_ConvertUTF16toUTF8(reportURIs[r]);
|
||
// try to create a new uri from every report-uri string
|
||
rv = NS_NewURI(getter_AddRefs(reportURI), reportURIs[r]);
|
||
if (NS_FAILED(rv)) {
|
||
const char16_t* params[] = { reportURIs[r].get() };
|
||
CSPCONTEXTLOG(("Could not create nsIURI for report URI %s",
|
||
reportURICstring.get()));
|
||
logToConsole(MOZ_UTF16("triedToSendReport"), params, ArrayLength(params),
|
||
aSourceFile, aScriptSample, aLineNum, 0, nsIScriptError::errorFlag);
|
||
continue; // don't return yet, there may be more URIs
|
||
}
|
||
|
||
// try to create a new channel for every report-uri
|
||
nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL | nsIChannel::LOAD_CLASSIFY_URI;
|
||
if (doc) {
|
||
rv = NS_NewChannel(getter_AddRefs(reportChannel),
|
||
reportURI,
|
||
doc,
|
||
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
|
||
nsIContentPolicy::TYPE_CSP_REPORT,
|
||
nullptr, // aLoadGroup
|
||
nullptr, // aCallbacks
|
||
loadFlags);
|
||
}
|
||
else {
|
||
rv = NS_NewChannel(getter_AddRefs(reportChannel),
|
||
reportURI,
|
||
mLoadingPrincipal,
|
||
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
|
||
nsIContentPolicy::TYPE_CSP_REPORT,
|
||
nullptr, // aLoadGroup
|
||
nullptr, // aCallbacks
|
||
loadFlags);
|
||
}
|
||
|
||
if (NS_FAILED(rv)) {
|
||
CSPCONTEXTLOG(("Could not create new channel for report URI %s",
|
||
reportURICstring.get()));
|
||
continue; // don't return yet, there may be more URIs
|
||
}
|
||
|
||
// log a warning to console if scheme is not http or https
|
||
bool isHttpScheme =
|
||
(NS_SUCCEEDED(reportURI->SchemeIs("http", &isHttpScheme)) && isHttpScheme) ||
|
||
(NS_SUCCEEDED(reportURI->SchemeIs("https", &isHttpScheme)) && isHttpScheme);
|
||
|
||
if (!isHttpScheme) {
|
||
const char16_t* params[] = { reportURIs[r].get() };
|
||
logToConsole(MOZ_UTF16("reportURInotHttpsOrHttp2"), params, ArrayLength(params),
|
||
aSourceFile, aScriptSample, aLineNum, 0, nsIScriptError::errorFlag);
|
||
continue;
|
||
}
|
||
|
||
// make sure this is an anonymous request (no cookies) so in case the
|
||
// policy URI is injected, it can't be abused for CSRF.
|
||
nsLoadFlags flags;
|
||
rv = reportChannel->GetLoadFlags(&flags);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
flags |= nsIRequest::LOAD_ANONYMOUS;
|
||
rv = reportChannel->SetLoadFlags(flags);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// we need to set an nsIChannelEventSink on the channel object
|
||
// so we can tell it to not follow redirects when posting the reports
|
||
RefPtr<CSPReportRedirectSink> reportSink = new CSPReportRedirectSink();
|
||
if (doc && doc->GetDocShell()) {
|
||
nsCOMPtr<nsINetworkInterceptController> interceptController =
|
||
do_QueryInterface(doc->GetDocShell());
|
||
reportSink->SetInterceptController(interceptController);
|
||
}
|
||
reportChannel->SetNotificationCallbacks(reportSink);
|
||
|
||
// apply the loadgroup from the channel taken by setRequestContext. If
|
||
// there's no loadgroup, AsyncOpen will fail on process-split necko (since
|
||
// the channel cannot query the iTabChild).
|
||
rv = reportChannel->SetLoadGroup(mCallingChannelLoadGroup);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// wire in the string input stream to send the report
|
||
nsCOMPtr<nsIStringInputStream> sis(do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID));
|
||
NS_ASSERTION(sis, "nsIStringInputStream is needed but not available to send CSP violation reports");
|
||
rv = sis->SetData(NS_ConvertUTF16toUTF8(csp_report).get(), csp_report.Length());
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(reportChannel));
|
||
if (!uploadChannel) {
|
||
// It's possible the URI provided can't be uploaded to, in which case
|
||
// we skip this one. We'll already have warned about a non-HTTP URI earlier.
|
||
continue;
|
||
}
|
||
|
||
rv = uploadChannel->SetUploadStream(sis, NS_LITERAL_CSTRING("application/csp-report"), -1);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// if this is an HTTP channel, set the request method to post
|
||
nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(reportChannel));
|
||
if (httpChannel) {
|
||
httpChannel->SetRequestMethod(NS_LITERAL_CSTRING("POST"));
|
||
}
|
||
|
||
RefPtr<CSPViolationReportListener> listener = new CSPViolationReportListener();
|
||
rv = reportChannel->AsyncOpen2(listener);
|
||
|
||
// AsyncOpen should not fail, but could if there's no load group (like if
|
||
// SetRequestContext is not given a channel). This should fail quietly and
|
||
// not return an error since it's really ok if reports don't go out, but
|
||
// it's good to log the error locally.
|
||
|
||
if (NS_FAILED(rv)) {
|
||
const char16_t* params[] = { reportURIs[r].get() };
|
||
CSPCONTEXTLOG(("AsyncOpen failed for report URI %s", params[0]));
|
||
logToConsole(MOZ_UTF16("triedToSendReport"), params, ArrayLength(params),
|
||
aSourceFile, aScriptSample, aLineNum, 0, nsIScriptError::errorFlag);
|
||
} else {
|
||
CSPCONTEXTLOG(("Sent violation report to URI %s", reportURICstring.get()));
|
||
}
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
/**
|
||
* Dispatched from the main thread to send reports for one CSP violation.
|
||
*/
|
||
class CSPReportSenderRunnable final : public Runnable
|
||
{
|
||
public:
|
||
CSPReportSenderRunnable(nsISupports* aBlockedContentSource,
|
||
nsIURI* aOriginalURI,
|
||
uint32_t aViolatedPolicyIndex,
|
||
bool aReportOnlyFlag,
|
||
const nsAString& aViolatedDirective,
|
||
const nsAString& aObserverSubject,
|
||
const nsAString& aSourceFile,
|
||
const nsAString& aScriptSample,
|
||
uint32_t aLineNum,
|
||
nsCSPContext* aCSPContext)
|
||
: mBlockedContentSource(aBlockedContentSource)
|
||
, mOriginalURI(aOriginalURI)
|
||
, mViolatedPolicyIndex(aViolatedPolicyIndex)
|
||
, mReportOnlyFlag(aReportOnlyFlag)
|
||
, mViolatedDirective(aViolatedDirective)
|
||
, mSourceFile(aSourceFile)
|
||
, mScriptSample(aScriptSample)
|
||
, mLineNum(aLineNum)
|
||
, mCSPContext(aCSPContext)
|
||
{
|
||
NS_ASSERTION(!aViolatedDirective.IsEmpty(), "Can not send reports without a violated directive");
|
||
// the observer subject is an nsISupports: either an nsISupportsCString
|
||
// from the arg passed in directly, or if that's empty, it's the blocked
|
||
// source.
|
||
if (aObserverSubject.IsEmpty()) {
|
||
mObserverSubject = aBlockedContentSource;
|
||
} else {
|
||
nsCOMPtr<nsISupportsCString> supportscstr =
|
||
do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID);
|
||
NS_ASSERTION(supportscstr, "Couldn't allocate nsISupportsCString");
|
||
supportscstr->SetData(NS_ConvertUTF16toUTF8(aObserverSubject));
|
||
mObserverSubject = do_QueryInterface(supportscstr);
|
||
}
|
||
}
|
||
|
||
NS_IMETHOD Run()
|
||
{
|
||
MOZ_ASSERT(NS_IsMainThread());
|
||
|
||
// 1) notify observers
|
||
nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
|
||
NS_ASSERTION(observerService, "needs observer service");
|
||
nsresult rv = observerService->NotifyObservers(mObserverSubject,
|
||
CSP_VIOLATION_TOPIC,
|
||
mViolatedDirective.get());
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// 2) send reports for the policy that was violated
|
||
mCSPContext->SendReports(mBlockedContentSource, mOriginalURI,
|
||
mViolatedDirective, mViolatedPolicyIndex,
|
||
mSourceFile, mScriptSample, mLineNum);
|
||
|
||
// 3) log to console (one per policy violation)
|
||
// mBlockedContentSource could be a URI or a string.
|
||
nsCOMPtr<nsIURI> blockedURI = do_QueryInterface(mBlockedContentSource);
|
||
// if mBlockedContentSource is not a URI, it could be a string
|
||
nsCOMPtr<nsISupportsCString> blockedString = do_QueryInterface(mBlockedContentSource);
|
||
|
||
nsCString blockedDataStr;
|
||
|
||
if (blockedURI) {
|
||
blockedURI->GetSpec(blockedDataStr);
|
||
} else if (blockedString) {
|
||
blockedString->GetData(blockedDataStr);
|
||
}
|
||
|
||
if (blockedDataStr.Length() > 0) {
|
||
nsString blockedDataChar16 = NS_ConvertUTF8toUTF16(blockedDataStr);
|
||
const char16_t* params[] = { mViolatedDirective.get(),
|
||
blockedDataChar16.get() };
|
||
mCSPContext->logToConsole(mReportOnlyFlag ? MOZ_UTF16("CSPROViolationWithURI") :
|
||
MOZ_UTF16("CSPViolationWithURI"),
|
||
params, ArrayLength(params), mSourceFile, mScriptSample,
|
||
mLineNum, 0, nsIScriptError::errorFlag);
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
private:
|
||
nsCOMPtr<nsISupports> mBlockedContentSource;
|
||
nsCOMPtr<nsIURI> mOriginalURI;
|
||
uint32_t mViolatedPolicyIndex;
|
||
bool mReportOnlyFlag;
|
||
nsString mViolatedDirective;
|
||
nsCOMPtr<nsISupports> mObserverSubject;
|
||
nsString mSourceFile;
|
||
nsString mScriptSample;
|
||
uint32_t mLineNum;
|
||
RefPtr<nsCSPContext> mCSPContext;
|
||
};
|
||
|
||
/**
|
||
* Asynchronously notifies any nsIObservers listening to the CSP violation
|
||
* topic that a violation occurred. Also triggers report sending and console
|
||
* logging. All asynchronous on the main thread.
|
||
*
|
||
* @param aBlockedContentSource
|
||
* Either a CSP Source (like 'self', as string) or nsIURI: the source
|
||
* of the violation.
|
||
* @param aOriginalUri
|
||
* The original URI if the blocked content is a redirect, else null
|
||
* @param aViolatedDirective
|
||
* the directive that was violated (string).
|
||
* @param aViolatedPolicyIndex
|
||
* the index of the policy that was violated (so we know where to send
|
||
* the reports).
|
||
* @param aObserverSubject
|
||
* optional, subject sent to the nsIObservers listening to the CSP
|
||
* violation topic.
|
||
* @param aSourceFile
|
||
* name of the file containing the inline script violation
|
||
* @param aScriptSample
|
||
* a sample of the violating inline script
|
||
* @param aLineNum
|
||
* source line number of the violation (if available)
|
||
*/
|
||
nsresult
|
||
nsCSPContext::AsyncReportViolation(nsISupports* aBlockedContentSource,
|
||
nsIURI* aOriginalURI,
|
||
const nsAString& aViolatedDirective,
|
||
uint32_t aViolatedPolicyIndex,
|
||
const nsAString& aObserverSubject,
|
||
const nsAString& aSourceFile,
|
||
const nsAString& aScriptSample,
|
||
uint32_t aLineNum)
|
||
{
|
||
NS_ENSURE_ARG_MAX(aViolatedPolicyIndex, mPolicies.Length() - 1);
|
||
|
||
NS_DispatchToMainThread(new CSPReportSenderRunnable(aBlockedContentSource,
|
||
aOriginalURI,
|
||
aViolatedPolicyIndex,
|
||
mPolicies[aViolatedPolicyIndex]->getReportOnlyFlag(),
|
||
aViolatedDirective,
|
||
aObserverSubject,
|
||
aSourceFile,
|
||
aScriptSample,
|
||
aLineNum,
|
||
this));
|
||
return NS_OK;
|
||
}
|
||
|
||
/**
|
||
* Based on the given docshell, determines if this CSP context allows the
|
||
* ancestry.
|
||
*
|
||
* In order to determine the URI of the parent document (one causing the load
|
||
* of this protected document), this function obtains the docShellTreeItem,
|
||
* then walks up the hierarchy until it finds a privileged (chrome) tree item.
|
||
* Getting the a tree item's URI looks like this in pseudocode:
|
||
*
|
||
* nsIDocShellTreeItem->GetDocument()->GetDocumentURI();
|
||
*
|
||
* aDocShell is the docShell for the protected document.
|
||
*/
|
||
NS_IMETHODIMP
|
||
nsCSPContext::PermitsAncestry(nsIDocShell* aDocShell, bool* outPermitsAncestry)
|
||
{
|
||
nsresult rv;
|
||
|
||
// Can't check ancestry without a docShell.
|
||
if (aDocShell == nullptr) {
|
||
return NS_ERROR_FAILURE;
|
||
}
|
||
|
||
*outPermitsAncestry = true;
|
||
|
||
// extract the ancestry as an array
|
||
nsCOMArray<nsIURI> ancestorsArray;
|
||
|
||
nsCOMPtr<nsIInterfaceRequestor> ir(do_QueryInterface(aDocShell));
|
||
nsCOMPtr<nsIDocShellTreeItem> treeItem(do_GetInterface(ir));
|
||
nsCOMPtr<nsIDocShellTreeItem> parentTreeItem;
|
||
nsCOMPtr<nsIURI> currentURI;
|
||
nsCOMPtr<nsIURI> uriClone;
|
||
|
||
// iterate through each docShell parent item
|
||
while (NS_SUCCEEDED(treeItem->GetParent(getter_AddRefs(parentTreeItem))) &&
|
||
parentTreeItem != nullptr) {
|
||
|
||
nsIDocument* doc = parentTreeItem->GetDocument();
|
||
NS_ASSERTION(doc, "Could not get nsIDocument from nsIDocShellTreeItem in nsCSPContext::PermitsAncestry");
|
||
NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
|
||
|
||
currentURI = doc->GetDocumentURI();
|
||
|
||
if (currentURI) {
|
||
// stop when reaching chrome
|
||
bool isChrome = false;
|
||
rv = currentURI->SchemeIs("chrome", &isChrome);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
if (isChrome) { break; }
|
||
|
||
// delete the userpass from the URI.
|
||
rv = currentURI->CloneIgnoringRef(getter_AddRefs(uriClone));
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// We don't care if this succeeds, just want to delete a userpass if
|
||
// there was one.
|
||
uriClone->SetUserPass(EmptyCString());
|
||
|
||
if (CSPCONTEXTLOGENABLED()) {
|
||
nsAutoCString spec;
|
||
uriClone->GetSpec(spec);
|
||
CSPCONTEXTLOG(("nsCSPContext::PermitsAncestry, found ancestor: %s", spec.get()));
|
||
}
|
||
ancestorsArray.AppendElement(uriClone);
|
||
}
|
||
|
||
// next ancestor
|
||
treeItem = parentTreeItem;
|
||
}
|
||
|
||
nsAutoString violatedDirective;
|
||
|
||
// Now that we've got the ancestry chain in ancestorsArray, time to check
|
||
// them against any CSP.
|
||
// NOTE: the ancestors are not allowed to be sent cross origin; this is a
|
||
// restriction not placed on subresource loads.
|
||
|
||
for (uint32_t a = 0; a < ancestorsArray.Length(); a++) {
|
||
if (CSPCONTEXTLOGENABLED()) {
|
||
nsAutoCString spec;
|
||
ancestorsArray[a]->GetSpec(spec);
|
||
CSPCONTEXTLOG(("nsCSPContext::PermitsAncestry, checking ancestor: %s", spec.get()));
|
||
}
|
||
// omit the ancestor URI in violation reports if cross-origin as per spec
|
||
// (it is a violation of the same-origin policy).
|
||
bool okToSendAncestor = NS_SecurityCompareURIs(ancestorsArray[a], mSelfURI, true);
|
||
|
||
|
||
bool permits = permitsInternal(nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE,
|
||
ancestorsArray[a],
|
||
nullptr, // no redirect here.
|
||
EmptyString(), // no nonce
|
||
false, // no redirect here.
|
||
false, // not a preload.
|
||
true, // specific, do not use default-src
|
||
true, // send violation reports
|
||
okToSendAncestor);
|
||
if (!permits) {
|
||
*outPermitsAncestry = false;
|
||
}
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::Permits(nsIURI* aURI,
|
||
CSPDirective aDir,
|
||
bool aSpecific,
|
||
bool* outPermits)
|
||
{
|
||
// Can't perform check without aURI
|
||
if (aURI == nullptr) {
|
||
return NS_ERROR_FAILURE;
|
||
}
|
||
|
||
*outPermits = permitsInternal(aDir,
|
||
aURI,
|
||
nullptr, // no original (pre-redirect) URI
|
||
EmptyString(), // no nonce
|
||
false, // not redirected.
|
||
false, // not a preload.
|
||
aSpecific,
|
||
true, // send violation reports
|
||
true); // send blocked URI in violation reports
|
||
|
||
if (CSPCONTEXTLOGENABLED()) {
|
||
nsAutoCString spec;
|
||
aURI->GetSpec(spec);
|
||
CSPCONTEXTLOG(("nsCSPContext::Permits, aUri: %s, aDir: %d, isAllowed: %s",
|
||
spec.get(), aDir,
|
||
*outPermits ? "allow" : "deny"));
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::ToJSON(nsAString& outCSPinJSON)
|
||
{
|
||
outCSPinJSON.Truncate();
|
||
dom::CSPPolicies jsonPolicies;
|
||
jsonPolicies.mCsp_policies.Construct();
|
||
|
||
for (uint32_t p = 0; p < mPolicies.Length(); p++) {
|
||
dom::CSP jsonCSP;
|
||
mPolicies[p]->toDomCSPStruct(jsonCSP);
|
||
jsonPolicies.mCsp_policies.Value().AppendElement(jsonCSP, fallible);
|
||
}
|
||
|
||
// convert the gathered information to JSON
|
||
if (!jsonPolicies.ToJSON(outCSPinJSON)) {
|
||
return NS_ERROR_FAILURE;
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
/* ========== CSPViolationReportListener implementation ========== */
|
||
|
||
NS_IMPL_ISUPPORTS(CSPViolationReportListener, nsIStreamListener, nsIRequestObserver, nsISupports);
|
||
|
||
CSPViolationReportListener::CSPViolationReportListener()
|
||
{
|
||
}
|
||
|
||
CSPViolationReportListener::~CSPViolationReportListener()
|
||
{
|
||
}
|
||
|
||
NS_METHOD
|
||
AppendSegmentToString(nsIInputStream* aInputStream,
|
||
void* aClosure,
|
||
const char* aRawSegment,
|
||
uint32_t aToOffset,
|
||
uint32_t aCount,
|
||
uint32_t* outWrittenCount)
|
||
{
|
||
nsCString* decodedData = static_cast<nsCString*>(aClosure);
|
||
decodedData->Append(aRawSegment, aCount);
|
||
*outWrittenCount = aCount;
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
CSPViolationReportListener::OnDataAvailable(nsIRequest* aRequest,
|
||
nsISupports* aContext,
|
||
nsIInputStream* aInputStream,
|
||
uint64_t aOffset,
|
||
uint32_t aCount)
|
||
{
|
||
uint32_t read;
|
||
nsCString decodedData;
|
||
return aInputStream->ReadSegments(AppendSegmentToString,
|
||
&decodedData,
|
||
aCount,
|
||
&read);
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
CSPViolationReportListener::OnStopRequest(nsIRequest* aRequest,
|
||
nsISupports* aContext,
|
||
nsresult aStatus)
|
||
{
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
CSPViolationReportListener::OnStartRequest(nsIRequest* aRequest,
|
||
nsISupports* aContext)
|
||
{
|
||
return NS_OK;
|
||
}
|
||
|
||
/* ========== CSPReportRedirectSink implementation ========== */
|
||
|
||
NS_IMPL_ISUPPORTS(CSPReportRedirectSink, nsIChannelEventSink, nsIInterfaceRequestor);
|
||
|
||
CSPReportRedirectSink::CSPReportRedirectSink()
|
||
{
|
||
}
|
||
|
||
CSPReportRedirectSink::~CSPReportRedirectSink()
|
||
{
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
CSPReportRedirectSink::AsyncOnChannelRedirect(nsIChannel* aOldChannel,
|
||
nsIChannel* aNewChannel,
|
||
uint32_t aRedirFlags,
|
||
nsIAsyncVerifyRedirectCallback* aCallback)
|
||
{
|
||
// cancel the old channel so XHR failure callback happens
|
||
nsresult rv = aOldChannel->Cancel(NS_ERROR_ABORT);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// notify an observer that we have blocked the report POST due to a redirect,
|
||
// used in testing, do this async since we're in an async call now to begin with
|
||
nsCOMPtr<nsIURI> uri;
|
||
rv = aOldChannel->GetURI(getter_AddRefs(uri));
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
|
||
NS_ASSERTION(observerService, "Observer service required to log CSP violations");
|
||
observerService->NotifyObservers(uri,
|
||
CSP_VIOLATION_TOPIC,
|
||
MOZ_UTF16("denied redirect while sending violation report"));
|
||
|
||
return NS_BINDING_REDIRECTED;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
CSPReportRedirectSink::GetInterface(const nsIID& aIID, void** aResult)
|
||
{
|
||
if (aIID.Equals(NS_GET_IID(nsINetworkInterceptController)) &&
|
||
mInterceptController) {
|
||
nsCOMPtr<nsINetworkInterceptController> copy(mInterceptController);
|
||
*aResult = copy.forget().take();
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
return QueryInterface(aIID, aResult);
|
||
}
|
||
|
||
void
|
||
CSPReportRedirectSink::SetInterceptController(nsINetworkInterceptController* aInterceptController)
|
||
{
|
||
mInterceptController = aInterceptController;
|
||
}
|
||
|
||
/* ===== nsISerializable implementation ====== */
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::Read(nsIObjectInputStream* aStream)
|
||
{
|
||
nsresult rv;
|
||
nsCOMPtr<nsISupports> supports;
|
||
|
||
rv = NS_ReadOptionalObject(aStream, true, getter_AddRefs(supports));
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
mSelfURI = do_QueryInterface(supports);
|
||
NS_ASSERTION(mSelfURI, "need a self URI to de-serialize");
|
||
|
||
uint32_t numPolicies;
|
||
rv = aStream->Read32(&numPolicies);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsAutoString policyString;
|
||
|
||
while (numPolicies > 0) {
|
||
numPolicies--;
|
||
|
||
rv = aStream->ReadString(policyString);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
bool reportOnly = false;
|
||
rv = aStream->ReadBoolean(&reportOnly);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// @param deliveredViaMetaTag:
|
||
// when parsing the CSP policy string initially we already remove directives
|
||
// that should not be processed when delivered via the meta tag. Such directives
|
||
// will not be present at this point anymore.
|
||
nsCSPPolicy* policy = nsCSPParser::parseContentSecurityPolicy(policyString,
|
||
mSelfURI,
|
||
reportOnly,
|
||
this,
|
||
false);
|
||
if (policy) {
|
||
mPolicies.AppendElement(policy);
|
||
}
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
nsCSPContext::Write(nsIObjectOutputStream* aStream)
|
||
{
|
||
nsresult rv = NS_WriteOptionalCompoundObject(aStream,
|
||
mSelfURI,
|
||
NS_GET_IID(nsIURI),
|
||
true);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// Serialize all the policies.
|
||
aStream->Write32(mPolicies.Length());
|
||
|
||
nsAutoString polStr;
|
||
for (uint32_t p = 0; p < mPolicies.Length(); p++) {
|
||
polStr.Truncate();
|
||
mPolicies[p]->toString(polStr);
|
||
aStream->WriteWStringZ(polStr.get());
|
||
aStream->WriteBoolean(mPolicies[p]->getReportOnlyFlag());
|
||
}
|
||
return NS_OK;
|
||
}
|