gecko-dev/dom/security/nsContentSecurityUtils.cpp
2020-08-24 15:29:23 +00:00

1179 lines
42 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/. */
/* A namespace class for static content security utilities. */
#include "nsContentSecurityUtils.h"
#include "nsIContentSecurityPolicy.h"
#include "nsIChannel.h"
#include "nsIHttpChannel.h"
#include "nsIMultiPartChannel.h"
#include "nsIURI.h"
#include "nsITransfer.h"
#if defined(XP_WIN)
# include "WinUtils.h"
# include <wininet.h>
#endif
#include "js/Array.h" // JS::GetArrayLength
#include "mozilla/ExtensionPolicyService.h"
#include "mozilla/Logging.h"
#include "mozilla/dom/Document.h"
#include "LoadInfo.h"
#include "mozilla/StaticPrefs_extensions.h"
#include "mozilla/StaticPrefs_dom.h"
// Helper function for IsConsideredSameOriginForUIR which makes
// Principals of scheme 'http' return Principals of scheme 'https'.
static already_AddRefed<nsIPrincipal> MakeHTTPPrincipalHTTPS(
nsIPrincipal* aPrincipal) {
nsCOMPtr<nsIPrincipal> principal = aPrincipal;
// if the principal is not http, then it can also not be upgraded
// to https.
if (!principal->SchemeIs("http")) {
return principal.forget();
}
nsAutoCString spec;
aPrincipal->GetAsciiSpec(spec);
// replace http with https
spec.ReplaceLiteral(0, 4, "https");
nsCOMPtr<nsIURI> newURI;
nsresult rv = NS_NewURI(getter_AddRefs(newURI), spec);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
mozilla::OriginAttributes OA =
BasePrincipal::Cast(aPrincipal)->OriginAttributesRef();
principal = BasePrincipal::CreateContentPrincipal(newURI, OA);
return principal.forget();
}
/* static */
bool nsContentSecurityUtils::IsConsideredSameOriginForUIR(
nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aResultPrincipal) {
MOZ_ASSERT(aTriggeringPrincipal);
MOZ_ASSERT(aResultPrincipal);
// we only have to make sure that the following truth table holds:
// aTriggeringPrincipal | aResultPrincipal | Result
// ----------------------------------------------------------------
// http://example.com/foo.html | http://example.com/bar.html | true
// http://example.com/foo.html | https://example.com/bar.html | true
// https://example.com/foo.html | https://example.com/bar.html | true
// https://example.com/foo.html | http://example.com/bar.html | true
// fast path if both principals are same-origin
if (aTriggeringPrincipal->Equals(aResultPrincipal)) {
return true;
}
// in case a principal uses a scheme of 'http' then we just upgrade to
// 'https' and use the principal equals comparison operator to check
// for same-origin.
nsCOMPtr<nsIPrincipal> compareTriggeringPrincipal =
MakeHTTPPrincipalHTTPS(aTriggeringPrincipal);
nsCOMPtr<nsIPrincipal> compareResultPrincipal =
MakeHTTPPrincipalHTTPS(aResultPrincipal);
return compareTriggeringPrincipal->Equals(compareResultPrincipal);
}
/*
* Performs a Regular Expression match, optionally returning the results.
* This function is not safe to use OMT.
*
* @param aPattern The regex pattern
* @param aString The string to compare against
* @param aOnlyMatch Whether we want match results or only a true/false for
* the match
* @param aMatchResult Out param for whether or not the pattern matched
* @param aRegexResults Out param for the matches of the regex, if requested
* @returns nsresult indicating correct function operation or error
*/
nsresult RegexEval(const nsAString& aPattern, const nsAString& aString,
bool aOnlyMatch, bool& aMatchResult,
nsTArray<nsString>* aRegexResults = nullptr) {
MOZ_ASSERT(NS_IsMainThread());
aMatchResult = false;
AutoJSAPI jsapi;
jsapi.Init();
JSContext* cx = jsapi.cx();
AutoDisableJSInterruptCallback disabler(cx);
// We can use the junk scope here, because we're just using it for regexp
// evaluation, not actual script execution, and we disable statics so that the
// evaluation does not interact with the execution global.
JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
JS::RootedObject regexp(
cx, JS::NewUCRegExpObject(cx, aPattern.BeginReading(), aPattern.Length(),
JS::RegExpFlag::Unicode));
if (!regexp) {
return NS_ERROR_ILLEGAL_VALUE;
}
JS::RootedValue regexResult(cx, JS::NullValue());
size_t index = 0;
if (!JS::ExecuteRegExpNoStatics(cx, regexp, aString.BeginReading(),
aString.Length(), &index, aOnlyMatch,
&regexResult)) {
return NS_ERROR_FAILURE;
}
if (regexResult.isNull()) {
// On no match, ExecuteRegExpNoStatics returns Null
return NS_OK;
}
if (aOnlyMatch) {
// On match, with aOnlyMatch = true, ExecuteRegExpNoStatics returns boolean
// true.
MOZ_ASSERT(regexResult.isBoolean() && regexResult.toBoolean());
aMatchResult = true;
return NS_OK;
}
if (aRegexResults == nullptr) {
return NS_ERROR_INVALID_ARG;
}
// Now we know we have a result, and we need to extract it so we can read it.
uint32_t length;
JS::RootedObject regexResultObj(cx, &regexResult.toObject());
if (!JS::GetArrayLength(cx, regexResultObj, &length)) {
return NS_ERROR_NOT_AVAILABLE;
}
MOZ_LOG(sCSMLog, LogLevel::Verbose, ("Regex Matched %i strings", length));
for (uint32_t i = 0; i < length; i++) {
JS::RootedValue element(cx);
if (!JS_GetElement(cx, regexResultObj, i, &element)) {
return NS_ERROR_NO_CONTENT;
}
nsAutoJSString value;
if (!value.init(cx, element)) {
return NS_ERROR_NO_CONTENT;
}
MOZ_LOG(sCSMLog, LogLevel::Verbose,
("Regex Matching: %i: %s", i, NS_ConvertUTF16toUTF8(value).get()));
aRegexResults->AppendElement(value);
}
aMatchResult = true;
return NS_OK;
}
/*
* Telemetry Events extra data only supports 80 characters, so we optimize the
* filename to be smaller and collect more data.
*/
nsString OptimizeFileName(const nsAString& aFileName) {
nsString optimizedName(aFileName);
MOZ_LOG(
sCSMLog, LogLevel::Verbose,
("Optimizing FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get()));
optimizedName.ReplaceSubstring(u".xpi!"_ns, u"!"_ns);
optimizedName.ReplaceSubstring(u"shield.mozilla.org!"_ns, u"s!"_ns);
optimizedName.ReplaceSubstring(u"mozilla.org!"_ns, u"m!"_ns);
if (optimizedName.Length() > 80) {
optimizedName.Truncate(80);
}
MOZ_LOG(
sCSMLog, LogLevel::Verbose,
("Optimized FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get()));
return optimizedName;
}
/*
* FilenameToFilenameType takes a fileName and returns a Pair of strings.
* The First entry is a string indicating the type of fileName
* The Second entry is a Maybe<string> that can contain additional details to
* report.
*
* The reason we use strings (instead of an int/enum) is because the Telemetry
* Events API only accepts strings.
*
* Function is a static member of the class to enable gtests.
*/
/* static */
FilenameTypeAndDetails nsContentSecurityUtils::FilenameToFilenameType(
const nsString& fileName, bool collectAdditionalExtensionData) {
// These are strings because the Telemetry Events API only accepts strings
static constexpr auto kChromeURI = "chromeuri"_ns;
static constexpr auto kResourceURI = "resourceuri"_ns;
static constexpr auto kBlobUri = "bloburi"_ns;
static constexpr auto kDataUri = "dataurl"_ns;
static constexpr auto kSingleString = "singlestring"_ns;
static constexpr auto kMozillaExtension = "mozillaextension"_ns;
static constexpr auto kOtherExtension = "otherextension"_ns;
static constexpr auto kSuspectedUserChromeJS = "suspectedUserChromeJS"_ns;
#if defined(XP_WIN)
static constexpr auto kSanitizedWindowsURL = "sanitizedWindowsURL"_ns;
static constexpr auto kSanitizedWindowsPath = "sanitizedWindowsPath"_ns;
#endif
static constexpr auto kOther = "other"_ns;
static constexpr auto kOtherWorker = "other-on-worker"_ns;
static constexpr auto kRegexFailure = "regexfailure"_ns;
static constexpr auto kUCJSRegex = u"(.+).uc.js\\?*[0-9]*$"_ns;
static constexpr auto kExtensionRegex = u"extensions/(.+)@(.+)!(.+)$"_ns;
static constexpr auto kSingleFileRegex = u"^[a-zA-Z0-9.?]+$"_ns;
if (fileName.IsEmpty()) {
return FilenameTypeAndDetails(kOther, Nothing());
}
// resource:// and chrome://
if (StringBeginsWith(fileName, u"chrome://"_ns)) {
return FilenameTypeAndDetails(kChromeURI, Some(fileName));
}
if (StringBeginsWith(fileName, u"resource://"_ns)) {
return FilenameTypeAndDetails(kResourceURI, Some(fileName));
}
// blob: and data:
if (StringBeginsWith(fileName, u"blob:"_ns)) {
return FilenameTypeAndDetails(kBlobUri, Nothing());
}
if (StringBeginsWith(fileName, u"data:"_ns)) {
return FilenameTypeAndDetails(kDataUri, Nothing());
}
if (!NS_IsMainThread()) {
// We can't do Regex matching off the main thread; so just report.
return FilenameTypeAndDetails(kOtherWorker, Nothing());
}
// Extension
bool regexMatch;
nsTArray<nsString> regexResults;
nsresult rv = RegexEval(kExtensionRegex, fileName, /* aOnlyMatch = */ false,
regexMatch, &regexResults);
if (NS_FAILED(rv)) {
return FilenameTypeAndDetails(kRegexFailure, Nothing());
}
if (regexMatch) {
nsCString type = StringEndsWith(regexResults[2], u"mozilla.org.xpi"_ns)
? kMozillaExtension
: kOtherExtension;
auto& extensionNameAndPath =
Substring(regexResults[0], ArrayLength("extensions/") - 1);
return FilenameTypeAndDetails(type,
Some(OptimizeFileName(extensionNameAndPath)));
}
// Single File
rv = RegexEval(kSingleFileRegex, fileName, /* aOnlyMatch = */ true,
regexMatch);
if (NS_FAILED(rv)) {
return FilenameTypeAndDetails(kRegexFailure, Nothing());
}
if (regexMatch) {
return FilenameTypeAndDetails(kSingleString, Some(fileName));
}
// Suspected userChromeJS script
rv = RegexEval(kUCJSRegex, fileName, /* aOnlyMatch = */ true, regexMatch);
if (NS_FAILED(rv)) {
return FilenameTypeAndDetails(kRegexFailure, Nothing());
}
if (regexMatch) {
return FilenameTypeAndDetails(kSuspectedUserChromeJS, Nothing());
}
#if defined(XP_WIN)
auto flags = mozilla::widget::WinUtils::PathTransformFlags::Default |
mozilla::widget::WinUtils::PathTransformFlags::RequireFilePath;
nsAutoString strSanitizedPath(fileName);
if (widget::WinUtils::PreparePathForTelemetry(strSanitizedPath, flags)) {
DWORD cchDecodedUrl = INTERNET_MAX_URL_LENGTH;
WCHAR szOut[INTERNET_MAX_URL_LENGTH];
HRESULT hr =
::CoInternetParseUrl(fileName.get(), PARSE_SCHEMA, 0, szOut,
INTERNET_MAX_URL_LENGTH, &cchDecodedUrl, 0);
if (hr == S_OK && cchDecodedUrl) {
nsAutoString sanitizedPathAndScheme;
sanitizedPathAndScheme.Append(szOut);
if (sanitizedPathAndScheme == u"file"_ns) {
sanitizedPathAndScheme.Append(u"://.../"_ns);
sanitizedPathAndScheme.Append(strSanitizedPath);
} else if (sanitizedPathAndScheme == u"moz-extension"_ns &&
collectAdditionalExtensionData) {
sanitizedPathAndScheme.Append(u"://["_ns);
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), fileName);
if (NS_FAILED(rv)) {
// Return after adding ://[ so we know we failed here.
return FilenameTypeAndDetails(kSanitizedWindowsURL,
Some(sanitizedPathAndScheme));
}
mozilla::extensions::URLInfo url(uri);
if (NS_IsMainThread()) {
// EPS is only usable on main thread
auto* policy =
ExtensionPolicyService::GetSingleton().GetByHost(url.Host());
if (policy) {
nsString addOnId;
policy->GetId(addOnId);
sanitizedPathAndScheme.Append(addOnId);
sanitizedPathAndScheme.Append(u": "_ns);
sanitizedPathAndScheme.Append(policy->Name());
} else {
sanitizedPathAndScheme.Append(u"failed finding addon by host"_ns);
}
} else {
sanitizedPathAndScheme.Append(u"can't get addon off main thread"_ns);
}
sanitizedPathAndScheme.Append(u"]"_ns);
sanitizedPathAndScheme.Append(url.FilePath());
}
return FilenameTypeAndDetails(kSanitizedWindowsURL,
Some(sanitizedPathAndScheme));
} else {
return FilenameTypeAndDetails(kSanitizedWindowsPath,
Some(strSanitizedPath));
}
}
#endif
return FilenameTypeAndDetails(kOther, Nothing());
}
class EvalUsageNotificationRunnable final : public Runnable {
public:
EvalUsageNotificationRunnable(bool aIsSystemPrincipal,
NS_ConvertUTF8toUTF16& aFileNameA,
uint64_t aWindowID, uint32_t aLineNumber,
uint32_t aColumnNumber)
: mozilla::Runnable("EvalUsageNotificationRunnable"),
mIsSystemPrincipal(aIsSystemPrincipal),
mFileNameA(aFileNameA),
mWindowID(aWindowID),
mLineNumber(aLineNumber),
mColumnNumber(aColumnNumber) {}
NS_IMETHOD Run() override {
nsContentSecurityUtils::NotifyEvalUsage(
mIsSystemPrincipal, mFileNameA, mWindowID, mLineNumber, mColumnNumber);
return NS_OK;
}
void Revoke() {}
private:
bool mIsSystemPrincipal;
NS_ConvertUTF8toUTF16 mFileNameA;
uint64_t mWindowID;
uint32_t mLineNumber;
uint32_t mColumnNumber;
};
// The Web Extension process pref may be toggled during a session, at which
// point stuff may be loaded in the parent process but we would send telemetry
// for it. Avoid this by observing if the pref ever was disabled.
static bool sWebExtensionsRemoteWasEverDisabled = false;
/* static */
bool nsContentSecurityUtils::IsEvalAllowed(JSContext* cx,
bool aIsSystemPrincipal,
const nsAString& aScript) {
// This allowlist contains files that are permanently allowed to use
// eval()-like functions. It will ideally be restricted to files that are
// exclusively used in testing contexts.
static nsLiteralCString evalAllowlist[] = {
// Test-only third-party library
"resource://testing-common/sinon-7.2.7.js"_ns,
// Test-only third-party library
"resource://testing-common/ajv-4.1.1.js"_ns,
// Test-only utility
"resource://testing-common/content-task.js"_ns,
// Tracked by Bug 1584605
"resource:///modules/translation/cld-worker.js"_ns,
// require.js implements a script loader for workers. It uses eval
// to load the script; but injection is only possible in situations
// that you could otherwise control script that gets executed, so
// it is okay to allow eval() as it adds no additional attack surface.
// Bug 1584564 tracks requiring safe usage of require.js
"resource://gre/modules/workers/require.js"_ns,
// The Browser Toolbox/Console
"debugger"_ns,
};
// We also permit two specific idioms in eval()-like contexts. We'd like to
// elminate these too; but there are in-the-wild Mozilla privileged extensions
// that use them.
static constexpr auto sAllowedEval1 = u"this"_ns;
static constexpr auto sAllowedEval2 =
u"function anonymous(\n) {\nreturn this\n}"_ns;
if (MOZ_LIKELY(!aIsSystemPrincipal && !XRE_IsE10sParentProcess())) {
// We restrict eval in the system principal and parent process.
// Other uses (like web content and null principal) are allowed.
return true;
}
if (JS::ContextOptionsRef(cx).disableEvalSecurityChecks()) {
MOZ_LOG(sCSMLog, LogLevel::Debug,
("Allowing eval() because this JSContext was set to allow it"));
return true;
}
if (aIsSystemPrincipal &&
StaticPrefs::security_allow_eval_with_system_principal()) {
MOZ_LOG(sCSMLog, LogLevel::Debug,
("Allowing eval() with System Principal because allowing pref is "
"enabled"));
return true;
}
if (XRE_IsE10sParentProcess() &&
StaticPrefs::security_allow_eval_in_parent_process()) {
MOZ_LOG(sCSMLog, LogLevel::Debug,
("Allowing eval() in parent process because allowing pref is "
"enabled"));
return true;
}
// We only perform a check of this preference on the Main Thread
// (because a String-based preference check is only safe on Main Thread.)
// The consequence of this is that if a user is using userChromeJS _and_
// the scripts they use start a worker and that worker uses eval - we will
// enter this function, skip over this pref check that would normally cause
// us to allow the eval usage - and we will block it.
// While not ideal, we do not officially support userChromeJS, and hopefully
// the usage of workers and eval in workers is even lower that userChromeJS
// usage.
if (NS_IsMainThread()) {
// This preference is a file used for autoconfiguration of Firefox
// by administrators. It has also been (ab)used by the userChromeJS
// project to run legacy-style 'extensions', some of which use eval,
// all of which run in the System Principal context.
nsAutoString jsConfigPref;
Preferences::GetString("general.config.filename", jsConfigPref);
if (!jsConfigPref.IsEmpty()) {
MOZ_LOG(sCSMLog, LogLevel::Debug,
("Allowing eval() %s because of "
"general.config.filename",
(aIsSystemPrincipal ? "with System Principal"
: "in parent process")));
return true;
}
}
if (XRE_IsE10sParentProcess() &&
!StaticPrefs::extensions_webextensions_remote()) {
sWebExtensionsRemoteWasEverDisabled = true;
MOZ_LOG(sCSMLog, LogLevel::Debug,
("Allowing eval() in parent process because the web extension "
"process is disabled"));
return true;
}
if (XRE_IsE10sParentProcess() && sWebExtensionsRemoteWasEverDisabled) {
MOZ_LOG(sCSMLog, LogLevel::Debug,
("Allowing eval() in parent process because the web extension "
"process was disabled at some point"));
return true;
}
// We permit these two common idioms to get access to the global JS object
if (!aScript.IsEmpty() &&
(aScript == sAllowedEval1 || aScript == sAllowedEval2)) {
MOZ_LOG(
sCSMLog, LogLevel::Debug,
("Allowing eval() %s because a key string is "
"provided",
(aIsSystemPrincipal ? "with System Principal" : "in parent process")));
return true;
}
// Check the allowlist for the provided filename. getFilename is a helper
// function
nsAutoCString fileName;
uint32_t lineNumber = 0, columnNumber = 0;
nsJSUtils::GetCallingLocation(cx, fileName, &lineNumber, &columnNumber);
if (fileName.IsEmpty()) {
fileName = "unknown-file"_ns;
}
NS_ConvertUTF8toUTF16 fileNameA(fileName);
for (const nsLiteralCString& allowlistEntry : evalAllowlist) {
// checking if current filename begins with entry, because JS Engine
// gives us additional stuff for code inside eval or Function ctor
// e.g., "require.js > Function"
if (StringBeginsWith(fileName, allowlistEntry)) {
MOZ_LOG(sCSMLog, LogLevel::Debug,
("Allowing eval() %s because the containing "
"file is in the allowlist",
(aIsSystemPrincipal ? "with System Principal"
: "in parent process")));
return true;
}
}
// Send Telemetry and Log to the Console
uint64_t windowID = nsJSUtils::GetCurrentlyRunningCodeInnerWindowID(cx);
if (NS_IsMainThread()) {
nsContentSecurityUtils::NotifyEvalUsage(aIsSystemPrincipal, fileNameA,
windowID, lineNumber, columnNumber);
} else {
auto runnable = new EvalUsageNotificationRunnable(
aIsSystemPrincipal, fileNameA, windowID, lineNumber, columnNumber);
NS_DispatchToMainThread(runnable);
}
// Log to MOZ_LOG
MOZ_LOG(sCSMLog, LogLevel::Warning,
("Blocking eval() %s from file %s and script "
"provided %s",
(aIsSystemPrincipal ? "with System Principal" : "in parent process"),
fileName.get(), NS_ConvertUTF16toUTF8(aScript).get()));
// Maybe Crash
#ifdef DEBUG
// MOZ_CRASH_UNSAFE_PRINTF gives us at most 1024 characters to print.
// The given string literal leaves us with ~950, so I'm leaving
// each 475 for fileName and aScript each.
if (fileName.Length() > 475) {
fileName.SetLength(475);
}
nsAutoCString trimmedScript = NS_ConvertUTF16toUTF8(aScript);
if (trimmedScript.Length() > 475) {
trimmedScript.SetLength(475);
}
MOZ_CRASH_UNSAFE_PRINTF(
"Blocking eval() %s from file %s and script provided "
"%s",
(aIsSystemPrincipal ? "with System Principal" : "in parent process"),
fileName.get(), trimmedScript.get());
#endif
#ifdef EARLY_BETA_OR_EARLIER
// Until we understand the events coming from release, we don't want to
// enforce eval restrictions on release. Limiting to Nightly and early beta.
return false;
#else
return true;
#endif
}
/* static */
void nsContentSecurityUtils::NotifyEvalUsage(bool aIsSystemPrincipal,
NS_ConvertUTF8toUTF16& aFileNameA,
uint64_t aWindowID,
uint32_t aLineNumber,
uint32_t aColumnNumber) {
// Send Telemetry
Telemetry::EventID eventType =
aIsSystemPrincipal ? Telemetry::EventID::Security_Evalusage_Systemcontext
: Telemetry::EventID::Security_Evalusage_Parentprocess;
FilenameTypeAndDetails fileNameTypeAndDetails =
FilenameToFilenameType(aFileNameA, false);
mozilla::Maybe<nsTArray<EventExtraEntry>> extra;
if (fileNameTypeAndDetails.second.isSome()) {
extra = Some<nsTArray<EventExtraEntry>>({EventExtraEntry{
"fileinfo"_ns,
NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())}});
} else {
extra = Nothing();
}
if (!sTelemetryEventEnabled.exchange(true)) {
sTelemetryEventEnabled = true;
Telemetry::SetEventRecordingEnabled("security"_ns, true);
}
Telemetry::RecordEvent(eventType, mozilla::Some(fileNameTypeAndDetails.first),
extra);
// Report an error to console
nsCOMPtr<nsIConsoleService> console(
do_GetService(NS_CONSOLESERVICE_CONTRACTID));
if (!console) {
return;
}
nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
if (!error) {
return;
}
nsCOMPtr<nsIStringBundle> bundle;
nsCOMPtr<nsIStringBundleService> stringService =
mozilla::services::GetStringBundleService();
if (!stringService) {
return;
}
stringService->CreateBundle(
"chrome://global/locale/security/security.properties",
getter_AddRefs(bundle));
if (!bundle) {
return;
}
nsAutoString message;
AutoTArray<nsString, 1> formatStrings = {aFileNameA};
nsresult rv = bundle->FormatStringFromName("RestrictBrowserEvalUsage",
formatStrings, message);
if (NS_FAILED(rv)) {
return;
}
rv = error->InitWithWindowID(message, aFileNameA, EmptyString(), aLineNumber,
aColumnNumber, nsIScriptError::errorFlag,
"BrowserEvalUsage", aWindowID,
true /* From chrome context */);
if (NS_FAILED(rv)) {
return;
}
console->LogMessage(error);
}
/* static */
nsresult nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart(
nsIChannel* aChannel, nsIHttpChannel** aHttpChannel) {
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
if (httpChannel) {
httpChannel.forget(aHttpChannel);
return NS_OK;
}
nsCOMPtr<nsIMultiPartChannel> multipart = do_QueryInterface(aChannel);
if (!multipart) {
*aHttpChannel = nullptr;
return NS_OK;
}
nsCOMPtr<nsIChannel> baseChannel;
nsresult rv = multipart->GetBaseChannel(getter_AddRefs(baseChannel));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
httpChannel = do_QueryInterface(baseChannel);
httpChannel.forget(aHttpChannel);
return NS_OK;
}
nsresult ParseCSPAndEnforceFrameAncestorCheck(
nsIChannel* aChannel, nsIContentSecurityPolicy** aOutCSP) {
MOZ_ASSERT(aChannel);
// CSP can only hang off an http channel, if this channel is not
// an http channel then there is nothing to do here.
nsCOMPtr<nsIHttpChannel> httpChannel;
nsresult rv = nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart(
aChannel, getter_AddRefs(httpChannel));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!httpChannel) {
return NS_OK;
}
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
nsContentPolicyType contentType = loadInfo->GetExternalContentPolicyType();
// frame-ancestor check only makes sense for subdocument and object loads,
// if this is not a load of such type, there is nothing to do here.
if (contentType != nsIContentPolicy::TYPE_SUBDOCUMENT &&
contentType != nsIContentPolicy::TYPE_OBJECT) {
return NS_OK;
}
nsAutoCString tCspHeaderValue, tCspROHeaderValue;
Unused << httpChannel->GetResponseHeader("content-security-policy"_ns,
tCspHeaderValue);
Unused << httpChannel->GetResponseHeader(
"content-security-policy-report-only"_ns, tCspROHeaderValue);
// if there are no CSP values, then there is nothing to do here.
if (tCspHeaderValue.IsEmpty() && tCspROHeaderValue.IsEmpty()) {
return NS_OK;
}
NS_ConvertASCIItoUTF16 cspHeaderValue(tCspHeaderValue);
NS_ConvertASCIItoUTF16 cspROHeaderValue(tCspROHeaderValue);
RefPtr<nsCSPContext> csp = new nsCSPContext();
nsCOMPtr<nsIPrincipal> resultPrincipal;
rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
aChannel, getter_AddRefs(resultPrincipal));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIURI> selfURI;
aChannel->GetURI(getter_AddRefs(selfURI));
nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo();
nsAutoString referrerSpec;
if (referrerInfo) {
referrerInfo->GetComputedReferrerSpec(referrerSpec);
}
uint64_t innerWindowID = loadInfo->GetInnerWindowID();
rv = csp->SetRequestContextWithPrincipal(resultPrincipal, selfURI,
referrerSpec, innerWindowID);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// ----- if there's a full-strength CSP header, apply it.
if (!cspHeaderValue.IsEmpty()) {
rv = CSP_AppendCSPFromHeader(csp, cspHeaderValue, false);
NS_ENSURE_SUCCESS(rv, rv);
}
// ----- if there's a report-only CSP header, apply it.
if (!cspROHeaderValue.IsEmpty()) {
rv = CSP_AppendCSPFromHeader(csp, cspROHeaderValue, true);
NS_ENSURE_SUCCESS(rv, rv);
}
// ----- Enforce frame-ancestor policy on any applied policies
bool safeAncestry = false;
// PermitsAncestry sends violation reports when necessary
rv = csp->PermitsAncestry(loadInfo, &safeAncestry);
if (NS_FAILED(rv) || !safeAncestry) {
// stop! ERROR page!
aChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION);
return NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION;
}
// return the CSP for x-frame-options check
csp.forget(aOutCSP);
return NS_OK;
}
void EnforceXFrameOptionsCheck(nsIChannel* aChannel,
nsIContentSecurityPolicy* aCsp) {
MOZ_ASSERT(aChannel);
if (!FramingChecker::CheckFrameOptions(aChannel, aCsp)) {
// stop! ERROR page!
aChannel->Cancel(NS_ERROR_XFO_VIOLATION);
}
}
/* static */
void nsContentSecurityUtils::PerformCSPFrameAncestorAndXFOCheck(
nsIChannel* aChannel) {
nsCOMPtr<nsIContentSecurityPolicy> csp;
nsresult rv =
ParseCSPAndEnforceFrameAncestorCheck(aChannel, getter_AddRefs(csp));
if (NS_FAILED(rv)) {
return;
}
// X-Frame-Options needs to be enforced after CSP frame-ancestors
// checks because if frame-ancestors is present, then x-frame-options
// will be discarded
EnforceXFrameOptionsCheck(aChannel, csp);
}
#if defined(DEBUG)
/* static */
void nsContentSecurityUtils::AssertAboutPageHasCSP(Document* aDocument) {
// We want to get to a point where all about: pages ship with a CSP. This
// assertion ensures that we can not deploy new about: pages without a CSP.
// Please note that any about: page should not use inline JS or inline CSS,
// and instead should load JS and CSS from an external file (*.js, *.css)
// which allows us to apply a strong CSP omitting 'unsafe-inline'. Ideally,
// the CSP allows precisely the resources that need to be loaded; but it
// should at least be as strong as:
// <meta http-equiv="Content-Security-Policy" content="default-src chrome:;
// object-src 'none'"/>
// Check if we should skip the assertion
if (StaticPrefs::dom_security_skip_about_page_has_csp_assert()) {
return;
}
// Check if we are loading an about: URI at all
nsCOMPtr<nsIURI> documentURI = aDocument->GetDocumentURI();
if (!documentURI->SchemeIs("about")) {
return;
}
nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp();
bool foundDefaultSrc = false;
bool foundObjectSrc = false;
bool foundUnsafeEval = false;
bool foundUnsafeInline = false;
bool foundScriptSrc = false;
bool foundWorkerSrc = false;
bool foundWebScheme = false;
if (csp) {
uint32_t policyCount = 0;
csp->GetPolicyCount(&policyCount);
nsAutoString parsedPolicyStr;
for (uint32_t i = 0; i < policyCount; ++i) {
csp->GetPolicyString(i, parsedPolicyStr);
if (parsedPolicyStr.Find("default-src") >= 0) {
foundDefaultSrc = true;
}
if (parsedPolicyStr.Find("object-src 'none'") >= 0) {
foundObjectSrc = true;
}
if (parsedPolicyStr.Find("'unsafe-eval'") >= 0) {
foundUnsafeEval = true;
}
if (parsedPolicyStr.Find("'unsafe-inline'") >= 0) {
foundUnsafeInline = true;
}
if (parsedPolicyStr.Find("script-src") >= 0) {
foundScriptSrc = true;
}
if (parsedPolicyStr.Find("worker-src") >= 0) {
foundWorkerSrc = true;
}
if (parsedPolicyStr.Find("http:") >= 0 ||
parsedPolicyStr.Find("https:") >= 0) {
foundWebScheme = true;
}
}
}
// Check if we should skip the allowlist and assert right away. Please note
// that this pref can and should only be set for automated testing.
if (StaticPrefs::dom_security_skip_about_page_csp_allowlist_and_assert()) {
NS_ASSERTION(foundDefaultSrc, "about: page must have a CSP");
return;
}
nsAutoCString aboutSpec;
documentURI->GetSpec(aboutSpec);
ToLowerCase(aboutSpec);
// This allowlist contains about: pages that are permanently allowed to
// render without a CSP applied.
static nsLiteralCString sAllowedAboutPagesWithNoCSP[] = {
// about:blank is a special about page -> no CSP
"about:blank"_ns,
// about:srcdoc is a special about page -> no CSP
"about:srcdoc"_ns,
// about:sync-log displays plain text only -> no CSP
"about:sync-log"_ns,
// about:printpreview displays plain text only -> no CSP
"about:printpreview"_ns,
// about:logo just displays the firefox logo -> no CSP
"about:logo"_ns,
# if defined(ANDROID)
"about:config"_ns,
# endif
};
for (const nsLiteralCString& allowlistEntry : sAllowedAboutPagesWithNoCSP) {
// please note that we perform a substring match here on purpose,
// so we don't have to deal and parse out all the query arguments
// the various about pages rely on.
if (StringBeginsWith(aboutSpec, allowlistEntry)) {
return;
}
}
MOZ_ASSERT(foundDefaultSrc,
"about: page must contain a CSP including default-src");
MOZ_ASSERT(foundObjectSrc,
"about: page must contain a CSP denying object-src");
// preferences and downloads allow legacy inline scripts through hash src.
MOZ_ASSERT(!foundScriptSrc ||
StringBeginsWith(aboutSpec, "about:preferences"_ns) ||
StringBeginsWith(aboutSpec, "about:downloads"_ns) ||
StringBeginsWith(aboutSpec, "about:newtab"_ns) ||
StringBeginsWith(aboutSpec, "about:logins"_ns) ||
StringBeginsWith(aboutSpec, "about:compat"_ns) ||
StringBeginsWith(aboutSpec, "about:welcome"_ns) ||
StringBeginsWith(aboutSpec, "about:profiling"_ns) ||
StringBeginsWith(aboutSpec, "about:studies"_ns) ||
StringBeginsWith(aboutSpec, "about:home"_ns),
"about: page must not contain a CSP including script-src");
MOZ_ASSERT(!foundWorkerSrc,
"about: page must not contain a CSP including worker-src");
// addons, preferences, debugging, newinstall, pioneer, devtools all have
// to allow some remote web resources
MOZ_ASSERT(!foundWebScheme ||
StringBeginsWith(aboutSpec, "about:preferences"_ns) ||
StringBeginsWith(aboutSpec, "about:addons"_ns) ||
StringBeginsWith(aboutSpec, "about:newtab"_ns) ||
StringBeginsWith(aboutSpec, "about:debugging"_ns) ||
StringBeginsWith(aboutSpec, "about:newinstall"_ns) ||
StringBeginsWith(aboutSpec, "about:pioneer"_ns) ||
StringBeginsWith(aboutSpec, "about:compat"_ns) ||
StringBeginsWith(aboutSpec, "about:logins"_ns) ||
StringBeginsWith(aboutSpec, "about:home"_ns) ||
StringBeginsWith(aboutSpec, "about:welcome"_ns) ||
StringBeginsWith(aboutSpec, "about:devtools"_ns),
"about: page must not contain a CSP including a web scheme");
if (aDocument->IsExtensionPage()) {
// Extensions have two CSP policies applied where the baseline CSP
// includes 'unsafe-eval' and 'unsafe-inline', hence we have to skip
// the 'unsafe-eval' and 'unsafe-inline' assertions for extension
// pages.
return;
}
MOZ_ASSERT(!foundUnsafeEval,
"about: page must not contain a CSP including 'unsafe-eval'");
static nsLiteralCString sLegacyUnsafeInlineAllowList[] = {
// Bug 1579160: Remove 'unsafe-inline' from style-src within
// about:preferences
"about:preferences"_ns,
// Bug 1571346: Remove 'unsafe-inline' from style-src within about:addons
"about:addons"_ns,
// Bug 1584485: Remove 'unsafe-inline' from style-src within:
// * about:newtab
// * about:welcome
// * about:home
"about:newtab"_ns,
"about:welcome"_ns,
"about:home"_ns,
};
for (const nsLiteralCString& aUnsafeInlineEntry :
sLegacyUnsafeInlineAllowList) {
// please note that we perform a substring match here on purpose,
// so we don't have to deal and parse out all the query arguments
// the various about pages rely on.
if (StringBeginsWith(aboutSpec, aUnsafeInlineEntry)) {
return;
}
}
MOZ_ASSERT(!foundUnsafeInline,
"about: page must not contain a CSP including 'unsafe-inline'");
}
#endif
/* static */
bool nsContentSecurityUtils::ValidateScriptFilename(const char* aFilename,
bool aIsSystemRealm) {
static Maybe<bool> sGeneralConfigFilenameSet;
// If the pref is permissive, allow everything
if (StaticPrefs::security_allow_parent_unrestricted_js_loads()) {
return true;
}
// If we're not in the parent process allow everything (presently)
if (!XRE_IsE10sParentProcess()) {
return true;
}
// We only perform a check of this preference on the Main Thread
// (because a String-based preference check is only safe on Main Thread.)
// The consequence of this is that if a user is using userChromeJS _and_
// the scripts they use start a worker - we will enter this function,
// skip over this pref check that would normally cause us to allow the
// load - and we will block it.
// While not ideal, we do not officially support userChromeJS, and hopefully
// the usage of workers is even lower than userChromeJS usage.
if (NS_IsMainThread()) {
// This preference is a file used for autoconfiguration of Firefox
// by administrators. It will also run in the parent process and throw
// assumptions about what can run where out of the window.
if (!sGeneralConfigFilenameSet.isSome()) {
nsAutoString jsConfigPref;
Preferences::GetString("general.config.filename", jsConfigPref);
sGeneralConfigFilenameSet.emplace(!jsConfigPref.IsEmpty());
}
if (sGeneralConfigFilenameSet.value()) {
MOZ_LOG(sCSMLog, LogLevel::Debug,
("Allowing a javascript load of %s because "
"general.config.filename is set",
aFilename));
return true;
}
}
if (XRE_IsE10sParentProcess() &&
!StaticPrefs::extensions_webextensions_remote()) {
sWebExtensionsRemoteWasEverDisabled = true;
MOZ_LOG(sCSMLog, LogLevel::Debug,
("Allowing a javascript load of %s because the web extension "
"process is disabled.",
aFilename));
return true;
}
if (XRE_IsE10sParentProcess() && sWebExtensionsRemoteWasEverDisabled) {
MOZ_LOG(sCSMLog, LogLevel::Debug,
("Allowing a javascript load of %s because the web extension "
"process was disabled at some point.",
aFilename));
return true;
}
NS_ConvertUTF8toUTF16 filenameU(aFilename);
if (StringBeginsWith(filenameU, u"chrome://"_ns)) {
// If it's a chrome:// url, allow it
return true;
}
if (StringBeginsWith(filenameU, u"resource://"_ns)) {
// If it's a resource:// url, allow it
return true;
}
if (StringBeginsWith(filenameU, u"file://"_ns)) {
// We will temporarily allow all file:// URIs through for now
return true;
}
if (StringBeginsWith(filenameU, u"jar:file://"_ns)) {
// We will temporarily allow all jar URIs through for now
return true;
}
if (filenameU.Equals(u"about:sync-log"_ns)) {
// about:sync-log runs in the parent process and displays a directory
// listing. The listing has inline javascript that executes on load.
return true;
}
// Log to MOZ_LOG
MOZ_LOG(sCSMLog, LogLevel::Info,
("ValidateScriptFilename System:%i %s\n", (aIsSystemRealm ? 1 : 0),
aFilename));
// Send Telemetry
FilenameTypeAndDetails fileNameTypeAndDetails =
FilenameToFilenameType(filenameU, true);
Telemetry::EventID eventType =
Telemetry::EventID::Security_Javascriptload_Parentprocess;
mozilla::Maybe<nsTArray<EventExtraEntry>> extra;
if (fileNameTypeAndDetails.second.isSome()) {
extra = Some<nsTArray<EventExtraEntry>>({EventExtraEntry{
"fileinfo"_ns,
NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())}});
} else {
extra = Nothing();
}
if (!sTelemetryEventEnabled.exchange(true)) {
sTelemetryEventEnabled = true;
Telemetry::SetEventRecordingEnabled("security"_ns, true);
}
Telemetry::RecordEvent(eventType, mozilla::Some(fileNameTypeAndDetails.first),
extra);
// Presently we are not enforcing any restrictions for the script filename,
// we're only reporting Telemetry. In the future we will assert in debug
// builds and return false to prevent execution in non-debug builds.
return true;
}
/* static */
void nsContentSecurityUtils::LogMessageToConsole(nsIHttpChannel* aChannel,
const char* aMsg) {
nsCOMPtr<nsIURI> uri;
nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
if (NS_FAILED(rv)) {
return;
}
uint64_t windowID = 0;
rv = aChannel->GetTopLevelContentWindowId(&windowID);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
if (!windowID) {
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
loadInfo->GetInnerWindowID(&windowID);
}
nsAutoString localizedMsg;
nsAutoCString spec;
uri->GetSpec(spec);
AutoTArray<nsString, 1> params = {NS_ConvertUTF8toUTF16(spec)};
rv = nsContentUtils::FormatLocalizedString(
nsContentUtils::eSECURITY_PROPERTIES, aMsg, params, localizedMsg);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
nsContentUtils::ReportToConsoleByWindowID(
localizedMsg, nsIScriptError::warningFlag, "Security"_ns, windowID, uri);
}
/* static */
long nsContentSecurityUtils::ClassifyDownload(
nsIChannel* aChannel, const nsAutoCString& aMimeTypeGuess) {
MOZ_ASSERT(aChannel, "IsDownloadAllowed without channel?");
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
nsCOMPtr<nsIURI> contentLocation;
aChannel->GetURI(getter_AddRefs(contentLocation));
nsCOMPtr<nsIPrincipal> loadingPrincipal = loadInfo->GetLoadingPrincipal();
if (!loadingPrincipal) {
loadingPrincipal = loadInfo->TriggeringPrincipal();
}
// Creating a fake Loadinfo that is just used for the MCB check.
nsCOMPtr<nsILoadInfo> secCheckLoadInfo =
new LoadInfo(loadingPrincipal, loadInfo->TriggeringPrincipal(), nullptr,
nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD);
int16_t decission = nsIContentPolicy::ACCEPT;
nsMixedContentBlocker::ShouldLoad(false, // aHadInsecureImageRedirect
contentLocation, // aContentLocation,
secCheckLoadInfo, // aLoadinfo
aMimeTypeGuess, // aMimeGuess,
false, // aReportError
&decission // aDecision
);
Telemetry::Accumulate(mozilla::Telemetry::MIXED_CONTENT_DOWNLOADS,
decission != nsIContentPolicy::ACCEPT);
if (StaticPrefs::dom_block_download_insecure() &&
decission != nsIContentPolicy::ACCEPT) {
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
if (httpChannel) {
LogMessageToConsole(httpChannel, "MixedContentBlockedDownload");
}
return nsITransfer::DOWNLOAD_POTENTIALLY_UNSAFE;
}
if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) {
return nsITransfer::DOWNLOAD_ACCEPTABLE;
}
if (!StaticPrefs::dom_block_download_in_sandboxed_iframes()) {
return nsITransfer::DOWNLOAD_ACCEPTABLE;
}
uint32_t triggeringFlags = loadInfo->GetTriggeringSandboxFlags();
uint32_t currentflags = loadInfo->GetSandboxFlags();
if ((triggeringFlags & SANDBOXED_ALLOW_DOWNLOADS) ||
(currentflags & SANDBOXED_ALLOW_DOWNLOADS)) {
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
if (httpChannel) {
LogMessageToConsole(httpChannel, "IframeSandboxBlockedDownload");
}
return nsITransfer::DOWNLOAD_FORBIDDEN;
}
return nsITransfer::DOWNLOAD_ACCEPTABLE;
}