/* -*- 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 "mozilla/Components.h" #include "mozilla/dom/nsMixedContentBlocker.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/WorkerCommon.h" #include "mozilla/dom/WorkerPrivate.h" #include "nsComponentManagerUtils.h" #include "nsIContentSecurityPolicy.h" #include "nsIChannel.h" #include "nsIHttpChannel.h" #include "nsIMultiPartChannel.h" #include "nsIURI.h" #include "nsITransfer.h" #include "nsNetUtil.h" #include "nsSandboxFlags.h" #if defined(XP_WIN) # include "mozilla/WinHeaderOnlyUtils.h" # include "WinUtils.h" # include #endif #include "FramingChecker.h" #include "js/Array.h" // JS::GetArrayLength #include "js/ContextOptions.h" #include "js/PropertyAndElement.h" // JS_GetElement #include "js/RegExp.h" #include "js/RegExpFlags.h" // JS::RegExpFlags #include "js/friend/ErrorMessages.h" // JSMSG_UNSAFE_FILENAME #include "mozilla/ExtensionPolicyService.h" #include "mozilla/Logging.h" #include "mozilla/Preferences.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/nsCSPContext.h" #include "mozilla/StaticPrefs_security.h" #include "LoadInfo.h" #include "mozilla/StaticPrefs_extensions.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/Telemetry.h" #include "mozilla/TelemetryComms.h" #include "mozilla/TelemetryEventEnums.h" #include "nsIConsoleService.h" #include "nsIStringBundle.h" using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::Telemetry; extern mozilla::LazyLogModule sCSMLog; extern Atomic sJSHacksChecked; extern Atomic sJSHacksPresent; extern Atomic sCSSHacksChecked; extern Atomic sCSSHacksPresent; extern Atomic sTelemetryEventEnabled; // Helper function for IsConsideredSameOriginForUIR which makes // Principals of scheme 'http' return Principals of scheme 'https'. static already_AddRefed MakeHTTPPrincipalHTTPS( nsIPrincipal* aPrincipal) { nsCOMPtr 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 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 compareTriggeringPrincipal = MakeHTTPPrincipalHTTPS(aTriggeringPrincipal); nsCOMPtr 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* aRegexResults = nullptr) { MOZ_ASSERT(NS_IsMainThread()); aMatchResult = false; mozilla::dom::AutoJSAPI jsapi; jsapi.Init(); JSContext* cx = jsapi.cx(); mozilla::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::Rooted regexp( cx, JS::NewUCRegExpObject(cx, aPattern.BeginReading(), aPattern.Length(), JS::RegExpFlag::Unicode)); if (!regexp) { return NS_ERROR_ILLEGAL_VALUE; } JS::Rooted regexResult(cx, JS::NullValue()); size_t index = 0; if (!JS::ExecuteRegExpNoStatics(cx, regexp, aString.BeginReading(), aString.Length(), &index, aOnlyMatch, ®exResult)) { 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::Rooted regexResultObj(cx, ®exResult.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::Rooted 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; } /* * MOZ_CRASH_UNSAFE_PRINTF has a sPrintfCrashReasonSize-sized buffer. We need * to make sure we don't exceed it. These functions perform this check and * munge things for us. * */ /* * Destructively truncates a string to fit within the limit */ char* nsContentSecurityUtils::SmartFormatCrashString(const char* str) { return nsContentSecurityUtils::SmartFormatCrashString(strdup(str)); } char* nsContentSecurityUtils::SmartFormatCrashString(char* str) { auto str_len = strlen(str); if (str_len > sPrintfCrashReasonSize) { str[sPrintfCrashReasonSize - 1] = '\0'; str_len = strlen(str); } MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize > str_len); return str; } /* * Destructively truncates two strings to fit within the limit. * format_string is a format string containing two %s entries * The second string will be truncated to the _last_ 25 characters * The first string will be truncated to the remaining limit. */ nsCString nsContentSecurityUtils::SmartFormatCrashString( const char* part1, const char* part2, const char* format_string) { return SmartFormatCrashString(strdup(part1), strdup(part2), format_string); } nsCString nsContentSecurityUtils::SmartFormatCrashString( char* part1, char* part2, const char* format_string) { auto part1_len = strlen(part1); auto part2_len = strlen(part2); auto constant_len = strlen(format_string) - 4; if (part1_len + part2_len + constant_len > sPrintfCrashReasonSize) { if (part2_len > 25) { part2 += (part2_len - 25); } part2_len = strlen(part2); part1[sPrintfCrashReasonSize - (constant_len + part2_len + 1)] = '\0'; part1_len = strlen(part1); } MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize > constant_len + part1_len + part2_len); auto parts = nsPrintfCString(format_string, part1, part2); return std::move(parts); } /* * 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 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 kAboutUri = "abouturi"_ns; static constexpr auto kDataUriWebExtCStyle = "dataurl-extension-contentstyle"_ns; static constexpr auto kSingleString = "singlestring"_ns; static constexpr auto kMozillaExtensionFile = "mozillaextension_file"_ns; static constexpr auto kOtherExtensionFile = "otherextension_file"_ns; static constexpr auto kExtensionURI = "extension_uri"_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:text/css;extension=style;"_ns)) { return FilenameTypeAndDetails(kDataUriWebExtCStyle, Nothing()); } if (StringBeginsWith(fileName, u"data:"_ns)) { return FilenameTypeAndDetails(kDataUri, Nothing()); } // Can't do regex matching off-main-thread if (NS_IsMainThread()) { // Extension as loaded via a file:// bool regexMatch; nsTArray regexResults; nsresult rv = RegexEval(kExtensionRegex, fileName, /* aOnlyMatch = */ false, regexMatch, ®exResults); if (NS_FAILED(rv)) { return FilenameTypeAndDetails(kRegexFailure, Nothing()); } if (regexMatch) { nsCString type = StringEndsWith(regexResults[2], u"mozilla.org.xpi"_ns) ? kMozillaExtensionFile : kOtherExtensionFile; const 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()); } } // Something loaded via an about:// URI. if (StringBeginsWith(fileName, u"about:"_ns)) { // Remove any querystrings and such long int desired_length = fileName.Length(); long int possible_new_length = 0; possible_new_length = fileName.FindChar('?'); if (possible_new_length != -1 && possible_new_length < desired_length) { desired_length = possible_new_length; } possible_new_length = fileName.FindChar('#'); if (possible_new_length != -1 && possible_new_length < desired_length) { desired_length = possible_new_length; } auto subFileName = Substring(fileName, 0, desired_length); return FilenameTypeAndDetails(kAboutUri, Some(subFileName)); } // Something loaded via a moz-extension:// URI. if (StringBeginsWith(fileName, u"moz-extension://"_ns)) { if (!collectAdditionalExtensionData) { return FilenameTypeAndDetails(kExtensionURI, Nothing()); } nsAutoString sanitizedPathAndScheme; sanitizedPathAndScheme.Append(u"moz-extension://["_ns); nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), fileName); if (NS_FAILED(rv)) { // Return after adding ://[ so we know we failed here. return FilenameTypeAndDetails(kExtensionURI, 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()); sanitizedPathAndScheme.Append(u"]"_ns); if (policy->IsPrivileged()) { sanitizedPathAndScheme.Append(u"P=1"_ns); } else { sanitizedPathAndScheme.Append(u"P=0"_ns); } } else { sanitizedPathAndScheme.Append(u"failed finding addon by host]"_ns); } } else { sanitizedPathAndScheme.Append(u"can't get addon off main thread]"_ns); } AppendUTF8toUTF16(url.FilePath(), sanitizedPathAndScheme); return FilenameTypeAndDetails(kExtensionURI, Some(sanitizedPathAndScheme)); } #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; SAFECALL_URLMON_FUNC(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); } return FilenameTypeAndDetails(kSanitizedWindowsURL, Some(sanitizedPathAndScheme)); } else { return FilenameTypeAndDetails(kSanitizedWindowsPath, Some(strSanitizedPath)); } } #endif if (!NS_IsMainThread()) { return FilenameTypeAndDetails(kOtherWorker, Nothing()); } return FilenameTypeAndDetails(kOther, Nothing()); } #ifdef NIGHTLY_BUILD // Crash String must be safe from a telemetry point of view. // This will be ensured when this function is used. void PossiblyCrash(const char* aPrefSuffix, const char* aUnsafeCrashString, const nsCString& aSafeCrashString) { if (MOZ_UNLIKELY(!XRE_IsParentProcess())) { // We only crash in the parent (unfortunately) because it's // the only place we can be sure that our only-crash-once // pref-writing works. return; } if (!NS_IsMainThread()) { // Setting a pref off the main thread causes ContentParent to observe the // pref set, resulting in a Release Assertion when it tries to update the // child off main thread. So don't do any of this off main thread. (Which // is a bit of a blind spot for this purpose...) return; } nsCString previous_crashes("security.crash_tracking."); previous_crashes.Append(aPrefSuffix); previous_crashes.Append(".prevCrashes"); nsCString max_crashes("security.crash_tracking."); max_crashes.Append(aPrefSuffix); max_crashes.Append(".maxCrashes"); int32_t numberOfPreviousCrashes = 0; numberOfPreviousCrashes = Preferences::GetInt(previous_crashes.get(), 0); int32_t maxAllowableCrashes = 0; maxAllowableCrashes = Preferences::GetInt(max_crashes.get(), 0); if (numberOfPreviousCrashes >= maxAllowableCrashes) { return; } nsresult rv = Preferences::SetInt(previous_crashes.get(), ++numberOfPreviousCrashes); if (NS_FAILED(rv)) { return; } nsCOMPtr prefsCom = Preferences::GetService(); Preferences* prefs = static_cast(prefsCom.get()); if (!prefs->AllowOffMainThreadSave()) { // Do not crash if we can't save prefs off the main thread return; } rv = prefs->SavePrefFileBlocking(); if (!NS_FAILED(rv)) { // We can only use this in local builds where we don't send stuff up to the // crash reporter because it has user private data. // MOZ_CRASH_UNSAFE_PRINTF("%s", // nsContentSecurityUtils::SmartFormatCrashString(aUnsafeCrashString)); MOZ_CRASH_UNSAFE_PRINTF( "%s", nsContentSecurityUtils::SmartFormatCrashString(aSafeCrashString.get())); } } #endif 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; }; /* 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 utility "resource://testing-common/content-task.js"_ns, // Tracked by Bug 1584605 "resource://gre/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 profiler's symbolication code uses a wasm module to extract symbols // from the binary files result of local builds. // See bug 1777479 "resource://devtools/client/performance-new/shared/symbolication.jsm.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; } DetectJsHacks(); if (MOZ_UNLIKELY(sJSHacksPresent)) { MOZ_LOG( sCSMLog, LogLevel::Debug, ("Allowing eval() %s because some " "JS hacks may be present.", (aIsSystemPrincipal ? "with System Principal" : "in parent process"))); return true; } if (XRE_IsE10sParentProcess() && !StaticPrefs::extensions_webextensions_remote()) { MOZ_LOG(sCSMLog, LogLevel::Debug, ("Allowing eval() in parent process because the web extension " "process is disabled")); 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::Error, ("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 #if defined(DEBUG) || defined(FUZZING) auto crashString = nsContentSecurityUtils::SmartFormatCrashString( NS_ConvertUTF16toUTF8(aScript).get(), fileName.get(), (aIsSystemPrincipal ? "Blocking eval() with System Principal with script %s from file %s" : "Blocking eval() in parent process with script %s from file %s")); MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get()); #endif return false; } /* 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> extra; if (fileNameTypeAndDetails.second.isSome()) { extra = Some>({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 console( do_GetService(NS_CONSOLESERVICE_CONTRACTID)); if (!console) { return; } nsCOMPtr error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID)); if (!error) { return; } nsCOMPtr bundle; nsCOMPtr stringService = mozilla::components::StringBundle::Service(); if (!stringService) { return; } stringService->CreateBundle( "chrome://global/locale/security/security.properties", getter_AddRefs(bundle)); if (!bundle) { return; } nsAutoString message; AutoTArray formatStrings = {aFileNameA}; nsresult rv = bundle->FormatStringFromName("RestrictBrowserEvalUsage", formatStrings, message); if (NS_FAILED(rv)) { return; } rv = error->InitWithWindowID(message, aFileNameA, u""_ns, aLineNumber, aColumnNumber, nsIScriptError::errorFlag, "BrowserEvalUsage", aWindowID, true /* From chrome context */); if (NS_FAILED(rv)) { return; } console->LogMessage(error); } /* static */ void nsContentSecurityUtils::DetectJsHacks() { // We can only perform the check of this preference on the Main Thread // (because a String-based preference check is only safe on Main Thread.) // In theory, it would be possible that a separate thread could get here // before the main thread, resulting in the other thread not being able to // perform this check, but the odds of that are small (and probably zero.) if (!NS_IsMainThread()) { return; } // If the pref service isn't available, do nothing and re-do this later. if (!Preferences::IsServiceAvailable()) { return; } // No need to check again. if (MOZ_LIKELY(sJSHacksChecked || sJSHacksPresent)) { return; } nsresult rv; sJSHacksChecked = true; // This preference is required by bootstrapLoader.xpi, which is an // alternate way to load legacy-style extensions. It only works on // DevEdition/Nightly. bool xpinstallSignatures; rv = Preferences::GetBool("xpinstall.signatures.required", &xpinstallSignatures, PrefValueKind::Default); if (!NS_FAILED(rv) && !xpinstallSignatures) { sJSHacksPresent = true; return; } rv = Preferences::GetBool("xpinstall.signatures.required", &xpinstallSignatures, PrefValueKind::User); if (!NS_FAILED(rv) && !xpinstallSignatures) { sJSHacksPresent = true; return; } // 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; rv = Preferences::GetString("general.config.filename", jsConfigPref, PrefValueKind::Default); if (!NS_FAILED(rv) && !jsConfigPref.IsEmpty()) { sJSHacksPresent = true; return; } rv = Preferences::GetString("general.config.filename", jsConfigPref, PrefValueKind::User); if (!NS_FAILED(rv) && !jsConfigPref.IsEmpty()) { sJSHacksPresent = true; return; } // These preferences are for autoconfiguration of Firefox by admins. // The first will load a file over the network; the second will // fall back to a local file if the network is unavailable nsAutoString configUrlPref; rv = Preferences::GetString("autoadmin.global_config_url", configUrlPref, PrefValueKind::Default); if (!NS_FAILED(rv) && !configUrlPref.IsEmpty()) { sJSHacksPresent = true; return; } rv = Preferences::GetString("autoadmin.global_config_url", configUrlPref, PrefValueKind::User); if (!NS_FAILED(rv) && !configUrlPref.IsEmpty()) { sJSHacksPresent = true; return; } bool failOverToCache; rv = Preferences::GetBool("autoadmin.failover_to_cached", &failOverToCache, PrefValueKind::Default); if (!NS_FAILED(rv) && failOverToCache) { sJSHacksPresent = true; return; } rv = Preferences::GetBool("autoadmin.failover_to_cached", &failOverToCache, PrefValueKind::User); if (!NS_FAILED(rv) && failOverToCache) { sJSHacksPresent = true; } } /* static */ void nsContentSecurityUtils::DetectCssHacks() { // We can only perform the check of this preference on the Main Thread // It's possible that this function may therefore race and we expect the // caller to ensure that the checks have actually happened. if (!NS_IsMainThread()) { return; } // If the pref service isn't available, do nothing and re-do this later. if (!Preferences::IsServiceAvailable()) { return; } // No need to check again. if (MOZ_LIKELY(sCSSHacksChecked || sCSSHacksPresent)) { return; } // This preference is a bool to see if userChrome css is loaded bool customStylesPresent = Preferences::GetBool( "toolkit.legacyUserProfileCustomizations.stylesheets", false); if (customStylesPresent) { sCSSHacksPresent = true; } sCSSHacksChecked = true; } /* static */ nsresult nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart( nsIChannel* aChannel, nsIHttpChannel** aHttpChannel) { nsCOMPtr httpChannel = do_QueryInterface(aChannel); if (httpChannel) { httpChannel.forget(aHttpChannel); return NS_OK; } nsCOMPtr multipart = do_QueryInterface(aChannel); if (!multipart) { *aHttpChannel = nullptr; return NS_OK; } nsCOMPtr 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 CheckCSPFrameAncestorPolicy(nsIChannel* aChannel, nsIContentSecurityPolicy** aOutCSP) { MOZ_ASSERT(aChannel); nsCOMPtr loadInfo = aChannel->LoadInfo(); ExtContentPolicyType 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 != ExtContentPolicy::TYPE_SUBDOCUMENT && contentType != ExtContentPolicy::TYPE_OBJECT) { return NS_OK; } // CSP can only hang off an http channel, if this channel is not // an http channel then there is nothing to do here, // except with add-ons, where the CSP is stored in a WebExtensionPolicy. nsCOMPtr httpChannel; nsresult rv = nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart( aChannel, getter_AddRefs(httpChannel)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoCString tCspHeaderValue, tCspROHeaderValue; if (httpChannel) { 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; } } nsCOMPtr resultPrincipal; rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( aChannel, getter_AddRefs(resultPrincipal)); NS_ENSURE_SUCCESS(rv, rv); RefPtr addonPolicy; if (!httpChannel) { addonPolicy = BasePrincipal::Cast(resultPrincipal)->AddonPolicy(); if (!addonPolicy) { // Neither a HTTP channel, nor a moz-extension:-resource. // CSP is not supported. return NS_OK; } } RefPtr csp = new nsCSPContext(); // This CSPContext is only used for checking frame-ancestors, we // will parse the CSP again anyway. (Unless this blocks the load, but // parser warnings aren't really important in that case) csp->SuppressParserLogMessages(); nsCOMPtr selfURI; nsAutoString referrerSpec; if (httpChannel) { aChannel->GetURI(getter_AddRefs(selfURI)); nsCOMPtr referrerInfo = httpChannel->GetReferrerInfo(); if (referrerInfo) { referrerInfo->GetComputedReferrerSpec(referrerSpec); } } else { // aChannel::GetURI would return the jar: or file:-URI for extensions. // Use the "final" URI to get the actual moz-extension:-URL. NS_GetFinalChannelURI(aChannel, getter_AddRefs(selfURI)); } uint64_t innerWindowID = loadInfo->GetInnerWindowID(); rv = csp->SetRequestContextWithPrincipal(resultPrincipal, selfURI, referrerSpec, innerWindowID); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (addonPolicy) { csp->AppendPolicy(addonPolicy->BaseCSP(), false, false); csp->AppendPolicy(addonPolicy->ExtensionPageCSP(), false, false); } else { NS_ConvertASCIItoUTF16 cspHeaderValue(tCspHeaderValue); NS_ConvertASCIItoUTF16 cspROHeaderValue(tCspROHeaderValue); // ----- 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! return NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION; } // return the CSP for x-frame-options check csp.forget(aOutCSP); return NS_OK; } void EnforceCSPFrameAncestorPolicy(nsIChannel* aChannel, const nsresult& aError) { if (aError == NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION) { aChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION); } } void EnforceXFrameOptionsCheck(nsIChannel* aChannel, nsIContentSecurityPolicy* aCsp) { MOZ_ASSERT(aChannel); bool isFrameOptionsIgnored = false; // check for XFO options // XFO checks can be skipped if there are frame ancestors if (!FramingChecker::CheckFrameOptions(aChannel, aCsp, isFrameOptionsIgnored)) { // stop! ERROR page! aChannel->Cancel(NS_ERROR_XFO_VIOLATION); } if (isFrameOptionsIgnored) { // log warning to console that xfo is ignored because of CSP nsCOMPtr loadInfo = aChannel->LoadInfo(); uint64_t innerWindowID = loadInfo->GetInnerWindowID(); bool privateWindow = !!loadInfo->GetOriginAttributes().mPrivateBrowsingId; AutoTArray params = {u"x-frame-options"_ns, u"frame-ancestors"_ns}; CSP_LogLocalizedStr("IgnoringSrcBecauseOfDirective", params, u""_ns, // no sourcefile u""_ns, // no scriptsample 0, // no linenumber 0, // no columnnumber nsIScriptError::warningFlag, "IgnoringSrcBecauseOfDirective"_ns, innerWindowID, privateWindow); } } /* static */ void nsContentSecurityUtils::PerformCSPFrameAncestorAndXFOCheck( nsIChannel* aChannel) { nsCOMPtr csp; nsresult rv = CheckCSPFrameAncestorPolicy(aChannel, getter_AddRefs(csp)); if (NS_FAILED(rv)) { EnforceCSPFrameAncestorPolicy(aChannel, 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); } /* static */ bool nsContentSecurityUtils::CheckCSPFrameAncestorAndXFO(nsIChannel* aChannel) { nsCOMPtr csp; nsresult rv = CheckCSPFrameAncestorPolicy(aChannel, getter_AddRefs(csp)); if (NS_FAILED(rv)) { return false; } bool isFrameOptionsIgnored = false; return FramingChecker::CheckFrameOptions(aChannel, csp, isFrameOptionsIgnored); } #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: // // This is a data document, created using DOMParser or // document.implementation.createDocument() or such, not an about: page which // is loaded as a web page. if (aDocument->IsLoadedAsData()) { return; } // 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 documentURI = aDocument->GetDocumentURI(); if (!documentURI->SchemeIs("about")) { return; } nsCOMPtr 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(u"default-src") >= 0) { foundDefaultSrc = true; } if (parsedPolicyStr.Find(u"object-src 'none'") >= 0) { foundObjectSrc = true; } if (parsedPolicyStr.Find(u"'unsafe-eval'") >= 0) { foundUnsafeEval = true; } if (parsedPolicyStr.Find(u"'unsafe-inline'") >= 0) { foundUnsafeInline = true; } if (parsedPolicyStr.Find(u"script-src") >= 0) { foundScriptSrc = true; } if (parsedPolicyStr.Find(u"worker-src") >= 0) { foundWorkerSrc = true; } if (parsedPolicyStr.Find(u"http:") >= 0 || parsedPolicyStr.Find(u"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:logo just displays the firefox logo -> no CSP "about:logo"_ns, // about:sync is a special mozilla-signed developer addon with low usage -> // no CSP "about:sync"_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, ion, 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:ion"_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) || StringBeginsWith(aboutSpec, "about:pocket-saved"_ns) || StringBeginsWith(aboutSpec, "about:pocket-home"_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(JSContext* cx, const char* aFilename) { // 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; } // If we have allowed eval (because of a user configuration or more // likely a test has requested it), and the script is an eval, allow it. NS_ConvertUTF8toUTF16 filenameU(aFilename); if (StaticPrefs::security_allow_eval_with_system_principal() || StaticPrefs::security_allow_eval_in_parent_process()) { if (StringEndsWith(filenameU, u"> eval"_ns)) { return true; } } DetectJsHacks(); if (MOZ_UNLIKELY(!sJSHacksChecked)) { MOZ_LOG( sCSMLog, LogLevel::Debug, ("Allowing a javascript load of %s because " "we have not yet been able to determine if JS hacks may be present", aFilename)); return true; } if (MOZ_UNLIKELY(sJSHacksPresent)) { MOZ_LOG(sCSMLog, LogLevel::Debug, ("Allowing a javascript load of %s because " "some JS hacks may be present", aFilename)); return true; } if (XRE_IsE10sParentProcess() && !StaticPrefs::extensions_webextensions_remote()) { MOZ_LOG(sCSMLog, LogLevel::Debug, ("Allowing a javascript load of %s because the web extension " "process is disabled.", aFilename)); return true; } 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; } if (StringBeginsWith(filenameU, u"moz-extension://"_ns)) { nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aFilename); if (!NS_FAILED(rv) && NS_IsMainThread()) { mozilla::extensions::URLInfo url(uri); auto* policy = ExtensionPolicyService::GetSingleton().GetByHost(url.Host()); if (policy && policy->IsPrivileged()) { MOZ_LOG(sCSMLog, LogLevel::Debug, ("Allowing a javascript load of %s because the web extension " "it is associated with is privileged.", aFilename)); return true; } } } else if (!NS_IsMainThread()) { WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); if (workerPrivate && workerPrivate->IsPrivilegedAddonGlobal()) { MOZ_LOG(sCSMLog, LogLevel::Debug, ("Allowing a javascript load of %s because the web extension " "it is associated with is privileged.", aFilename)); return true; } } auto kAllowedFilenamesExact = { // Allow through the injection provided by about:sync addon u"data:,new function() {\n const { AboutSyncRedirector } = ChromeUtils.import(\"chrome://aboutsync/content/AboutSyncRedirector.js\");\n AboutSyncRedirector.register();\n}"_ns, }; for (auto allowedFilename : kAllowedFilenamesExact) { if (filenameU == allowedFilename) { return true; } } auto kAllowedFilenamesPrefix = { // Until 371900 is fixed, we need to do something about about:downloads // and this is the most reasonable. See 1727770 u"about:downloads"_ns, // We think this is the same problem as about:downloads u"about:preferences"_ns, // Browser console will give a filename of 'debugger' See 1763943 // Sometimes it's 'debugger eager eval code', other times just 'debugger // eval code' u"debugger"_ns}; for (auto allowedFilenamePrefix : kAllowedFilenamesPrefix) { if (StringBeginsWith(filenameU, allowedFilenamePrefix)) { return true; } } // Log to MOZ_LOG MOZ_LOG(sCSMLog, LogLevel::Error, ("ValidateScriptFilename Failed: %s\n", aFilename)); // Send Telemetry FilenameTypeAndDetails fileNameTypeAndDetails = FilenameToFilenameType(filenameU, true); Telemetry::EventID eventType = Telemetry::EventID::Security_Javascriptload_Parentprocess; mozilla::Maybe> extra; if (fileNameTypeAndDetails.second.isSome()) { extra = Some>({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); #if defined(DEBUG) || defined(FUZZING) auto crashString = nsContentSecurityUtils::SmartFormatCrashString( aFilename, fileNameTypeAndDetails.second.isSome() ? NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value()).get() : "(None)", "Blocking a script load %s from file %s"); MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get()); #elif defined(NIGHTLY_BUILD) // Cause a crash (if we've never crashed before and we can ensure we won't do // it again.) // The details in the second arg, passed to UNSAFE_PRINTF, are also included // in Event Telemetry and have received data review. if (fileNameTypeAndDetails.second.isSome()) { PossiblyCrash("js_load_1", aFilename, NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())); } else { PossiblyCrash("js_load_1", aFilename, "(None)"_ns); } #endif // Presently we are only enforcing restrictions for the script filename // on Nightly. On all channels we are reporting Telemetry. In the future we // will assert in debug builds and return false to prevent execution in // non-debug builds. #ifdef NIGHTLY_BUILD return false; #else return true; #endif } /* static */ void nsContentSecurityUtils::LogMessageToConsole(nsIHttpChannel* aChannel, const char* aMsg) { nsCOMPtr 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 loadInfo = aChannel->LoadInfo(); loadInfo->GetInnerWindowID(&windowID); } nsAutoString localizedMsg; nsAutoCString spec; uri->GetSpec(spec); AutoTArray 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 loadInfo = aChannel->LoadInfo(); nsCOMPtr contentLocation; aChannel->GetURI(getter_AddRefs(contentLocation)); nsCOMPtr loadingPrincipal = loadInfo->GetLoadingPrincipal(); if (!loadingPrincipal) { loadingPrincipal = loadInfo->TriggeringPrincipal(); } // Creating a fake Loadinfo that is just used for the MCB check. nsCOMPtr secCheckLoadInfo = new mozilla::net::LoadInfo( loadingPrincipal, loadInfo->TriggeringPrincipal(), nullptr, nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, nsIContentPolicy::TYPE_FETCH); 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 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 httpChannel = do_QueryInterface(aChannel); if (httpChannel) { LogMessageToConsole(httpChannel, "IframeSandboxBlockedDownload"); } return nsITransfer::DOWNLOAD_FORBIDDEN; } return nsITransfer::DOWNLOAD_ACCEPTABLE; }