diff options
Diffstat (limited to '')
-rw-r--r-- | dom/security/nsCSPUtils.cpp | 1777 |
1 files changed, 1777 insertions, 0 deletions
diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp new file mode 100644 index 0000000000..50730b691b --- /dev/null +++ b/dom/security/nsCSPUtils.cpp @@ -0,0 +1,1777 @@ +/* -*- 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 "nsAttrValue.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsContentUtils.h" +#include "nsCSPUtils.h" +#include "nsDebug.h" +#include "nsCSPParser.h" +#include "nsComponentManagerUtils.h" +#include "nsIConsoleService.h" +#include "nsIChannel.h" +#include "nsICryptoHash.h" +#include "nsIScriptError.h" +#include "nsIStringBundle.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsSandboxFlags.h" +#include "nsServiceManagerUtils.h" +#include "nsWhitespaceTokenizer.h" + +#include "mozilla/Components.h" +#include "mozilla/dom/CSPDictionariesBinding.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/SRIMetadata.h" +#include "mozilla/StaticPrefs_security.h" + +using namespace mozilla; +using mozilla::dom::SRIMetadata; + +#define DEFAULT_PORT -1 + +static mozilla::LogModule* GetCspUtilsLog() { + static mozilla::LazyLogModule gCspUtilsPRLog("CSPUtils"); + return gCspUtilsPRLog; +} + +#define CSPUTILSLOG(args) \ + MOZ_LOG(GetCspUtilsLog(), mozilla::LogLevel::Debug, args) +#define CSPUTILSLOGENABLED() \ + MOZ_LOG_TEST(GetCspUtilsLog(), mozilla::LogLevel::Debug) + +void CSP_PercentDecodeStr(const nsAString& aEncStr, nsAString& outDecStr) { + outDecStr.Truncate(); + + // helper function that should not be visible outside this methods scope + struct local { + static inline char16_t convertHexDig(char16_t aHexDig) { + if (isNumberToken(aHexDig)) { + return aHexDig - '0'; + } + if (aHexDig >= 'A' && aHexDig <= 'F') { + return aHexDig - 'A' + 10; + } + // must be a lower case character + // (aHexDig >= 'a' && aHexDig <= 'f') + return aHexDig - 'a' + 10; + } + }; + + const char16_t *cur, *end, *hexDig1, *hexDig2; + cur = aEncStr.BeginReading(); + end = aEncStr.EndReading(); + + while (cur != end) { + // if it's not a percent sign then there is + // nothing to do for that character + if (*cur != PERCENT_SIGN) { + outDecStr.Append(*cur); + cur++; + continue; + } + + // get the two hexDigs following the '%'-sign + hexDig1 = cur + 1; + hexDig2 = cur + 2; + + // if there are no hexdigs after the '%' then + // there is nothing to do for us. + if (hexDig1 == end || hexDig2 == end || !isValidHexDig(*hexDig1) || + !isValidHexDig(*hexDig2)) { + outDecStr.Append(PERCENT_SIGN); + cur++; + continue; + } + + // decode "% hexDig1 hexDig2" into a character. + char16_t decChar = + (local::convertHexDig(*hexDig1) << 4) + local::convertHexDig(*hexDig2); + outDecStr.Append(decChar); + + // increment 'cur' to after the second hexDig + cur = ++hexDig2; + } +} + +// The Content Security Policy should be inherited for +// local schemes like: "about", "blob", "data", or "filesystem". +// see: https://w3c.github.io/webappsec-csp/#initialize-document-csp +bool CSP_ShouldResponseInheritCSP(nsIChannel* aChannel) { + if (!aChannel) { + return false; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, false); + + bool isAbout = uri->SchemeIs("about"); + if (isAbout) { + nsAutoCString aboutSpec; + rv = uri->GetSpec(aboutSpec); + NS_ENSURE_SUCCESS(rv, false); + // also allow about:blank#foo + if (StringBeginsWith(aboutSpec, "about:blank"_ns) || + StringBeginsWith(aboutSpec, "about:srcdoc"_ns)) { + return true; + } + } + + return uri->SchemeIs("blob") || uri->SchemeIs("data") || + uri->SchemeIs("filesystem") || uri->SchemeIs("javascript"); +} + +void CSP_ApplyMetaCSPToDoc(mozilla::dom::Document& aDoc, + const nsAString& aPolicyStr) { + if (aDoc.IsLoadedAsData()) { + return; + } + + nsAutoString policyStr( + nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>( + aPolicyStr)); + + if (policyStr.IsEmpty()) { + return; + } + + nsCOMPtr<nsIContentSecurityPolicy> csp = aDoc.GetCsp(); + if (!csp) { + MOZ_ASSERT(false, "how come there is no CSP"); + return; + } + + // Multiple CSPs (delivered through either header of meta tag) need to + // be joined together, see: + // https://w3c.github.io/webappsec/specs/content-security-policy/#delivery-html-meta-element + nsresult rv = + csp->AppendPolicy(policyStr, + false, // csp via meta tag can not be report only + true); // delivered through the meta tag + NS_ENSURE_SUCCESS_VOID(rv); + if (nsPIDOMWindowInner* inner = aDoc.GetInnerWindow()) { + inner->SetCsp(csp); + } + aDoc.ApplySettingsFromCSP(false); +} + +void CSP_GetLocalizedStr(const char* aName, const nsTArray<nsString>& aParams, + nsAString& outResult) { + nsCOMPtr<nsIStringBundle> keyStringBundle; + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::components::StringBundle::Service(); + + NS_ASSERTION(stringBundleService, "String bundle service must be present!"); + stringBundleService->CreateBundle( + "chrome://global/locale/security/csp.properties", + getter_AddRefs(keyStringBundle)); + + NS_ASSERTION(keyStringBundle, "Key string bundle must be available!"); + + if (!keyStringBundle) { + return; + } + keyStringBundle->FormatStringFromName(aName, aParams, outResult); +} + +void CSP_LogStrMessage(const nsAString& aMsg) { + nsCOMPtr<nsIConsoleService> console( + do_GetService("@mozilla.org/consoleservice;1")); + + if (!console) { + return; + } + nsString msg(aMsg); + console->LogStringMessage(msg.get()); +} + +void CSP_LogMessage(const nsAString& aMessage, const nsAString& aSourceName, + const nsAString& aSourceLine, uint32_t aLineNumber, + uint32_t aColumnNumber, uint32_t aFlags, + const nsACString& aCategory, uint64_t aInnerWindowID, + bool aFromPrivateWindow) { + nsCOMPtr<nsIConsoleService> console( + do_GetService(NS_CONSOLESERVICE_CONTRACTID)); + + nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID)); + + if (!console || !error) { + return; + } + + // Prepending CSP to the outgoing console message + nsString cspMsg; + CSP_GetLocalizedStr("CSPMessagePrefix", + AutoTArray<nsString, 1>{nsString(aMessage)}, cspMsg); + + // Currently 'aSourceLine' is not logged to the console, because similar + // information is already included within the source link of the message. + // For inline violations however, the line and column number are 0 and + // information contained within 'aSourceLine' can be really useful for devs. + // E.g. 'aSourceLine' might be: 'onclick attribute on DIV element'. + // In such cases we append 'aSourceLine' directly to the error message. + if (!aSourceLine.IsEmpty() && aLineNumber == 0) { + cspMsg.AppendLiteral(u"\nSource: "); + cspMsg.Append(aSourceLine); + } + + // Since we are leveraging csp errors as the category names which + // we pass to devtools, we should prepend them with "CSP_" to + // allow easy distincution in devtools code. e.g. + // upgradeInsecureRequest -> CSP_upgradeInsecureRequest + nsCString category("CSP_"); + category.Append(aCategory); + + nsresult rv; + if (aInnerWindowID > 0) { + rv = error->InitWithWindowID(cspMsg, aSourceName, aSourceLine, aLineNumber, + aColumnNumber, aFlags, category, + aInnerWindowID); + } else { + rv = error->Init(cspMsg, aSourceName, aSourceLine, aLineNumber, + aColumnNumber, aFlags, category, aFromPrivateWindow, + true /* from chrome context */); + } + if (NS_FAILED(rv)) { + return; + } + console->LogMessage(error); +} + +/** + * Combines CSP_LogMessage and CSP_GetLocalizedStr into one call. + */ +void CSP_LogLocalizedStr(const char* aName, const nsTArray<nsString>& aParams, + const nsAString& aSourceName, + const nsAString& aSourceLine, uint32_t aLineNumber, + uint32_t aColumnNumber, uint32_t aFlags, + const nsACString& aCategory, uint64_t aInnerWindowID, + bool aFromPrivateWindow) { + nsAutoString logMsg; + CSP_GetLocalizedStr(aName, aParams, logMsg); + CSP_LogMessage(logMsg, aSourceName, aSourceLine, aLineNumber, aColumnNumber, + aFlags, aCategory, aInnerWindowID, aFromPrivateWindow); +} + +/* ===== Helpers ============================ */ +// This implements +// https://w3c.github.io/webappsec-csp/#effective-directive-for-a-request. +// However the spec doesn't currently cover all request destinations, which +// we roughly represent using nsContentPolicyType. +CSPDirective CSP_ContentTypeToDirective(nsContentPolicyType aType) { + switch (aType) { + case nsIContentPolicy::TYPE_IMAGE: + case nsIContentPolicy::TYPE_IMAGESET: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON: + return nsIContentSecurityPolicy::IMG_SRC_DIRECTIVE; + + // BLock XSLT as script, see bug 910139 + case nsIContentPolicy::TYPE_XSLT: + case nsIContentPolicy::TYPE_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_MODULE: + case nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS: + case nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET: + case nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET: + case nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT: + // (https://github.com/w3c/webappsec-csp/issues/554) + // Some of these types are not explicitly defined in the spec. + // + // Chrome seems to use script-src-elem for worklet! + return nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE; + + case nsIContentPolicy::TYPE_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD: + return nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE; + + case nsIContentPolicy::TYPE_FONT: + case nsIContentPolicy::TYPE_INTERNAL_FONT_PRELOAD: + return nsIContentSecurityPolicy::FONT_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_MEDIA: + case nsIContentPolicy::TYPE_INTERNAL_AUDIO: + case nsIContentPolicy::TYPE_INTERNAL_VIDEO: + case nsIContentPolicy::TYPE_INTERNAL_TRACK: + return nsIContentSecurityPolicy::MEDIA_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_WEB_MANIFEST: + return nsIContentSecurityPolicy::WEB_MANIFEST_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_INTERNAL_WORKER: + case nsIContentPolicy::TYPE_INTERNAL_WORKER_STATIC_MODULE: + case nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER: + case nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER: + return nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_SUBDOCUMENT: + case nsIContentPolicy::TYPE_INTERNAL_FRAME: + case nsIContentPolicy::TYPE_INTERNAL_IFRAME: + return nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_WEBSOCKET: + case nsIContentPolicy::TYPE_XMLHTTPREQUEST: + case nsIContentPolicy::TYPE_BEACON: + case nsIContentPolicy::TYPE_PING: + case nsIContentPolicy::TYPE_FETCH: + case nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST: + case nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE: + case nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD: + case nsIContentPolicy::TYPE_WEB_IDENTITY: + case nsIContentPolicy::TYPE_WEB_TRANSPORT: + return nsIContentSecurityPolicy::CONNECT_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_OBJECT: + case nsIContentPolicy::TYPE_OBJECT_SUBREQUEST: + case nsIContentPolicy::TYPE_INTERNAL_EMBED: + case nsIContentPolicy::TYPE_INTERNAL_OBJECT: + return nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE; + + case nsIContentPolicy::TYPE_DTD: + case nsIContentPolicy::TYPE_OTHER: + case nsIContentPolicy::TYPE_SPECULATIVE: + case nsIContentPolicy::TYPE_INTERNAL_DTD: + case nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD: + return nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE; + + // CSP does not apply to webrtc connections + case nsIContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA: + // csp shold not block top level loads, e.g. in case + // of a redirect. + case nsIContentPolicy::TYPE_DOCUMENT: + // CSP can not block csp reports + case nsIContentPolicy::TYPE_CSP_REPORT: + return nsIContentSecurityPolicy::NO_DIRECTIVE; + + case nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD: + case nsIContentPolicy::TYPE_UA_FONT: + return nsIContentSecurityPolicy::NO_DIRECTIVE; + + // Fall through to error for all other directives + case nsIContentPolicy::TYPE_INVALID: + case nsIContentPolicy::TYPE_END: + MOZ_ASSERT(false, "Can not map nsContentPolicyType to CSPDirective"); + // Do not add default: so that compilers can catch the missing case. + } + return nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE; +} + +nsCSPHostSrc* CSP_CreateHostSrcFromSelfURI(nsIURI* aSelfURI) { + // Create the host first + nsCString host; + aSelfURI->GetAsciiHost(host); + nsCSPHostSrc* hostsrc = new nsCSPHostSrc(NS_ConvertUTF8toUTF16(host)); + hostsrc->setGeneratedFromSelfKeyword(); + + // Add the scheme. + nsCString scheme; + aSelfURI->GetScheme(scheme); + hostsrc->setScheme(NS_ConvertUTF8toUTF16(scheme)); + + // An empty host (e.g. for data:) indicates it's effectively a unique origin. + // Please note that we still need to set the scheme on hostsrc (see above), + // because it's used for reporting. + if (host.EqualsLiteral("")) { + hostsrc->setIsUniqueOrigin(); + // no need to query the port in that case. + return hostsrc; + } + + int32_t port; + aSelfURI->GetPort(&port); + // Only add port if it's not default port. + if (port > 0) { + nsAutoString portStr; + portStr.AppendInt(port); + hostsrc->setPort(portStr); + } + return hostsrc; +} + +bool CSP_IsEmptyDirective(const nsAString& aValue, const nsAString& aDir) { + return (aDir.Length() == 0 && aValue.Length() == 0); +} +bool CSP_IsDirective(const nsAString& aValue, CSPDirective aDir) { + return aValue.LowerCaseEqualsASCII(CSP_CSPDirectiveToString(aDir)); +} + +bool CSP_IsKeyword(const nsAString& aValue, enum CSPKeyword aKey) { + return aValue.LowerCaseEqualsASCII(CSP_EnumToUTF8Keyword(aKey)); +} + +bool CSP_IsQuotelessKeyword(const nsAString& aKey) { + nsString lowerKey; + ToLowerCase(aKey, lowerKey); + + nsAutoString keyword; + for (uint32_t i = 0; i < CSP_LAST_KEYWORD_VALUE; i++) { + // skipping the leading ' and trimming the trailing ' + keyword.AssignASCII(gCSPUTF8Keywords[i] + 1); + keyword.Trim("'", false, true); + if (lowerKey.Equals(keyword)) { + return true; + } + } + return false; +} + +/* + * Checks whether the current directive permits a specific + * scheme. This function is called from nsCSPSchemeSrc() and + * also nsCSPHostSrc. + * @param aEnforcementScheme + * The scheme that this directive allows + * @param aUri + * The uri of the subresource load. + * @param aReportOnly + * Whether the enforced policy is report only or not. + * @param aUpgradeInsecure + * Whether the policy makes use of the directive + * 'upgrade-insecure-requests'. + * @param aFromSelfURI + * Whether a scheme was generated from the keyword 'self' + * which then allows schemeless sources to match ws and wss. + */ + +bool permitsScheme(const nsAString& aEnforcementScheme, nsIURI* aUri, + bool aReportOnly, bool aUpgradeInsecure, bool aFromSelfURI) { + nsAutoCString scheme; + nsresult rv = aUri->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, false); + + // no scheme to enforce, let's allow the load (e.g. script-src *) + if (aEnforcementScheme.IsEmpty()) { + return true; + } + + // if the scheme matches, all good - allow the load + if (aEnforcementScheme.EqualsASCII(scheme.get())) { + return true; + } + + // allow scheme-less sources where the protected resource is http + // and the load is https, see: + // http://www.w3.org/TR/CSP2/#match-source-expression + if (aEnforcementScheme.EqualsASCII("http")) { + if (scheme.EqualsASCII("https")) { + return true; + } + if ((scheme.EqualsASCII("ws") || scheme.EqualsASCII("wss")) && + aFromSelfURI) { + return true; + } + } + if (aEnforcementScheme.EqualsASCII("https")) { + if (scheme.EqualsLiteral("wss") && aFromSelfURI) { + return true; + } + } + if (aEnforcementScheme.EqualsASCII("ws") && scheme.EqualsASCII("wss")) { + return true; + } + + // Allow the load when enforcing upgrade-insecure-requests with the + // promise the request gets upgraded from http to https and ws to wss. + // See nsHttpChannel::Connect() and also WebSocket.cpp. Please note, + // the report only policies should not allow the load and report + // the error back to the page. + return ( + (aUpgradeInsecure && !aReportOnly) && + ((scheme.EqualsASCII("http") && + aEnforcementScheme.EqualsASCII("https")) || + (scheme.EqualsASCII("ws") && aEnforcementScheme.EqualsASCII("wss")))); +} + +/* + * A helper function for appending a CSP header to an existing CSP + * policy. + * + * @param aCsp the CSP policy + * @param aHeaderValue the header + * @param aReportOnly is this a report-only header? + */ + +nsresult CSP_AppendCSPFromHeader(nsIContentSecurityPolicy* aCsp, + const nsAString& aHeaderValue, + bool aReportOnly) { + NS_ENSURE_ARG(aCsp); + + // Need to tokenize the header value since multiple headers could be + // concatenated into one comma-separated list of policies. + // See RFC2616 section 4.2 (last paragraph) + nsresult rv = NS_OK; + for (const nsAString& policy : + nsCharSeparatedTokenizer(aHeaderValue, ',').ToRange()) { + rv = aCsp->AppendPolicy(policy, aReportOnly, false); + NS_ENSURE_SUCCESS(rv, rv); + { + CSPUTILSLOG(("CSP refined with policy: \"%s\"", + NS_ConvertUTF16toUTF8(policy).get())); + } + } + return NS_OK; +} + +/* ===== nsCSPSrc ============================ */ + +nsCSPBaseSrc::nsCSPBaseSrc() {} + +nsCSPBaseSrc::~nsCSPBaseSrc() = default; + +// ::permits is only called for external load requests, therefore: +// nsCSPKeywordSrc and nsCSPHashSource fall back to this base class +// implementation which will never allow the load. +bool nsCSPBaseSrc::permits(nsIURI* aUri, bool aWasRedirected, bool aReportOnly, + bool aUpgradeInsecure) const { + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG( + ("nsCSPBaseSrc::permits, aUri: %s", aUri->GetSpecOrDefault().get())); + } + return false; +} + +// ::allows is only called for inlined loads, therefore: +// nsCSPSchemeSrc, nsCSPHostSrc fall back +// to this base class implementation which will never allow the load. +bool nsCSPBaseSrc::allows(enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce) const { + CSPUTILSLOG(("nsCSPBaseSrc::allows, aKeyWord: %s, a HashOrNonce: %s", + aKeyword == CSP_HASH ? "hash" : CSP_EnumToUTF8Keyword(aKeyword), + NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + return false; +} + +/* ====== nsCSPSchemeSrc ===================== */ + +nsCSPSchemeSrc::nsCSPSchemeSrc(const nsAString& aScheme) : mScheme(aScheme) { + ToLowerCase(mScheme); +} + +nsCSPSchemeSrc::~nsCSPSchemeSrc() = default; + +bool nsCSPSchemeSrc::permits(nsIURI* aUri, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure) const { + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG( + ("nsCSPSchemeSrc::permits, aUri: %s", aUri->GetSpecOrDefault().get())); + } + MOZ_ASSERT((!mScheme.EqualsASCII("")), "scheme can not be the empty string"); + return permitsScheme(mScheme, aUri, aReportOnly, aUpgradeInsecure, false); +} + +bool nsCSPSchemeSrc::visit(nsCSPSrcVisitor* aVisitor) const { + return aVisitor->visitSchemeSrc(*this); +} + +void nsCSPSchemeSrc::toString(nsAString& outStr) const { + outStr.Append(mScheme); + outStr.AppendLiteral(":"); +} + +/* ===== nsCSPHostSrc ======================== */ + +nsCSPHostSrc::nsCSPHostSrc(const nsAString& aHost) + : mHost(aHost), + mGeneratedFromSelfKeyword(false), + mIsUniqueOrigin(false), + mWithinFrameAncstorsDir(false) { + ToLowerCase(mHost); +} + +nsCSPHostSrc::~nsCSPHostSrc() = default; + +/* + * Checks whether the current directive permits a specific port. + * @param aEnforcementScheme + * The scheme that this directive allows + * (used to query the default port for that scheme) + * @param aEnforcementPort + * The port that this directive allows + * @param aResourceURI + * The uri of the subresource load + */ +bool permitsPort(const nsAString& aEnforcementScheme, + const nsAString& aEnforcementPort, nsIURI* aResourceURI) { + // If enforcement port is the wildcard, don't block the load. + if (aEnforcementPort.EqualsASCII("*")) { + return true; + } + + int32_t resourcePort; + nsresult rv = aResourceURI->GetPort(&resourcePort); + if (NS_FAILED(rv) && aEnforcementPort.IsEmpty()) { + // If we cannot get a Port (e.g. because of an Custom Protocol handler) + // We need to check if a default port is associated with the Scheme + if (aEnforcementScheme.IsEmpty()) { + return false; + } + int defaultPortforScheme = + NS_GetDefaultPort(NS_ConvertUTF16toUTF8(aEnforcementScheme).get()); + + // If there is no default port associated with the Scheme ( + // defaultPortforScheme == -1) or it is an externally handled protocol ( + // defaultPortforScheme == 0 ) and the csp does not enforce a port - we can + // allow not having a port + return (defaultPortforScheme == -1 || defaultPortforScheme == -0); + } + // Avoid unnecessary string creation/manipulation and don't block the + // load if the resource to be loaded uses the default port for that + // scheme and there is no port to be enforced. + // Note, this optimization relies on scheme checks within permitsScheme(). + if (resourcePort == DEFAULT_PORT && aEnforcementPort.IsEmpty()) { + return true; + } + + // By now we know at that either the resourcePort does not use the default + // port or there is a port restriction to be enforced. A port value of -1 + // corresponds to the protocol's default port (eg. -1 implies port 80 for + // http URIs), in such a case we have to query the default port of the + // resource to be loaded. + if (resourcePort == DEFAULT_PORT) { + nsAutoCString resourceScheme; + rv = aResourceURI->GetScheme(resourceScheme); + NS_ENSURE_SUCCESS(rv, false); + resourcePort = NS_GetDefaultPort(resourceScheme.get()); + } + + // If there is a port to be enforced and the ports match, then + // don't block the load. + nsString resourcePortStr; + resourcePortStr.AppendInt(resourcePort); + if (aEnforcementPort.Equals(resourcePortStr)) { + return true; + } + + // If there is no port to be enforced, query the default port for the load. + nsString enforcementPort(aEnforcementPort); + if (enforcementPort.IsEmpty()) { + // For scheme less sources, our parser always generates a scheme + // which is the scheme of the protected resource. + MOZ_ASSERT(!aEnforcementScheme.IsEmpty(), + "need a scheme to query default port"); + int32_t defaultEnforcementPort = + NS_GetDefaultPort(NS_ConvertUTF16toUTF8(aEnforcementScheme).get()); + enforcementPort.Truncate(); + enforcementPort.AppendInt(defaultEnforcementPort); + } + + // If default ports match, don't block the load + if (enforcementPort.Equals(resourcePortStr)) { + return true; + } + + // Additional port matching where the regular URL matching algorithm + // treats insecure ports as matching their secure variants. + // default port for http is :80 + // default port for https is :443 + if (enforcementPort.EqualsLiteral("80") && + resourcePortStr.EqualsLiteral("443")) { + return true; + } + + // ports do not match, block the load. + return false; +} + +bool nsCSPHostSrc::permits(nsIURI* aUri, bool aWasRedirected, bool aReportOnly, + bool aUpgradeInsecure) const { + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG( + ("nsCSPHostSrc::permits, aUri: %s", aUri->GetSpecOrDefault().get())); + } + + if (mIsUniqueOrigin) { + return false; + } + + // we are following the enforcement rules from the spec, see: + // http://www.w3.org/TR/CSP11/#match-source-expression + + // 4.3) scheme matching: Check if the scheme matches. + if (!permitsScheme(mScheme, aUri, aReportOnly, aUpgradeInsecure, + mGeneratedFromSelfKeyword)) { + return false; + } + + // The host in nsCSpHostSrc should never be empty. In case we are enforcing + // just a specific scheme, the parser should generate a nsCSPSchemeSource. + NS_ASSERTION((!mHost.IsEmpty()), "host can not be the empty string"); + + // Before we can check if the host matches, we have to + // extract the host part from aUri. + nsAutoCString uriHost; + nsresult rv = aUri->GetAsciiHost(uriHost); + NS_ENSURE_SUCCESS(rv, false); + + nsString decodedUriHost; + CSP_PercentDecodeStr(NS_ConvertUTF8toUTF16(uriHost), decodedUriHost); + + // 2) host matching: Enforce a single * + if (mHost.EqualsASCII("*")) { + // The single ASTERISK character (*) does not match a URI's scheme of a type + // designating a globally unique identifier (such as blob:, data:, or + // filesystem:) At the moment firefox does not support filesystem; but for + // future compatibility we support it in CSP according to the spec, + // see: 4.2.2 Matching Source Expressions Note, that allowlisting any of + // these schemes would call nsCSPSchemeSrc::permits(). + if (aUri->SchemeIs("blob") || aUri->SchemeIs("data") || + aUri->SchemeIs("filesystem")) { + return false; + } + + // If no scheme is present there also wont be a port and folder to check + // which means we can return early + if (mScheme.IsEmpty()) { + return true; + } + } + // 4.5) host matching: Check if the allowed host starts with a wilcard. + else if (mHost.First() == '*') { + NS_ASSERTION( + mHost[1] == '.', + "Second character needs to be '.' whenever host starts with '*'"); + + // Eliminate leading "*", but keeping the FULL STOP (.) thereafter before + // checking if the remaining characters match + nsString wildCardHost = mHost; + wildCardHost = Substring(wildCardHost, 1, wildCardHost.Length() - 1); + if (!StringEndsWith(decodedUriHost, wildCardHost)) { + return false; + } + } + // 4.6) host matching: Check if hosts match. + else if (!mHost.Equals(decodedUriHost)) { + return false; + } + + // Port matching: Check if the ports match. + if (!permitsPort(mScheme, mPort, aUri)) { + return false; + } + + // 4.9) Path matching: If there is a path, we have to enforce + // path-level matching, unless the channel got redirected, see: + // http://www.w3.org/TR/CSP11/#source-list-paths-and-redirects + if (!aWasRedirected && !mPath.IsEmpty()) { + // converting aUri into nsIURL so we can strip query and ref + // example.com/test#foo -> example.com/test + // example.com/test?val=foo -> example.com/test + nsCOMPtr<nsIURL> url = do_QueryInterface(aUri); + if (!url) { + NS_ASSERTION(false, "can't QI into nsIURI"); + return false; + } + nsAutoCString uriPath; + rv = url->GetFilePath(uriPath); + NS_ENSURE_SUCCESS(rv, false); + + if (mWithinFrameAncstorsDir) { + // no path matching for frame-ancestors to not leak any path information. + return true; + } + + nsString decodedUriPath; + CSP_PercentDecodeStr(NS_ConvertUTF8toUTF16(uriPath), decodedUriPath); + + // check if the last character of mPath is '/'; if so + // we just have to check loading resource is within + // the allowed path. + if (mPath.Last() == '/') { + if (!StringBeginsWith(decodedUriPath, mPath)) { + return false; + } + } + // otherwise mPath refers to a specific file, and we have to + // check if the loading resource matches the file. + else { + if (!mPath.Equals(decodedUriPath)) { + return false; + } + } + } + + // At the end: scheme, host, port and path match -> allow the load. + return true; +} + +bool nsCSPHostSrc::visit(nsCSPSrcVisitor* aVisitor) const { + return aVisitor->visitHostSrc(*this); +} + +void nsCSPHostSrc::toString(nsAString& outStr) const { + if (mGeneratedFromSelfKeyword) { + outStr.AppendLiteral("'self'"); + return; + } + + // If mHost is a single "*", we append the wildcard and return. + if (mHost.EqualsASCII("*") && mScheme.IsEmpty() && mPort.IsEmpty()) { + outStr.Append(mHost); + return; + } + + // append scheme + outStr.Append(mScheme); + + // append host + outStr.AppendLiteral("://"); + outStr.Append(mHost); + + // append port + if (!mPort.IsEmpty()) { + outStr.AppendLiteral(":"); + outStr.Append(mPort); + } + + // append path + outStr.Append(mPath); +} + +void nsCSPHostSrc::setScheme(const nsAString& aScheme) { + mScheme = aScheme; + ToLowerCase(mScheme); +} + +void nsCSPHostSrc::setPort(const nsAString& aPort) { mPort = aPort; } + +void nsCSPHostSrc::appendPath(const nsAString& aPath) { mPath.Append(aPath); } + +/* ===== nsCSPKeywordSrc ===================== */ + +nsCSPKeywordSrc::nsCSPKeywordSrc(enum CSPKeyword aKeyword) + : mKeyword(aKeyword) { + NS_ASSERTION((aKeyword != CSP_SELF), + "'self' should have been replaced in the parser"); +} + +nsCSPKeywordSrc::~nsCSPKeywordSrc() = default; + +bool nsCSPKeywordSrc::allows(enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce) const { + CSPUTILSLOG(("nsCSPKeywordSrc::allows, aKeyWord: %s, aHashOrNonce: %s", + CSP_EnumToUTF8Keyword(aKeyword), + NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + return mKeyword == aKeyword; +} + +bool nsCSPKeywordSrc::visit(nsCSPSrcVisitor* aVisitor) const { + return aVisitor->visitKeywordSrc(*this); +} + +void nsCSPKeywordSrc::toString(nsAString& outStr) const { + outStr.Append(CSP_EnumToUTF16Keyword(mKeyword)); +} + +/* ===== nsCSPNonceSrc ==================== */ + +nsCSPNonceSrc::nsCSPNonceSrc(const nsAString& aNonce) : mNonce(aNonce) {} + +nsCSPNonceSrc::~nsCSPNonceSrc() = default; + +bool nsCSPNonceSrc::allows(enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce) const { + CSPUTILSLOG(("nsCSPNonceSrc::allows, aKeyWord: %s, a HashOrNonce: %s", + CSP_EnumToUTF8Keyword(aKeyword), + NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + + if (aKeyword != CSP_NONCE) { + return false; + } + // nonces can not be invalidated by strict-dynamic + return mNonce.Equals(aHashOrNonce); +} + +bool nsCSPNonceSrc::visit(nsCSPSrcVisitor* aVisitor) const { + return aVisitor->visitNonceSrc(*this); +} + +void nsCSPNonceSrc::toString(nsAString& outStr) const { + outStr.Append(CSP_EnumToUTF16Keyword(CSP_NONCE)); + outStr.Append(mNonce); + outStr.AppendLiteral("'"); +} + +/* ===== nsCSPHashSrc ===================== */ + +nsCSPHashSrc::nsCSPHashSrc(const nsAString& aAlgo, const nsAString& aHash) + : mAlgorithm(aAlgo), mHash(aHash) { + // Only the algo should be rewritten to lowercase, the hash must remain the + // same. + ToLowerCase(mAlgorithm); + // Normalize the base64url encoding to base64 encoding: + char16_t* cur = mHash.BeginWriting(); + char16_t* end = mHash.EndWriting(); + + for (; cur < end; ++cur) { + if (char16_t('-') == *cur) { + *cur = char16_t('+'); + } + if (char16_t('_') == *cur) { + *cur = char16_t('/'); + } + } +} + +nsCSPHashSrc::~nsCSPHashSrc() = default; + +bool nsCSPHashSrc::allows(enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce) const { + CSPUTILSLOG(("nsCSPHashSrc::allows, aKeyWord: %s, a HashOrNonce: %s", + CSP_EnumToUTF8Keyword(aKeyword), + NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + + if (aKeyword != CSP_HASH) { + return false; + } + + // hashes can not be invalidated by strict-dynamic + + // Convert aHashOrNonce to UTF-8 + NS_ConvertUTF16toUTF8 utf8_hash(aHashOrNonce); + + nsCOMPtr<nsICryptoHash> hasher; + nsresult rv = NS_NewCryptoHash(NS_ConvertUTF16toUTF8(mAlgorithm), + getter_AddRefs(hasher)); + NS_ENSURE_SUCCESS(rv, false); + + rv = hasher->Update((uint8_t*)utf8_hash.get(), utf8_hash.Length()); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoCString hash; + rv = hasher->Finish(true, hash); + NS_ENSURE_SUCCESS(rv, false); + + return NS_ConvertUTF16toUTF8(mHash).Equals(hash); +} + +bool nsCSPHashSrc::visit(nsCSPSrcVisitor* aVisitor) const { + return aVisitor->visitHashSrc(*this); +} + +void nsCSPHashSrc::toString(nsAString& outStr) const { + outStr.AppendLiteral("'"); + outStr.Append(mAlgorithm); + outStr.AppendLiteral("-"); + outStr.Append(mHash); + outStr.AppendLiteral("'"); +} + +/* ===== nsCSPReportURI ===================== */ + +nsCSPReportURI::nsCSPReportURI(nsIURI* aURI) : mReportURI(aURI) {} + +nsCSPReportURI::~nsCSPReportURI() = default; + +bool nsCSPReportURI::visit(nsCSPSrcVisitor* aVisitor) const { return false; } + +void nsCSPReportURI::toString(nsAString& outStr) const { + nsAutoCString spec; + nsresult rv = mReportURI->GetSpec(spec); + if (NS_FAILED(rv)) { + return; + } + outStr.AppendASCII(spec.get()); +} + +/* ===== nsCSPSandboxFlags ===================== */ + +nsCSPSandboxFlags::nsCSPSandboxFlags(const nsAString& aFlags) : mFlags(aFlags) { + ToLowerCase(mFlags); +} + +nsCSPSandboxFlags::~nsCSPSandboxFlags() = default; + +bool nsCSPSandboxFlags::visit(nsCSPSrcVisitor* aVisitor) const { return false; } + +void nsCSPSandboxFlags::toString(nsAString& outStr) const { + outStr.Append(mFlags); +} + +/* ===== nsCSPDirective ====================== */ + +nsCSPDirective::nsCSPDirective(CSPDirective aDirective) { + mDirective = aDirective; +} + +nsCSPDirective::~nsCSPDirective() { + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + delete mSrcs[i]; + } +} + +// https://w3c.github.io/webappsec-csp/#match-nonce-to-source-list +static bool DoesNonceMatchSourceList(nsILoadInfo* aLoadInfo, + const nsTArray<nsCSPBaseSrc*>& aSrcs) { + // Step 1. Assert: source list is not null. (implicit) + + // Note: For code-reuse we do "request’s cryptographic nonce metadata" here + // instead of the caller. + nsAutoString nonce; + MOZ_ALWAYS_SUCCEEDS(aLoadInfo->GetCspNonce(nonce)); + + // Step 2. If nonce is the empty string, return "Does Not Match". + if (nonce.IsEmpty()) { + return false; + } + + // Step 3. For each expression of source list: + for (nsCSPBaseSrc* src : aSrcs) { + // Step 3.1. If expression matches the nonce-source grammar, and nonce is + // identical to expression’s base64-value part, return "Matches". + if (src->isNonce()) { + nsAutoString srcNonce; + static_cast<nsCSPNonceSrc*>(src)->getNonce(srcNonce); + if (srcNonce == nonce) { + return true; + } + } + } + + // Step 4. Return "Does Not Match". + return false; +} + +// https://www.w3.org/TR/SRI/#parse-metadata +// This function is similar to SRICheck::IntegrityMetadata, but also keeps +// SRI metadata with weaker hashes. +// CSP treats "no metadata" and empty results the same way. +static nsTArray<SRIMetadata> ParseSRIMetadata(const nsAString& aMetadata) { + // Step 1. Let result be the empty set. + // Step 2. Let empty be equal to true. + nsTArray<SRIMetadata> result; + + NS_ConvertUTF16toUTF8 metadataList(aMetadata); + nsAutoCString token; + + // Step 3. For each token returned by splitting metadata on spaces: + nsCWhitespaceTokenizer tokenizer(metadataList); + while (tokenizer.hasMoreTokens()) { + token = tokenizer.nextToken(); + // Step 3.1. Set empty to false. + // Step 3.3. Parse token per the grammar in integrity metadata. + SRIMetadata metadata(token); + // Step 3.2. If token is not a valid metadata, skip the remaining steps, and + // proceed to the next token. + if (metadata.IsMalformed()) { + continue; + } + + // Step 3.4. Let algorithm be the alg component of token. + // Step 3.5. If algorithm is a hash function recognized by the user agent, + // add the + // parsed token to result. + if (metadata.IsAlgorithmSupported()) { + result.AppendElement(metadata); + } + } + + // Step 4. Return no metadata if empty is true, otherwise return result. + return result; +} + +bool nsCSPDirective::permits(CSPDirective aDirective, nsILoadInfo* aLoadInfo, + nsIURI* aUri, bool aWasRedirected, + bool aReportOnly, bool aUpgradeInsecure) const { + MOZ_ASSERT(equals(aDirective) || isDefaultDirective()); + + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG(("nsCSPDirective::permits, aUri: %s, aDirective: %s", + aUri->GetSpecOrDefault().get(), + CSP_CSPDirectiveToString(aDirective))); + } + + if (aLoadInfo) { + // https://w3c.github.io/webappsec-csp/#style-src-elem-pre-request + if (aDirective == CSPDirective::STYLE_SRC_ELEM_DIRECTIVE) { + // Step 3. If the result of executing §6.7.2.3 Does nonce match source + // list? on request’s cryptographic nonce metadata and this directive’s + // value is "Matches", return "Allowed". + if (DoesNonceMatchSourceList(aLoadInfo, mSrcs)) { + CSPUTILSLOG((" Allowed by matching nonce (style)")); + return true; + } + } + + // https://w3c.github.io/webappsec-csp/#script-pre-request + // Step 1. If request’s destination is script-like: + else if (aDirective == CSPDirective::SCRIPT_SRC_ELEM_DIRECTIVE || + aDirective == CSPDirective::WORKER_SRC_DIRECTIVE) { + // Step 1.1. If the result of executing §6.7.2.3 Does nonce match source + // list? on request’s cryptographic nonce metadata and this directive’s + // value is "Matches", return "Allowed". + if (DoesNonceMatchSourceList(aLoadInfo, mSrcs)) { + CSPUTILSLOG((" Allowed by matching nonce (script-like)")); + return true; + } + + // Step 1.2. Let integrity expressions be the set of source expressions in + // directive’s value that match the hash-source grammar. + nsTArray<nsCSPHashSrc*> integrityExpressions; + bool hasStrictDynamicKeyword = + false; // Optimization to reduce number of iterations. + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + if (mSrcs[i]->isHash()) { + integrityExpressions.AppendElement( + static_cast<nsCSPHashSrc*>(mSrcs[i])); + } else if (mSrcs[i]->isKeyword(CSP_STRICT_DYNAMIC)) { + hasStrictDynamicKeyword = true; + } + } + + // Step 1.3. If integrity expressions is not empty: + if (!integrityExpressions.IsEmpty()) { + // Step 1.3.1. Let integrity sources be the result of executing the + // algorithm defined in [SRI 3.3.3 Parse metadata] on request’s + // integrity metadata. + nsAutoString integrityMetadata; + aLoadInfo->GetIntegrityMetadata(integrityMetadata); + + nsTArray<SRIMetadata> integritySources = + ParseSRIMetadata(integrityMetadata); + MOZ_ASSERT( + integritySources.IsEmpty() == integrityMetadata.IsEmpty(), + "The integrity metadata should be only be empty, " + "when the parsed string was completely empty, otherwise it should " + "include at least one valid hash"); + + // Step 1.3.2. If integrity sources is "no metadata" or an empty set, + // skip the remaining substeps. + if (!integritySources.IsEmpty()) { + // Step 1.3.3. Let bypass due to integrity match be true. + bool bypass = true; + + nsAutoCString sourceAlgorithmUTF8; + nsAutoCString sourceHashUTF8; + nsAutoString sourceAlgorithm; + nsAutoString sourceHash; + nsAutoString algorithm; + nsAutoString hash; + + // Step 1.3.4. For each source of integrity sources: + for (const SRIMetadata& source : integritySources) { + source.GetAlgorithm(&sourceAlgorithmUTF8); + sourceAlgorithm = NS_ConvertUTF8toUTF16(sourceAlgorithmUTF8); + source.GetHash(0, &sourceHashUTF8); + sourceHash = NS_ConvertUTF8toUTF16(sourceHashUTF8); + + // Step 1.3.4.1 If directive’s value does not contain a source + // expression whose hash-algorithm is an ASCII case-insensitive + // match for source’s hash-algorithm, and whose base64-value is + // identical to source’s base64-value, then set bypass due to + // integrity match to false. + bool found = false; + for (const nsCSPHashSrc* hashSrc : integrityExpressions) { + hashSrc->getAlgorithm(algorithm); + hashSrc->getHash(hash); + + // The nsCSPHashSrc constructor lowercases algorithm, so this + // is case-insensitive. + if (sourceAlgorithm == algorithm && sourceHash == hash) { + found = true; + break; + } + } + + if (!found) { + bypass = false; + break; + } + } + + // Step 1.3.5. If bypass due to integrity match is true, return + // "Allowed". + if (bypass) { + CSPUTILSLOG( + (" Allowed by matching integrity metadata (script-like)")); + return true; + } + } + } + + // Step 1.4. If directive’s value contains a source expression that is an + // ASCII case-insensitive match for the "'strict-dynamic'" keyword-source: + + // XXX I don't think we should apply strict-dynamic to XSLT. + if (hasStrictDynamicKeyword && aLoadInfo->InternalContentPolicyType() != + nsIContentPolicy::TYPE_XSLT) { + // Step 1.4.1 If the request’s parser metadata is "parser-inserted", + // return "Blocked". Otherwise, return "Allowed". + if (aLoadInfo->GetParserCreatedScript()) { + CSPUTILSLOG( + (" Blocked by 'strict-dynamic' because parser-inserted")); + return false; + } + + CSPUTILSLOG( + (" Allowed by 'strict-dynamic' because not-parser-inserted")); + return true; + } + } + } + + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + if (mSrcs[i]->permits(aUri, aWasRedirected, aReportOnly, + aUpgradeInsecure)) { + return true; + } + } + return false; +} + +bool nsCSPDirective::allows(enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce) const { + CSPUTILSLOG(("nsCSPDirective::allows, aKeyWord: %s, aHashOrNonce: %s", + CSP_EnumToUTF8Keyword(aKeyword), + NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + if (mSrcs[i]->allows(aKeyword, aHashOrNonce)) { + return true; + } + } + return false; +} + +// https://w3c.github.io/webappsec-csp/#allow-all-inline +bool nsCSPDirective::allowsAllInlineBehavior(CSPDirective aDir) const { + // Step 1. Let allow all inline be false. + bool allowAll = false; + + // Step 2. For each expression of list: + for (nsCSPBaseSrc* src : mSrcs) { + // Step 2.1. If expression matches the nonce-source or hash-source grammar, + // return "Does Not Allow". + if (src->isNonce() || src->isHash()) { + return false; + } + + // Step 2.2. If type is "script", "script attribute" or "navigation" and + // expression matches the keyword-source "'strict-dynamic'", return "Does + // Not Allow". + if ((aDir == nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE || + aDir == nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) && + src->isKeyword(CSP_STRICT_DYNAMIC)) { + return false; + } + + // Step 2.3. If expression is an ASCII case-insensitive match for the + // keyword-source "'unsafe-inline'", set allow all inline to true. + if (src->isKeyword(CSP_UNSAFE_INLINE)) { + allowAll = true; + } + } + + // Step 3. If allow all inline is true, return "Allows". Otherwise, return + // "Does Not Allow". + return allowAll; +} + +void nsCSPDirective::toString(nsAString& outStr) const { + // Append directive name + outStr.AppendASCII(CSP_CSPDirectiveToString(mDirective)); + outStr.AppendLiteral(" "); + + // Append srcs + StringJoinAppend(outStr, u" "_ns, mSrcs, + [](nsAString& dest, nsCSPBaseSrc* cspBaseSrc) { + cspBaseSrc->toString(dest); + }); +} + +void nsCSPDirective::toDomCSPStruct(mozilla::dom::CSP& outCSP) const { + mozilla::dom::Sequence<nsString> srcs; + nsString src; + if (NS_WARN_IF(!srcs.SetCapacity(mSrcs.Length(), mozilla::fallible))) { + MOZ_ASSERT(false, + "Not enough memory for 'sources' sequence in " + "nsCSPDirective::toDomCSPStruct()."); + return; + } + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + src.Truncate(); + mSrcs[i]->toString(src); + if (!srcs.AppendElement(src, mozilla::fallible)) { + MOZ_ASSERT(false, + "Failed to append to 'sources' sequence in " + "nsCSPDirective::toDomCSPStruct()."); + } + } + + switch (mDirective) { + case nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE: + outCSP.mDefault_src.Construct(); + outCSP.mDefault_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE: + outCSP.mScript_src.Construct(); + outCSP.mScript_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE: + outCSP.mObject_src.Construct(); + outCSP.mObject_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE: + outCSP.mStyle_src.Construct(); + outCSP.mStyle_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::IMG_SRC_DIRECTIVE: + outCSP.mImg_src.Construct(); + outCSP.mImg_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::MEDIA_SRC_DIRECTIVE: + outCSP.mMedia_src.Construct(); + outCSP.mMedia_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE: + outCSP.mFrame_src.Construct(); + outCSP.mFrame_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::FONT_SRC_DIRECTIVE: + outCSP.mFont_src.Construct(); + outCSP.mFont_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::CONNECT_SRC_DIRECTIVE: + outCSP.mConnect_src.Construct(); + outCSP.mConnect_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE: + outCSP.mReport_uri.Construct(); + outCSP.mReport_uri.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::FRAME_ANCESTORS_DIRECTIVE: + outCSP.mFrame_ancestors.Construct(); + outCSP.mFrame_ancestors.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::WEB_MANIFEST_SRC_DIRECTIVE: + outCSP.mManifest_src.Construct(); + outCSP.mManifest_src.Value() = std::move(srcs); + return; + // not supporting REFLECTED_XSS_DIRECTIVE + + case nsIContentSecurityPolicy::BASE_URI_DIRECTIVE: + outCSP.mBase_uri.Construct(); + outCSP.mBase_uri.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::FORM_ACTION_DIRECTIVE: + outCSP.mForm_action.Construct(); + outCSP.mForm_action.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT: + outCSP.mBlock_all_mixed_content.Construct(); + // does not have any srcs + return; + + case nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE: + outCSP.mUpgrade_insecure_requests.Construct(); + // does not have any srcs + return; + + case nsIContentSecurityPolicy::CHILD_SRC_DIRECTIVE: + outCSP.mChild_src.Construct(); + outCSP.mChild_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::SANDBOX_DIRECTIVE: + outCSP.mSandbox.Construct(); + outCSP.mSandbox.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE: + outCSP.mWorker_src.Construct(); + outCSP.mWorker_src.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE: + outCSP.mScript_src_elem.Construct(); + outCSP.mScript_src_elem.Value() = std::move(srcs); + return; + + case nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE: + outCSP.mScript_src_attr.Construct(); + outCSP.mScript_src_attr.Value() = std::move(srcs); + return; + + default: + NS_ASSERTION(false, "cannot find directive to convert CSP to JSON"); + } +} + +void nsCSPDirective::getReportURIs(nsTArray<nsString>& outReportURIs) const { + NS_ASSERTION((mDirective == nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE), + "not a report-uri directive"); + + // append uris + nsString tmpReportURI; + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + tmpReportURI.Truncate(); + mSrcs[i]->toString(tmpReportURI); + outReportURIs.AppendElement(tmpReportURI); + } +} + +bool nsCSPDirective::visitSrcs(nsCSPSrcVisitor* aVisitor) const { + for (uint32_t i = 0; i < mSrcs.Length(); i++) { + if (!mSrcs[i]->visit(aVisitor)) { + return false; + } + } + return true; +} + +bool nsCSPDirective::equals(CSPDirective aDirective) const { + return (mDirective == aDirective); +} + +void nsCSPDirective::getDirName(nsAString& outStr) const { + outStr.AppendASCII(CSP_CSPDirectiveToString(mDirective)); +} + +bool nsCSPDirective::hasReportSampleKeyword() const { + for (nsCSPBaseSrc* src : mSrcs) { + if (src->isReportSample()) { + return true; + } + } + + return false; +} + +/* =============== nsCSPChildSrcDirective ============= */ + +nsCSPChildSrcDirective::nsCSPChildSrcDirective(CSPDirective aDirective) + : nsCSPDirective(aDirective), + mRestrictFrames(false), + mRestrictWorkers(false) {} + +nsCSPChildSrcDirective::~nsCSPChildSrcDirective() = default; + +bool nsCSPChildSrcDirective::equals(CSPDirective aDirective) const { + if (aDirective == nsIContentSecurityPolicy::FRAME_SRC_DIRECTIVE) { + return mRestrictFrames; + } + if (aDirective == nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE) { + return mRestrictWorkers; + } + return (mDirective == aDirective); +} + +/* =============== nsCSPScriptSrcDirective ============= */ + +nsCSPScriptSrcDirective::nsCSPScriptSrcDirective(CSPDirective aDirective) + : nsCSPDirective(aDirective) {} + +nsCSPScriptSrcDirective::~nsCSPScriptSrcDirective() = default; + +bool nsCSPScriptSrcDirective::equals(CSPDirective aDirective) const { + if (aDirective == nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE) { + return mRestrictWorkers; + } + if (aDirective == nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) { + return mRestrictScriptElem; + } + if (aDirective == nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) { + return mRestrictScriptAttr; + } + return mDirective == aDirective; +} + +/* =============== nsCSPStyleSrcDirective ============= */ + +nsCSPStyleSrcDirective::nsCSPStyleSrcDirective(CSPDirective aDirective) + : nsCSPDirective(aDirective) {} + +nsCSPStyleSrcDirective::~nsCSPStyleSrcDirective() = default; + +bool nsCSPStyleSrcDirective::equals(CSPDirective aDirective) const { + if (aDirective == nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE) { + return mRestrictStyleElem; + } + if (aDirective == nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE) { + return mRestrictStyleAttr; + } + return mDirective == aDirective; +} + +/* =============== nsBlockAllMixedContentDirective ============= */ + +nsBlockAllMixedContentDirective::nsBlockAllMixedContentDirective( + CSPDirective aDirective) + : nsCSPDirective(aDirective) {} + +nsBlockAllMixedContentDirective::~nsBlockAllMixedContentDirective() = default; + +void nsBlockAllMixedContentDirective::toString(nsAString& outStr) const { + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)); +} + +void nsBlockAllMixedContentDirective::getDirName(nsAString& outStr) const { + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::BLOCK_ALL_MIXED_CONTENT)); +} + +/* =============== nsUpgradeInsecureDirective ============= */ + +nsUpgradeInsecureDirective::nsUpgradeInsecureDirective(CSPDirective aDirective) + : nsCSPDirective(aDirective) {} + +nsUpgradeInsecureDirective::~nsUpgradeInsecureDirective() = default; + +void nsUpgradeInsecureDirective::toString(nsAString& outStr) const { + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)); +} + +void nsUpgradeInsecureDirective::getDirName(nsAString& outStr) const { + outStr.AppendASCII(CSP_CSPDirectiveToString( + nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)); +} + +/* ===== nsCSPPolicy ========================= */ + +nsCSPPolicy::nsCSPPolicy() + : mUpgradeInsecDir(nullptr), + mReportOnly(false), + mDeliveredViaMetaTag(false) { + CSPUTILSLOG(("nsCSPPolicy::nsCSPPolicy")); +} + +nsCSPPolicy::~nsCSPPolicy() { + CSPUTILSLOG(("nsCSPPolicy::~nsCSPPolicy")); + + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + delete mDirectives[i]; + } +} + +bool nsCSPPolicy::permits(CSPDirective aDir, nsILoadInfo* aLoadInfo, + nsIURI* aUri, bool aWasRedirected, bool aSpecific, + nsAString& outViolatedDirective) const { + if (CSPUTILSLOGENABLED()) { + CSPUTILSLOG(("nsCSPPolicy::permits, aUri: %s, aDir: %s, aSpecific: %s", + aUri->GetSpecOrDefault().get(), CSP_CSPDirectiveToString(aDir), + aSpecific ? "true" : "false")); + } + + NS_ASSERTION(aUri, "permits needs an uri to perform the check!"); + outViolatedDirective.Truncate(); + + nsCSPDirective* defaultDir = nullptr; + + // Try to find a relevant directive + // These directive arrays are short (1-5 elements), not worth using a + // hashtable. + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(aDir)) { + if (!mDirectives[i]->permits(aDir, aLoadInfo, aUri, aWasRedirected, + mReportOnly, mUpgradeInsecDir)) { + mDirectives[i]->getDirName(outViolatedDirective); + return false; + } + return true; + } + if (mDirectives[i]->isDefaultDirective()) { + defaultDir = mDirectives[i]; + } + } + + // If the above loop runs through, we haven't found a matching directive. + // Avoid relooping, just store the result of default-src while looping. + if (!aSpecific && defaultDir) { + if (!defaultDir->permits(aDir, aLoadInfo, aUri, aWasRedirected, mReportOnly, + mUpgradeInsecDir)) { + defaultDir->getDirName(outViolatedDirective); + return false; + } + return true; + } + + // Nothing restricts this, so we're allowing the load + // See bug 764937 + return true; +} + +bool nsCSPPolicy::allows(CSPDirective aDirective, enum CSPKeyword aKeyword, + const nsAString& aHashOrNonce) const { + CSPUTILSLOG(("nsCSPPolicy::allows, aKeyWord: %s, a HashOrNonce: %s", + CSP_EnumToUTF8Keyword(aKeyword), + NS_ConvertUTF16toUTF8(aHashOrNonce).get())); + + if (nsCSPDirective* directive = matchingOrDefaultDirective(aDirective)) { + return directive->allows(aKeyword, aHashOrNonce); + } + + // No matching directive or default directive as fallback found, thus + // allowing the load; see Bug 885433 + // a) inline scripts (also unsafe eval) should only be blocked + // if there is a [script-src] or [default-src] + // b) inline styles should only be blocked + // if there is a [style-src] or [default-src] + return true; +} + +nsCSPDirective* nsCSPPolicy::matchingOrDefaultDirective( + CSPDirective aDirective) const { + nsCSPDirective* defaultDir = nullptr; + + // Try to find a matching directive + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->isDefaultDirective()) { + defaultDir = mDirectives[i]; + continue; + } + if (mDirectives[i]->equals(aDirective)) { + return mDirectives[i]; + } + } + + return defaultDir; +} + +void nsCSPPolicy::toString(nsAString& outStr) const { + StringJoinAppend(outStr, u"; "_ns, mDirectives, + [](nsAString& dest, nsCSPDirective* cspDirective) { + cspDirective->toString(dest); + }); +} + +void nsCSPPolicy::toDomCSPStruct(mozilla::dom::CSP& outCSP) const { + outCSP.mReport_only = mReportOnly; + + for (uint32_t i = 0; i < mDirectives.Length(); ++i) { + mDirectives[i]->toDomCSPStruct(outCSP); + } +} + +bool nsCSPPolicy::hasDirective(CSPDirective aDir) const { + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(aDir)) { + return true; + } + } + return false; +} + +bool nsCSPPolicy::allowsAllInlineBehavior(CSPDirective aDir) const { + nsCSPDirective* directive = matchingOrDefaultDirective(aDir); + if (!directive) { + // No matching or default directive found thus allow the all inline + // scripts or styles. (See nsCSPPolicy::allows) + return true; + } + + return directive->allowsAllInlineBehavior(aDir); +} + +/* + * Use this function only after ::allows() returned 'false'. Most and + * foremost it's used to get the violated directive before sending reports. + * The parameter outDirective is the equivalent of 'outViolatedDirective' + * for the ::permits() function family. + */ +void nsCSPPolicy::getDirectiveStringAndReportSampleForContentType( + CSPDirective aDirective, nsAString& outDirective, + bool* aReportSample) const { + MOZ_ASSERT(aReportSample); + *aReportSample = false; + + nsCSPDirective* defaultDir = nullptr; + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->isDefaultDirective()) { + defaultDir = mDirectives[i]; + continue; + } + if (mDirectives[i]->equals(aDirective)) { + mDirectives[i]->getDirName(outDirective); + *aReportSample = mDirectives[i]->hasReportSampleKeyword(); + return; + } + } + // if we haven't found a matching directive yet, + // the contentType must be restricted by the default directive + if (defaultDir) { + defaultDir->getDirName(outDirective); + *aReportSample = defaultDir->hasReportSampleKeyword(); + return; + } + NS_ASSERTION(false, "Can not query directive string for contentType!"); + outDirective.AppendLiteral("couldNotQueryViolatedDirective"); +} + +void nsCSPPolicy::getDirectiveAsString(CSPDirective aDir, + nsAString& outDirective) const { + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(aDir)) { + mDirectives[i]->toString(outDirective); + return; + } + } +} + +/* + * Helper function that returns the underlying bit representation of sandbox + * flags. The function returns SANDBOXED_NONE if there are no sandbox + * directives. + */ +uint32_t nsCSPPolicy::getSandboxFlags() const { + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(nsIContentSecurityPolicy::SANDBOX_DIRECTIVE)) { + nsAutoString flags; + mDirectives[i]->toString(flags); + + if (flags.IsEmpty()) { + return SANDBOX_ALL_FLAGS; + } + + nsAttrValue attr; + attr.ParseAtomArray(flags); + + return nsContentUtils::ParseSandboxAttributeToFlags(&attr); + } + } + + return SANDBOXED_NONE; +} + +void nsCSPPolicy::getReportURIs(nsTArray<nsString>& outReportURIs) const { + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals( + nsIContentSecurityPolicy::REPORT_URI_DIRECTIVE)) { + mDirectives[i]->getReportURIs(outReportURIs); + return; + } + } +} + +bool nsCSPPolicy::visitDirectiveSrcs(CSPDirective aDir, + nsCSPSrcVisitor* aVisitor) const { + for (uint32_t i = 0; i < mDirectives.Length(); i++) { + if (mDirectives[i]->equals(aDir)) { + return mDirectives[i]->visitSrcs(aVisitor); + } + } + return false; +} |