/* -*- 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/dom/nsMixedContentBlocker.h" #include "mozilla/dom/ScriptSettings.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 "WinUtils.h" # include #endif #include "FramingChecker.h" #include "js/Array.h" // JS::GetArrayLength #include "js/ContextOptions.h" #include "js/RegExp.h" #include "js/RegExpFlags.h" // JS::RegExpFlags #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 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::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, ®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::RootedObject 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::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 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 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) ? 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; 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); } else if (sanitizedPathAndScheme == u"moz-extension"_ns && collectAdditionalExtensionData) { sanitizedPathAndScheme.Append(u"://["_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(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; }; /* 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()) { 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::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 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::services::GetStringBundleService(); 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 */ 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 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 httpChannel; nsresult rv = nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart( aChannel, getter_AddRefs(httpChannel)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!httpChannel) { return NS_OK; } 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; } 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 csp = new nsCSPContext(); nsCOMPtr resultPrincipal; rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( aChannel, getter_AddRefs(resultPrincipal)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr selfURI; aChannel->GetURI(getter_AddRefs(selfURI)); nsCOMPtr 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 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: // // 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("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, 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:newinstall"_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), "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 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()) { MOZ_LOG(sCSMLog, LogLevel::Debug, ("Allowing a javascript load of %s because the web extension " "process is disabled.", 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> 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); // 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 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; }