Merge mozilla-central to autoland. a=merge CLOSED TREE
[gecko.git] / dom / security / nsContentSecurityUtils.cpp
blob01e9c6d5db75f66ca6f088ed574afb6dfab9c6bb
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /* A namespace class for static content security utilities. */
9 #include "nsContentSecurityUtils.h"
11 #include "mozilla/Components.h"
12 #include "mozilla/dom/nsMixedContentBlocker.h"
13 #include "mozilla/dom/ScriptSettings.h"
14 #include "mozilla/dom/WorkerCommon.h"
15 #include "mozilla/dom/WorkerPrivate.h"
16 #include "nsComponentManagerUtils.h"
17 #include "nsIContentSecurityPolicy.h"
18 #include "nsIChannel.h"
19 #include "nsIHttpChannel.h"
20 #include "nsIMultiPartChannel.h"
21 #include "nsIURI.h"
22 #include "nsITransfer.h"
23 #include "nsNetUtil.h"
24 #include "nsSandboxFlags.h"
25 #if defined(XP_WIN)
26 # include "mozilla/WinHeaderOnlyUtils.h"
27 # include "WinUtils.h"
28 # include <wininet.h>
29 #endif
31 #include "FramingChecker.h"
32 #include "js/Array.h" // JS::GetArrayLength
33 #include "js/ContextOptions.h"
34 #include "js/PropertyAndElement.h" // JS_GetElement
35 #include "js/RegExp.h"
36 #include "js/RegExpFlags.h" // JS::RegExpFlags
37 #include "js/friend/ErrorMessages.h" // JSMSG_UNSAFE_FILENAME
38 #include "mozilla/ExtensionPolicyService.h"
39 #include "mozilla/Logging.h"
40 #include "mozilla/Preferences.h"
41 #include "mozilla/dom/Document.h"
42 #include "mozilla/dom/nsCSPContext.h"
43 #include "mozilla/StaticPrefs_security.h"
44 #include "LoadInfo.h"
45 #include "mozilla/StaticPrefs_extensions.h"
46 #include "mozilla/StaticPrefs_dom.h"
47 #include "mozilla/Telemetry.h"
48 #include "mozilla/TelemetryComms.h"
49 #include "mozilla/TelemetryEventEnums.h"
50 #include "nsIConsoleService.h"
51 #include "nsIStringBundle.h"
53 using namespace mozilla;
54 using namespace mozilla::dom;
55 using namespace mozilla::Telemetry;
57 extern mozilla::LazyLogModule sCSMLog;
58 extern Atomic<bool, mozilla::Relaxed> sJSHacksChecked;
59 extern Atomic<bool, mozilla::Relaxed> sJSHacksPresent;
60 extern Atomic<bool, mozilla::Relaxed> sCSSHacksChecked;
61 extern Atomic<bool, mozilla::Relaxed> sCSSHacksPresent;
62 extern Atomic<bool, mozilla::Relaxed> sTelemetryEventEnabled;
64 // Helper function for IsConsideredSameOriginForUIR which makes
65 // Principals of scheme 'http' return Principals of scheme 'https'.
66 static already_AddRefed<nsIPrincipal> MakeHTTPPrincipalHTTPS(
67 nsIPrincipal* aPrincipal) {
68 nsCOMPtr<nsIPrincipal> principal = aPrincipal;
69 // if the principal is not http, then it can also not be upgraded
70 // to https.
71 if (!principal->SchemeIs("http")) {
72 return principal.forget();
75 nsAutoCString spec;
76 aPrincipal->GetAsciiSpec(spec);
77 // replace http with https
78 spec.ReplaceLiteral(0, 4, "https");
80 nsCOMPtr<nsIURI> newURI;
81 nsresult rv = NS_NewURI(getter_AddRefs(newURI), spec);
82 if (NS_WARN_IF(NS_FAILED(rv))) {
83 return nullptr;
86 mozilla::OriginAttributes OA =
87 BasePrincipal::Cast(aPrincipal)->OriginAttributesRef();
89 principal = BasePrincipal::CreateContentPrincipal(newURI, OA);
90 return principal.forget();
93 /* static */
94 bool nsContentSecurityUtils::IsConsideredSameOriginForUIR(
95 nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aResultPrincipal) {
96 MOZ_ASSERT(aTriggeringPrincipal);
97 MOZ_ASSERT(aResultPrincipal);
98 // we only have to make sure that the following truth table holds:
99 // aTriggeringPrincipal | aResultPrincipal | Result
100 // ----------------------------------------------------------------
101 // http://example.com/foo.html | http://example.com/bar.html | true
102 // http://example.com/foo.html | https://example.com/bar.html | true
103 // https://example.com/foo.html | https://example.com/bar.html | true
104 // https://example.com/foo.html | http://example.com/bar.html | true
106 // fast path if both principals are same-origin
107 if (aTriggeringPrincipal->Equals(aResultPrincipal)) {
108 return true;
111 // in case a principal uses a scheme of 'http' then we just upgrade to
112 // 'https' and use the principal equals comparison operator to check
113 // for same-origin.
114 nsCOMPtr<nsIPrincipal> compareTriggeringPrincipal =
115 MakeHTTPPrincipalHTTPS(aTriggeringPrincipal);
117 nsCOMPtr<nsIPrincipal> compareResultPrincipal =
118 MakeHTTPPrincipalHTTPS(aResultPrincipal);
120 return compareTriggeringPrincipal->Equals(compareResultPrincipal);
124 * Performs a Regular Expression match, optionally returning the results.
125 * This function is not safe to use OMT.
127 * @param aPattern The regex pattern
128 * @param aString The string to compare against
129 * @param aOnlyMatch Whether we want match results or only a true/false for
130 * the match
131 * @param aMatchResult Out param for whether or not the pattern matched
132 * @param aRegexResults Out param for the matches of the regex, if requested
133 * @returns nsresult indicating correct function operation or error
135 nsresult RegexEval(const nsAString& aPattern, const nsAString& aString,
136 bool aOnlyMatch, bool& aMatchResult,
137 nsTArray<nsString>* aRegexResults = nullptr) {
138 MOZ_ASSERT(NS_IsMainThread());
139 aMatchResult = false;
141 mozilla::dom::AutoJSAPI jsapi;
142 jsapi.Init();
144 JSContext* cx = jsapi.cx();
145 mozilla::AutoDisableJSInterruptCallback disabler(cx);
147 // We can use the junk scope here, because we're just using it for regexp
148 // evaluation, not actual script execution, and we disable statics so that the
149 // evaluation does not interact with the execution global.
150 JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
152 JS::Rooted<JSObject*> regexp(
153 cx, JS::NewUCRegExpObject(cx, aPattern.BeginReading(), aPattern.Length(),
154 JS::RegExpFlag::Unicode));
155 if (!regexp) {
156 return NS_ERROR_ILLEGAL_VALUE;
159 JS::Rooted<JS::Value> regexResult(cx, JS::NullValue());
161 size_t index = 0;
162 if (!JS::ExecuteRegExpNoStatics(cx, regexp, aString.BeginReading(),
163 aString.Length(), &index, aOnlyMatch,
164 &regexResult)) {
165 return NS_ERROR_FAILURE;
168 if (regexResult.isNull()) {
169 // On no match, ExecuteRegExpNoStatics returns Null
170 return NS_OK;
172 if (aOnlyMatch) {
173 // On match, with aOnlyMatch = true, ExecuteRegExpNoStatics returns boolean
174 // true.
175 MOZ_ASSERT(regexResult.isBoolean() && regexResult.toBoolean());
176 aMatchResult = true;
177 return NS_OK;
179 if (aRegexResults == nullptr) {
180 return NS_ERROR_INVALID_ARG;
183 // Now we know we have a result, and we need to extract it so we can read it.
184 uint32_t length;
185 JS::Rooted<JSObject*> regexResultObj(cx, &regexResult.toObject());
186 if (!JS::GetArrayLength(cx, regexResultObj, &length)) {
187 return NS_ERROR_NOT_AVAILABLE;
189 MOZ_LOG(sCSMLog, LogLevel::Verbose, ("Regex Matched %i strings", length));
191 for (uint32_t i = 0; i < length; i++) {
192 JS::Rooted<JS::Value> element(cx);
193 if (!JS_GetElement(cx, regexResultObj, i, &element)) {
194 return NS_ERROR_NO_CONTENT;
197 nsAutoJSString value;
198 if (!value.init(cx, element)) {
199 return NS_ERROR_NO_CONTENT;
202 MOZ_LOG(sCSMLog, LogLevel::Verbose,
203 ("Regex Matching: %i: %s", i, NS_ConvertUTF16toUTF8(value).get()));
204 aRegexResults->AppendElement(value);
207 aMatchResult = true;
208 return NS_OK;
212 * MOZ_CRASH_UNSAFE_PRINTF has a sPrintfCrashReasonSize-sized buffer. We need
213 * to make sure we don't exceed it. These functions perform this check and
214 * munge things for us.
219 * Destructively truncates a string to fit within the limit
221 char* nsContentSecurityUtils::SmartFormatCrashString(const char* str) {
222 return nsContentSecurityUtils::SmartFormatCrashString(strdup(str));
225 char* nsContentSecurityUtils::SmartFormatCrashString(char* str) {
226 auto str_len = strlen(str);
228 if (str_len > sPrintfCrashReasonSize) {
229 str[sPrintfCrashReasonSize - 1] = '\0';
230 str_len = strlen(str);
232 MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize > str_len);
234 return str;
238 * Destructively truncates two strings to fit within the limit.
239 * format_string is a format string containing two %s entries
240 * The second string will be truncated to the _last_ 25 characters
241 * The first string will be truncated to the remaining limit.
243 nsCString nsContentSecurityUtils::SmartFormatCrashString(
244 const char* part1, const char* part2, const char* format_string) {
245 return SmartFormatCrashString(strdup(part1), strdup(part2), format_string);
248 nsCString nsContentSecurityUtils::SmartFormatCrashString(
249 char* part1, char* part2, const char* format_string) {
250 auto part1_len = strlen(part1);
251 auto part2_len = strlen(part2);
253 auto constant_len = strlen(format_string) - 4;
255 if (part1_len + part2_len + constant_len > sPrintfCrashReasonSize) {
256 if (part2_len > 25) {
257 part2 += (part2_len - 25);
259 part2_len = strlen(part2);
261 part1[sPrintfCrashReasonSize - (constant_len + part2_len + 1)] = '\0';
262 part1_len = strlen(part1);
264 MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize >
265 constant_len + part1_len + part2_len);
267 auto parts = nsPrintfCString(format_string, part1, part2);
268 return std::move(parts);
272 * Telemetry Events extra data only supports 80 characters, so we optimize the
273 * filename to be smaller and collect more data.
275 nsString OptimizeFileName(const nsAString& aFileName) {
276 nsString optimizedName(aFileName);
278 MOZ_LOG(
279 sCSMLog, LogLevel::Verbose,
280 ("Optimizing FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get()));
282 optimizedName.ReplaceSubstring(u".xpi!"_ns, u"!"_ns);
283 optimizedName.ReplaceSubstring(u"shield.mozilla.org!"_ns, u"s!"_ns);
284 optimizedName.ReplaceSubstring(u"mozilla.org!"_ns, u"m!"_ns);
285 if (optimizedName.Length() > 80) {
286 optimizedName.Truncate(80);
289 MOZ_LOG(
290 sCSMLog, LogLevel::Verbose,
291 ("Optimized FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get()));
292 return optimizedName;
296 * FilenameToFilenameType takes a fileName and returns a Pair of strings.
297 * The First entry is a string indicating the type of fileName
298 * The Second entry is a Maybe<string> that can contain additional details to
299 * report.
301 * The reason we use strings (instead of an int/enum) is because the Telemetry
302 * Events API only accepts strings.
304 * Function is a static member of the class to enable gtests.
307 /* static */
308 FilenameTypeAndDetails nsContentSecurityUtils::FilenameToFilenameType(
309 const nsString& fileName, bool collectAdditionalExtensionData) {
310 // These are strings because the Telemetry Events API only accepts strings
311 static constexpr auto kChromeURI = "chromeuri"_ns;
312 static constexpr auto kResourceURI = "resourceuri"_ns;
313 static constexpr auto kBlobUri = "bloburi"_ns;
314 static constexpr auto kDataUri = "dataurl"_ns;
315 static constexpr auto kAboutUri = "abouturi"_ns;
316 static constexpr auto kDataUriWebExtCStyle =
317 "dataurl-extension-contentstyle"_ns;
318 static constexpr auto kSingleString = "singlestring"_ns;
319 static constexpr auto kMozillaExtensionFile = "mozillaextension_file"_ns;
320 static constexpr auto kOtherExtensionFile = "otherextension_file"_ns;
321 static constexpr auto kExtensionURI = "extension_uri"_ns;
322 static constexpr auto kSuspectedUserChromeJS = "suspectedUserChromeJS"_ns;
323 #if defined(XP_WIN)
324 static constexpr auto kSanitizedWindowsURL = "sanitizedWindowsURL"_ns;
325 static constexpr auto kSanitizedWindowsPath = "sanitizedWindowsPath"_ns;
326 #endif
327 static constexpr auto kOther = "other"_ns;
328 static constexpr auto kOtherWorker = "other-on-worker"_ns;
329 static constexpr auto kRegexFailure = "regexfailure"_ns;
331 static constexpr auto kUCJSRegex = u"(.+).uc.js\\?*[0-9]*$"_ns;
332 static constexpr auto kExtensionRegex = u"extensions/(.+)@(.+)!(.+)$"_ns;
333 static constexpr auto kSingleFileRegex = u"^[a-zA-Z0-9.?]+$"_ns;
335 if (fileName.IsEmpty()) {
336 return FilenameTypeAndDetails(kOther, Nothing());
339 // resource:// and chrome://
340 if (StringBeginsWith(fileName, u"chrome://"_ns)) {
341 return FilenameTypeAndDetails(kChromeURI, Some(fileName));
343 if (StringBeginsWith(fileName, u"resource://"_ns)) {
344 return FilenameTypeAndDetails(kResourceURI, Some(fileName));
347 // blob: and data:
348 if (StringBeginsWith(fileName, u"blob:"_ns)) {
349 return FilenameTypeAndDetails(kBlobUri, Nothing());
351 if (StringBeginsWith(fileName, u"data:text/css;extension=style;"_ns)) {
352 return FilenameTypeAndDetails(kDataUriWebExtCStyle, Nothing());
354 if (StringBeginsWith(fileName, u"data:"_ns)) {
355 return FilenameTypeAndDetails(kDataUri, Nothing());
358 // Can't do regex matching off-main-thread
359 if (NS_IsMainThread()) {
360 // Extension as loaded via a file://
361 bool regexMatch;
362 nsTArray<nsString> regexResults;
363 nsresult rv = RegexEval(kExtensionRegex, fileName, /* aOnlyMatch = */ false,
364 regexMatch, &regexResults);
365 if (NS_FAILED(rv)) {
366 return FilenameTypeAndDetails(kRegexFailure, Nothing());
368 if (regexMatch) {
369 nsCString type = StringEndsWith(regexResults[2], u"mozilla.org.xpi"_ns)
370 ? kMozillaExtensionFile
371 : kOtherExtensionFile;
372 const auto& extensionNameAndPath =
373 Substring(regexResults[0], ArrayLength("extensions/") - 1);
374 return FilenameTypeAndDetails(
375 type, Some(OptimizeFileName(extensionNameAndPath)));
378 // Single File
379 rv = RegexEval(kSingleFileRegex, fileName, /* aOnlyMatch = */ true,
380 regexMatch);
381 if (NS_FAILED(rv)) {
382 return FilenameTypeAndDetails(kRegexFailure, Nothing());
384 if (regexMatch) {
385 return FilenameTypeAndDetails(kSingleString, Some(fileName));
388 // Suspected userChromeJS script
389 rv = RegexEval(kUCJSRegex, fileName, /* aOnlyMatch = */ true, regexMatch);
390 if (NS_FAILED(rv)) {
391 return FilenameTypeAndDetails(kRegexFailure, Nothing());
393 if (regexMatch) {
394 return FilenameTypeAndDetails(kSuspectedUserChromeJS, Nothing());
398 // Something loaded via an about:// URI.
399 if (StringBeginsWith(fileName, u"about:"_ns)) {
400 // Remove any querystrings and such
401 long int desired_length = fileName.Length();
402 long int possible_new_length = 0;
404 possible_new_length = fileName.FindChar('?');
405 if (possible_new_length != -1 && possible_new_length < desired_length) {
406 desired_length = possible_new_length;
409 possible_new_length = fileName.FindChar('#');
410 if (possible_new_length != -1 && possible_new_length < desired_length) {
411 desired_length = possible_new_length;
414 auto subFileName = Substring(fileName, 0, desired_length);
416 return FilenameTypeAndDetails(kAboutUri, Some(subFileName));
419 // Something loaded via a moz-extension:// URI.
420 if (StringBeginsWith(fileName, u"moz-extension://"_ns)) {
421 if (!collectAdditionalExtensionData) {
422 return FilenameTypeAndDetails(kExtensionURI, Nothing());
425 nsAutoString sanitizedPathAndScheme;
426 sanitizedPathAndScheme.Append(u"moz-extension://["_ns);
428 nsCOMPtr<nsIURI> uri;
429 nsresult rv = NS_NewURI(getter_AddRefs(uri), fileName);
430 if (NS_FAILED(rv)) {
431 // Return after adding ://[ so we know we failed here.
432 return FilenameTypeAndDetails(kExtensionURI,
433 Some(sanitizedPathAndScheme));
436 mozilla::extensions::URLInfo url(uri);
437 if (NS_IsMainThread()) {
438 // EPS is only usable on main thread
439 auto* policy =
440 ExtensionPolicyService::GetSingleton().GetByHost(url.Host());
441 if (policy) {
442 nsString addOnId;
443 policy->GetId(addOnId);
445 sanitizedPathAndScheme.Append(addOnId);
446 sanitizedPathAndScheme.Append(u": "_ns);
447 sanitizedPathAndScheme.Append(policy->Name());
448 sanitizedPathAndScheme.Append(u"]"_ns);
450 if (policy->IsPrivileged()) {
451 sanitizedPathAndScheme.Append(u"P=1"_ns);
452 } else {
453 sanitizedPathAndScheme.Append(u"P=0"_ns);
455 } else {
456 sanitizedPathAndScheme.Append(u"failed finding addon by host]"_ns);
458 } else {
459 sanitizedPathAndScheme.Append(u"can't get addon off main thread]"_ns);
462 AppendUTF8toUTF16(url.FilePath(), sanitizedPathAndScheme);
463 return FilenameTypeAndDetails(kExtensionURI, Some(sanitizedPathAndScheme));
466 #if defined(XP_WIN)
467 auto flags = mozilla::widget::WinUtils::PathTransformFlags::Default |
468 mozilla::widget::WinUtils::PathTransformFlags::RequireFilePath;
469 nsAutoString strSanitizedPath(fileName);
470 if (widget::WinUtils::PreparePathForTelemetry(strSanitizedPath, flags)) {
471 DWORD cchDecodedUrl = INTERNET_MAX_URL_LENGTH;
472 WCHAR szOut[INTERNET_MAX_URL_LENGTH];
473 HRESULT hr;
474 SAFECALL_URLMON_FUNC(CoInternetParseUrl, fileName.get(), PARSE_SCHEMA, 0,
475 szOut, INTERNET_MAX_URL_LENGTH, &cchDecodedUrl, 0);
476 if (hr == S_OK && cchDecodedUrl) {
477 nsAutoString sanitizedPathAndScheme;
478 sanitizedPathAndScheme.Append(szOut);
479 if (sanitizedPathAndScheme == u"file"_ns) {
480 sanitizedPathAndScheme.Append(u"://.../"_ns);
481 sanitizedPathAndScheme.Append(strSanitizedPath);
483 return FilenameTypeAndDetails(kSanitizedWindowsURL,
484 Some(sanitizedPathAndScheme));
485 } else {
486 return FilenameTypeAndDetails(kSanitizedWindowsPath,
487 Some(strSanitizedPath));
490 #endif
492 if (!NS_IsMainThread()) {
493 return FilenameTypeAndDetails(kOtherWorker, Nothing());
495 return FilenameTypeAndDetails(kOther, Nothing());
498 #if defined(EARLY_BETA_OR_EARLIER)
499 // Crash String must be safe from a telemetry point of view.
500 // This will be ensured when this function is used.
501 void PossiblyCrash(const char* aPrefSuffix, const char* aUnsafeCrashString,
502 const nsCString& aSafeCrashString) {
503 if (MOZ_UNLIKELY(!XRE_IsParentProcess())) {
504 // We only crash in the parent (unfortunately) because it's
505 // the only place we can be sure that our only-crash-once
506 // pref-writing works.
507 return;
509 if (!NS_IsMainThread()) {
510 // Setting a pref off the main thread causes ContentParent to observe the
511 // pref set, resulting in a Release Assertion when it tries to update the
512 // child off main thread. So don't do any of this off main thread. (Which
513 // is a bit of a blind spot for this purpose...)
514 return;
517 nsCString previous_crashes("security.crash_tracking.");
518 previous_crashes.Append(aPrefSuffix);
519 previous_crashes.Append(".prevCrashes");
521 nsCString max_crashes("security.crash_tracking.");
522 max_crashes.Append(aPrefSuffix);
523 max_crashes.Append(".maxCrashes");
525 int32_t numberOfPreviousCrashes = 0;
526 numberOfPreviousCrashes = Preferences::GetInt(previous_crashes.get(), 0);
528 int32_t maxAllowableCrashes = 0;
529 maxAllowableCrashes = Preferences::GetInt(max_crashes.get(), 0);
531 if (numberOfPreviousCrashes >= maxAllowableCrashes) {
532 return;
535 nsresult rv =
536 Preferences::SetInt(previous_crashes.get(), ++numberOfPreviousCrashes);
537 if (NS_FAILED(rv)) {
538 return;
541 nsCOMPtr<nsIPrefService> prefsCom = Preferences::GetService();
542 Preferences* prefs = static_cast<Preferences*>(prefsCom.get());
544 if (!prefs->AllowOffMainThreadSave()) {
545 // Do not crash if we can't save prefs off the main thread
546 return;
549 rv = prefs->SavePrefFileBlocking();
550 if (!NS_FAILED(rv)) {
551 // We can only use this in local builds where we don't send stuff up to the
552 // crash reporter because it has user private data.
553 // MOZ_CRASH_UNSAFE_PRINTF("%s",
554 // nsContentSecurityUtils::SmartFormatCrashString(aUnsafeCrashString));
555 MOZ_CRASH_UNSAFE_PRINTF(
556 "%s",
557 nsContentSecurityUtils::SmartFormatCrashString(aSafeCrashString.get()));
560 #endif
562 class EvalUsageNotificationRunnable final : public Runnable {
563 public:
564 EvalUsageNotificationRunnable(bool aIsSystemPrincipal,
565 NS_ConvertUTF8toUTF16& aFileNameA,
566 uint64_t aWindowID, uint32_t aLineNumber,
567 uint32_t aColumnNumber)
568 : mozilla::Runnable("EvalUsageNotificationRunnable"),
569 mIsSystemPrincipal(aIsSystemPrincipal),
570 mFileNameA(aFileNameA),
571 mWindowID(aWindowID),
572 mLineNumber(aLineNumber),
573 mColumnNumber(aColumnNumber) {}
575 NS_IMETHOD Run() override {
576 nsContentSecurityUtils::NotifyEvalUsage(
577 mIsSystemPrincipal, mFileNameA, mWindowID, mLineNumber, mColumnNumber);
578 return NS_OK;
581 void Revoke() {}
583 private:
584 bool mIsSystemPrincipal;
585 NS_ConvertUTF8toUTF16 mFileNameA;
586 uint64_t mWindowID;
587 uint32_t mLineNumber;
588 uint32_t mColumnNumber;
591 /* static */
592 bool nsContentSecurityUtils::IsEvalAllowed(JSContext* cx,
593 bool aIsSystemPrincipal,
594 const nsAString& aScript) {
595 // This allowlist contains files that are permanently allowed to use
596 // eval()-like functions. It will ideally be restricted to files that are
597 // exclusively used in testing contexts.
598 static nsLiteralCString evalAllowlist[] = {
599 // Test-only third-party library
600 "resource://testing-common/sinon-7.2.7.js"_ns,
601 // Test-only utility
602 "resource://testing-common/content-task.js"_ns,
604 // Tracked by Bug 1584605
605 "resource://gre/modules/translation/cld-worker.js"_ns,
607 // require.js implements a script loader for workers. It uses eval
608 // to load the script; but injection is only possible in situations
609 // that you could otherwise control script that gets executed, so
610 // it is okay to allow eval() as it adds no additional attack surface.
611 // Bug 1584564 tracks requiring safe usage of require.js
612 "resource://gre/modules/workers/require.js"_ns,
614 // The profiler's symbolication code uses a wasm module to extract symbols
615 // from the binary files result of local builds.
616 // See bug 1777479
617 "resource://devtools/client/performance-new/shared/symbolication.sys.mjs"_ns,
619 // The Browser Toolbox/Console
620 "debugger"_ns,
623 // We also permit two specific idioms in eval()-like contexts. We'd like to
624 // elminate these too; but there are in-the-wild Mozilla privileged extensions
625 // that use them.
626 static constexpr auto sAllowedEval1 = u"this"_ns;
627 static constexpr auto sAllowedEval2 =
628 u"function anonymous(\n) {\nreturn this\n}"_ns;
630 if (MOZ_LIKELY(!aIsSystemPrincipal && !XRE_IsE10sParentProcess())) {
631 // We restrict eval in the system principal and parent process.
632 // Other uses (like web content and null principal) are allowed.
633 return true;
636 if (JS::ContextOptionsRef(cx).disableEvalSecurityChecks()) {
637 MOZ_LOG(sCSMLog, LogLevel::Debug,
638 ("Allowing eval() because this JSContext was set to allow it"));
639 return true;
642 if (aIsSystemPrincipal &&
643 StaticPrefs::security_allow_eval_with_system_principal()) {
644 MOZ_LOG(sCSMLog, LogLevel::Debug,
645 ("Allowing eval() with System Principal because allowing pref is "
646 "enabled"));
647 return true;
650 if (XRE_IsE10sParentProcess() &&
651 StaticPrefs::security_allow_eval_in_parent_process()) {
652 MOZ_LOG(sCSMLog, LogLevel::Debug,
653 ("Allowing eval() in parent process because allowing pref is "
654 "enabled"));
655 return true;
658 DetectJsHacks();
659 if (MOZ_UNLIKELY(sJSHacksPresent)) {
660 MOZ_LOG(
661 sCSMLog, LogLevel::Debug,
662 ("Allowing eval() %s because some "
663 "JS hacks may be present.",
664 (aIsSystemPrincipal ? "with System Principal" : "in parent process")));
665 return true;
668 if (XRE_IsE10sParentProcess() &&
669 !StaticPrefs::extensions_webextensions_remote()) {
670 MOZ_LOG(sCSMLog, LogLevel::Debug,
671 ("Allowing eval() in parent process because the web extension "
672 "process is disabled"));
673 return true;
676 // We permit these two common idioms to get access to the global JS object
677 if (!aScript.IsEmpty() &&
678 (aScript == sAllowedEval1 || aScript == sAllowedEval2)) {
679 MOZ_LOG(
680 sCSMLog, LogLevel::Debug,
681 ("Allowing eval() %s because a key string is "
682 "provided",
683 (aIsSystemPrincipal ? "with System Principal" : "in parent process")));
684 return true;
687 // Check the allowlist for the provided filename. getFilename is a helper
688 // function
689 nsAutoCString fileName;
690 uint32_t lineNumber = 0, columnNumber = 1;
691 nsJSUtils::GetCallingLocation(cx, fileName, &lineNumber, &columnNumber);
692 if (fileName.IsEmpty()) {
693 fileName = "unknown-file"_ns;
696 NS_ConvertUTF8toUTF16 fileNameA(fileName);
697 for (const nsLiteralCString& allowlistEntry : evalAllowlist) {
698 // checking if current filename begins with entry, because JS Engine
699 // gives us additional stuff for code inside eval or Function ctor
700 // e.g., "require.js > Function"
701 if (StringBeginsWith(fileName, allowlistEntry)) {
702 MOZ_LOG(sCSMLog, LogLevel::Debug,
703 ("Allowing eval() %s because the containing "
704 "file is in the allowlist",
705 (aIsSystemPrincipal ? "with System Principal"
706 : "in parent process")));
707 return true;
711 // Send Telemetry and Log to the Console
712 uint64_t windowID = nsJSUtils::GetCurrentlyRunningCodeInnerWindowID(cx);
713 if (NS_IsMainThread()) {
714 nsContentSecurityUtils::NotifyEvalUsage(aIsSystemPrincipal, fileNameA,
715 windowID, lineNumber, columnNumber);
716 } else {
717 auto runnable = new EvalUsageNotificationRunnable(
718 aIsSystemPrincipal, fileNameA, windowID, lineNumber, columnNumber);
719 NS_DispatchToMainThread(runnable);
722 // Log to MOZ_LOG
723 MOZ_LOG(sCSMLog, LogLevel::Error,
724 ("Blocking eval() %s from file %s and script "
725 "provided %s",
726 (aIsSystemPrincipal ? "with System Principal" : "in parent process"),
727 fileName.get(), NS_ConvertUTF16toUTF8(aScript).get()));
729 // Maybe Crash
730 #if defined(DEBUG) || defined(FUZZING)
731 auto crashString = nsContentSecurityUtils::SmartFormatCrashString(
732 NS_ConvertUTF16toUTF8(aScript).get(), fileName.get(),
733 (aIsSystemPrincipal
734 ? "Blocking eval() with System Principal with script %s from file %s"
735 : "Blocking eval() in parent process with script %s from file %s"));
736 MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get());
737 #endif
739 return false;
742 /* static */
743 void nsContentSecurityUtils::NotifyEvalUsage(bool aIsSystemPrincipal,
744 NS_ConvertUTF8toUTF16& aFileNameA,
745 uint64_t aWindowID,
746 uint32_t aLineNumber,
747 uint32_t aColumnNumber) {
748 // Send Telemetry
749 Telemetry::EventID eventType =
750 aIsSystemPrincipal ? Telemetry::EventID::Security_Evalusage_Systemcontext
751 : Telemetry::EventID::Security_Evalusage_Parentprocess;
753 FilenameTypeAndDetails fileNameTypeAndDetails =
754 FilenameToFilenameType(aFileNameA, false);
755 mozilla::Maybe<nsTArray<EventExtraEntry>> extra;
756 if (fileNameTypeAndDetails.second.isSome()) {
757 extra = Some<nsTArray<EventExtraEntry>>({EventExtraEntry{
758 "fileinfo"_ns,
759 NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())}});
760 } else {
761 extra = Nothing();
763 if (!sTelemetryEventEnabled.exchange(true)) {
764 sTelemetryEventEnabled = true;
765 Telemetry::SetEventRecordingEnabled("security"_ns, true);
767 Telemetry::RecordEvent(eventType, mozilla::Some(fileNameTypeAndDetails.first),
768 extra);
770 // Report an error to console
771 nsCOMPtr<nsIConsoleService> console(
772 do_GetService(NS_CONSOLESERVICE_CONTRACTID));
773 if (!console) {
774 return;
776 nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
777 if (!error) {
778 return;
780 nsCOMPtr<nsIStringBundle> bundle;
781 nsCOMPtr<nsIStringBundleService> stringService =
782 mozilla::components::StringBundle::Service();
783 if (!stringService) {
784 return;
786 stringService->CreateBundle(
787 "chrome://global/locale/security/security.properties",
788 getter_AddRefs(bundle));
789 if (!bundle) {
790 return;
792 nsAutoString message;
793 AutoTArray<nsString, 1> formatStrings = {aFileNameA};
794 nsresult rv = bundle->FormatStringFromName("RestrictBrowserEvalUsage",
795 formatStrings, message);
796 if (NS_FAILED(rv)) {
797 return;
800 rv = error->InitWithWindowID(message, aFileNameA, u""_ns, aLineNumber,
801 aColumnNumber, nsIScriptError::errorFlag,
802 "BrowserEvalUsage", aWindowID,
803 true /* From chrome context */);
804 if (NS_FAILED(rv)) {
805 return;
807 console->LogMessage(error);
810 // If we detect that one of the relevant prefs has been changed, reset
811 // sJSHacksChecked to cause us to re-evaluate all the pref values.
812 // This will stop us from crashing because a user enabled one of these
813 // prefs during a session and then triggered the JavaScript load mitigation
814 // (which can cause a crash).
815 class JSHackPrefObserver final {
816 public:
817 JSHackPrefObserver() = default;
818 static void PrefChanged(const char* aPref, void* aData);
820 protected:
821 ~JSHackPrefObserver() = default;
824 // static
825 void JSHackPrefObserver::PrefChanged(const char* aPref, void* aData) {
826 sJSHacksChecked = false;
829 static bool sJSHackObserverAdded = false;
831 /* static */
832 void nsContentSecurityUtils::DetectJsHacks() {
833 // We can only perform the check of this preference on the Main Thread
834 // (because a String-based preference check is only safe on Main Thread.)
835 // In theory, it would be possible that a separate thread could get here
836 // before the main thread, resulting in the other thread not being able to
837 // perform this check, but the odds of that are small (and probably zero.)
838 if (!NS_IsMainThread()) {
839 return;
842 // If the pref service isn't available, do nothing and re-do this later.
843 if (!Preferences::IsServiceAvailable()) {
844 return;
847 // No need to check again.
848 if (MOZ_LIKELY(sJSHacksChecked || sJSHacksPresent)) {
849 return;
852 static const char* kObservedPrefs[] = {
853 "xpinstall.signatures.required", "general.config.filename",
854 "autoadmin.global_config_url", "autoadmin.failover_to_cached", nullptr};
855 if (MOZ_UNLIKELY(!sJSHackObserverAdded)) {
856 Preferences::RegisterCallbacks(JSHackPrefObserver::PrefChanged,
857 kObservedPrefs);
858 sJSHackObserverAdded = true;
861 nsresult rv;
862 sJSHacksChecked = true;
864 // This preference is required by bootstrapLoader.xpi, which is an
865 // alternate way to load legacy-style extensions. It only works on
866 // DevEdition/Nightly.
867 bool xpinstallSignatures;
868 rv = Preferences::GetBool("xpinstall.signatures.required",
869 &xpinstallSignatures, PrefValueKind::Default);
870 if (!NS_FAILED(rv) && !xpinstallSignatures) {
871 sJSHacksPresent = true;
872 return;
874 rv = Preferences::GetBool("xpinstall.signatures.required",
875 &xpinstallSignatures, PrefValueKind::User);
876 if (!NS_FAILED(rv) && !xpinstallSignatures) {
877 sJSHacksPresent = true;
878 return;
881 // The content process code is probably safe to use for both, but
882 // this hack detection and related efforts has been very fragile so
883 // I'm being extra conservative.
884 if (XRE_IsParentProcess()) {
885 // This preference is a file used for autoconfiguration of Firefox
886 // by administrators. It has also been (ab)used by the userChromeJS
887 // project to run legacy-style 'extensions', some of which use eval,
888 // all of which run in the System Principal context.
889 nsAutoString jsConfigPref;
890 rv = Preferences::GetString("general.config.filename", jsConfigPref,
891 PrefValueKind::Default);
892 if (!NS_FAILED(rv) && !jsConfigPref.IsEmpty()) {
893 sJSHacksPresent = true;
894 return;
896 rv = Preferences::GetString("general.config.filename", jsConfigPref,
897 PrefValueKind::User);
898 if (!NS_FAILED(rv) && !jsConfigPref.IsEmpty()) {
899 sJSHacksPresent = true;
900 return;
903 // These preferences are for autoconfiguration of Firefox by admins.
904 // The first will load a file over the network; the second will
905 // fall back to a local file if the network is unavailable
906 nsAutoString configUrlPref;
907 rv = Preferences::GetString("autoadmin.global_config_url", configUrlPref,
908 PrefValueKind::Default);
909 if (!NS_FAILED(rv) && !configUrlPref.IsEmpty()) {
910 sJSHacksPresent = true;
911 return;
913 rv = Preferences::GetString("autoadmin.global_config_url", configUrlPref,
914 PrefValueKind::User);
915 if (!NS_FAILED(rv) && !configUrlPref.IsEmpty()) {
916 sJSHacksPresent = true;
917 return;
920 } else {
921 if (Preferences::HasDefaultValue("general.config.filename")) {
922 sJSHacksPresent = true;
923 return;
925 if (Preferences::HasUserValue("general.config.filename")) {
926 sJSHacksPresent = true;
927 return;
929 if (Preferences::HasDefaultValue("autoadmin.global_config_url")) {
930 sJSHacksPresent = true;
931 return;
933 if (Preferences::HasUserValue("autoadmin.global_config_url")) {
934 sJSHacksPresent = true;
935 return;
939 bool failOverToCache;
940 rv = Preferences::GetBool("autoadmin.failover_to_cached", &failOverToCache,
941 PrefValueKind::Default);
942 if (!NS_FAILED(rv) && failOverToCache) {
943 sJSHacksPresent = true;
944 return;
946 rv = Preferences::GetBool("autoadmin.failover_to_cached", &failOverToCache,
947 PrefValueKind::User);
948 if (!NS_FAILED(rv) && failOverToCache) {
949 sJSHacksPresent = true;
953 /* static */
954 void nsContentSecurityUtils::DetectCssHacks() {
955 // We can only perform the check of this preference on the Main Thread
956 // It's possible that this function may therefore race and we expect the
957 // caller to ensure that the checks have actually happened.
958 if (!NS_IsMainThread()) {
959 return;
962 // If the pref service isn't available, do nothing and re-do this later.
963 if (!Preferences::IsServiceAvailable()) {
964 return;
967 // No need to check again.
968 if (MOZ_LIKELY(sCSSHacksChecked || sCSSHacksPresent)) {
969 return;
972 // This preference is a bool to see if userChrome css is loaded
973 bool customStylesPresent = Preferences::GetBool(
974 "toolkit.legacyUserProfileCustomizations.stylesheets", false);
975 if (customStylesPresent) {
976 sCSSHacksPresent = true;
979 sCSSHacksChecked = true;
982 /* static */
983 nsresult nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart(
984 nsIChannel* aChannel, nsIHttpChannel** aHttpChannel) {
985 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
986 if (httpChannel) {
987 httpChannel.forget(aHttpChannel);
988 return NS_OK;
991 nsCOMPtr<nsIMultiPartChannel> multipart = do_QueryInterface(aChannel);
992 if (!multipart) {
993 *aHttpChannel = nullptr;
994 return NS_OK;
997 nsCOMPtr<nsIChannel> baseChannel;
998 nsresult rv = multipart->GetBaseChannel(getter_AddRefs(baseChannel));
999 if (NS_WARN_IF(NS_FAILED(rv))) {
1000 return rv;
1003 httpChannel = do_QueryInterface(baseChannel);
1004 httpChannel.forget(aHttpChannel);
1006 return NS_OK;
1009 nsresult CheckCSPFrameAncestorPolicy(nsIChannel* aChannel,
1010 nsIContentSecurityPolicy** aOutCSP) {
1011 MOZ_ASSERT(aChannel);
1013 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1014 ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType();
1015 // frame-ancestor check only makes sense for subdocument and object loads,
1016 // if this is not a load of such type, there is nothing to do here.
1017 if (contentType != ExtContentPolicy::TYPE_SUBDOCUMENT &&
1018 contentType != ExtContentPolicy::TYPE_OBJECT) {
1019 return NS_OK;
1022 // CSP can only hang off an http channel, if this channel is not
1023 // an http channel then there is nothing to do here,
1024 // except with add-ons, where the CSP is stored in a WebExtensionPolicy.
1025 nsCOMPtr<nsIHttpChannel> httpChannel;
1026 nsresult rv = nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart(
1027 aChannel, getter_AddRefs(httpChannel));
1028 if (NS_WARN_IF(NS_FAILED(rv))) {
1029 return rv;
1032 nsAutoCString tCspHeaderValue, tCspROHeaderValue;
1033 if (httpChannel) {
1034 Unused << httpChannel->GetResponseHeader("content-security-policy"_ns,
1035 tCspHeaderValue);
1037 Unused << httpChannel->GetResponseHeader(
1038 "content-security-policy-report-only"_ns, tCspROHeaderValue);
1040 // if there are no CSP values, then there is nothing to do here.
1041 if (tCspHeaderValue.IsEmpty() && tCspROHeaderValue.IsEmpty()) {
1042 return NS_OK;
1046 nsCOMPtr<nsIPrincipal> resultPrincipal;
1047 rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
1048 aChannel, getter_AddRefs(resultPrincipal));
1049 NS_ENSURE_SUCCESS(rv, rv);
1051 RefPtr<extensions::WebExtensionPolicy> addonPolicy;
1052 if (!httpChannel) {
1053 addonPolicy = BasePrincipal::Cast(resultPrincipal)->AddonPolicy();
1054 if (!addonPolicy) {
1055 // Neither a HTTP channel, nor a moz-extension:-resource.
1056 // CSP is not supported.
1057 return NS_OK;
1061 RefPtr<nsCSPContext> csp = new nsCSPContext();
1062 // This CSPContext is only used for checking frame-ancestors, we
1063 // will parse the CSP again anyway. (Unless this blocks the load, but
1064 // parser warnings aren't really important in that case)
1065 csp->SuppressParserLogMessages();
1067 nsCOMPtr<nsIURI> selfURI;
1068 nsAutoString referrerSpec;
1069 if (httpChannel) {
1070 aChannel->GetURI(getter_AddRefs(selfURI));
1071 nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo();
1072 if (referrerInfo) {
1073 referrerInfo->GetComputedReferrerSpec(referrerSpec);
1075 } else {
1076 // aChannel::GetURI would return the jar: or file:-URI for extensions.
1077 // Use the "final" URI to get the actual moz-extension:-URL.
1078 NS_GetFinalChannelURI(aChannel, getter_AddRefs(selfURI));
1081 uint64_t innerWindowID = loadInfo->GetInnerWindowID();
1083 rv = csp->SetRequestContextWithPrincipal(resultPrincipal, selfURI,
1084 referrerSpec, innerWindowID);
1085 if (NS_WARN_IF(NS_FAILED(rv))) {
1086 return rv;
1089 if (addonPolicy) {
1090 csp->AppendPolicy(addonPolicy->BaseCSP(), false, false);
1091 csp->AppendPolicy(addonPolicy->ExtensionPageCSP(), false, false);
1092 } else {
1093 NS_ConvertASCIItoUTF16 cspHeaderValue(tCspHeaderValue);
1094 NS_ConvertASCIItoUTF16 cspROHeaderValue(tCspROHeaderValue);
1096 // ----- if there's a full-strength CSP header, apply it.
1097 if (!cspHeaderValue.IsEmpty()) {
1098 rv = CSP_AppendCSPFromHeader(csp, cspHeaderValue, false);
1099 NS_ENSURE_SUCCESS(rv, rv);
1102 // ----- if there's a report-only CSP header, apply it.
1103 if (!cspROHeaderValue.IsEmpty()) {
1104 rv = CSP_AppendCSPFromHeader(csp, cspROHeaderValue, true);
1105 NS_ENSURE_SUCCESS(rv, rv);
1109 // ----- Enforce frame-ancestor policy on any applied policies
1110 bool safeAncestry = false;
1111 // PermitsAncestry sends violation reports when necessary
1112 rv = csp->PermitsAncestry(loadInfo, &safeAncestry);
1114 if (NS_FAILED(rv) || !safeAncestry) {
1115 // stop! ERROR page!
1116 return NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION;
1119 // return the CSP for x-frame-options check
1120 csp.forget(aOutCSP);
1122 return NS_OK;
1125 void EnforceCSPFrameAncestorPolicy(nsIChannel* aChannel,
1126 const nsresult& aError) {
1127 if (aError == NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION) {
1128 aChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION);
1132 void EnforceXFrameOptionsCheck(nsIChannel* aChannel,
1133 nsIContentSecurityPolicy* aCsp) {
1134 MOZ_ASSERT(aChannel);
1135 bool isFrameOptionsIgnored = false;
1136 // check for XFO options
1137 // XFO checks can be skipped if there are frame ancestors
1138 if (!FramingChecker::CheckFrameOptions(aChannel, aCsp,
1139 isFrameOptionsIgnored)) {
1140 // stop! ERROR page!
1141 aChannel->Cancel(NS_ERROR_XFO_VIOLATION);
1144 if (isFrameOptionsIgnored) {
1145 // log warning to console that xfo is ignored because of CSP
1146 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1147 uint64_t innerWindowID = loadInfo->GetInnerWindowID();
1148 bool privateWindow = !!loadInfo->GetOriginAttributes().mPrivateBrowsingId;
1149 AutoTArray<nsString, 2> params = {u"x-frame-options"_ns,
1150 u"frame-ancestors"_ns};
1151 CSP_LogLocalizedStr("IgnoringSrcBecauseOfDirective", params,
1152 u""_ns, // no sourcefile
1153 u""_ns, // no scriptsample
1154 0, // no linenumber
1155 1, // no columnnumber
1156 nsIScriptError::warningFlag,
1157 "IgnoringSrcBecauseOfDirective"_ns, innerWindowID,
1158 privateWindow);
1162 /* static */
1163 void nsContentSecurityUtils::PerformCSPFrameAncestorAndXFOCheck(
1164 nsIChannel* aChannel) {
1165 nsCOMPtr<nsIContentSecurityPolicy> csp;
1166 nsresult rv = CheckCSPFrameAncestorPolicy(aChannel, getter_AddRefs(csp));
1168 if (NS_FAILED(rv)) {
1169 EnforceCSPFrameAncestorPolicy(aChannel, rv);
1170 return;
1173 // X-Frame-Options needs to be enforced after CSP frame-ancestors
1174 // checks because if frame-ancestors is present, then x-frame-options
1175 // will be discarded
1176 EnforceXFrameOptionsCheck(aChannel, csp);
1178 /* static */
1179 bool nsContentSecurityUtils::CheckCSPFrameAncestorAndXFO(nsIChannel* aChannel) {
1180 nsCOMPtr<nsIContentSecurityPolicy> csp;
1181 nsresult rv = CheckCSPFrameAncestorPolicy(aChannel, getter_AddRefs(csp));
1183 if (NS_FAILED(rv)) {
1184 return false;
1187 bool isFrameOptionsIgnored = false;
1189 return FramingChecker::CheckFrameOptions(aChannel, csp,
1190 isFrameOptionsIgnored);
1193 // https://w3c.github.io/webappsec-csp/#is-element-nonceable
1194 /* static */
1195 nsString nsContentSecurityUtils::GetIsElementNonceableNonce(
1196 const Element& aElement) {
1197 // Step 1. If element does not have an attribute named "nonce", return "Not
1198 // Nonceable".
1199 nsString nonce;
1200 if (nsString* cspNonce =
1201 static_cast<nsString*>(aElement.GetProperty(nsGkAtoms::nonce))) {
1202 nonce = *cspNonce;
1204 if (nonce.IsEmpty()) {
1205 return nonce;
1208 // Step 2. If element is a script element, then for each attribute of
1209 // element’s attribute list:
1210 if (nsCOMPtr<nsIScriptElement> script =
1211 do_QueryInterface(const_cast<Element*>(&aElement))) {
1212 auto containsScriptOrStyle = [](const nsAString& aStr) {
1213 return aStr.LowerCaseFindASCII("<script") != kNotFound ||
1214 aStr.LowerCaseFindASCII("<style") != kNotFound;
1217 nsString value;
1218 uint32_t i = 0;
1219 while (BorrowedAttrInfo info = aElement.GetAttrInfoAt(i++)) {
1220 // Step 2.1. If attribute’s name contains an ASCII case-insensitive match
1221 // for "<script" or "<style", return "Not Nonceable".
1222 const nsAttrName* name = info.mName;
1223 if (nsAtom* prefix = name->GetPrefix()) {
1224 if (containsScriptOrStyle(nsDependentAtomString(prefix))) {
1225 return EmptyString();
1228 if (containsScriptOrStyle(nsDependentAtomString(name->LocalName()))) {
1229 return EmptyString();
1232 // Step 2.2. If attribute’s value contains an ASCII case-insensitive match
1233 // for "<script" or "<style", return "Not Nonceable".
1234 info.mValue->ToString(value);
1235 if (containsScriptOrStyle(value)) {
1236 return EmptyString();
1241 // Step 3. If element had a duplicate-attribute parse error during
1242 // tokenization, return "Not Nonceable".
1243 if (aElement.HasFlag(ELEMENT_PARSER_HAD_DUPLICATE_ATTR_ERROR)) {
1244 return EmptyString();
1247 // Step 4. Return "Nonceable".
1248 return nonce;
1251 #if defined(DEBUG)
1252 /* static */
1253 void nsContentSecurityUtils::AssertAboutPageHasCSP(Document* aDocument) {
1254 // We want to get to a point where all about: pages ship with a CSP. This
1255 // assertion ensures that we can not deploy new about: pages without a CSP.
1256 // Please note that any about: page should not use inline JS or inline CSS,
1257 // and instead should load JS and CSS from an external file (*.js, *.css)
1258 // which allows us to apply a strong CSP omitting 'unsafe-inline'. Ideally,
1259 // the CSP allows precisely the resources that need to be loaded; but it
1260 // should at least be as strong as:
1261 // <meta http-equiv="Content-Security-Policy" content="default-src chrome:;
1262 // object-src 'none'"/>
1264 // This is a data document, created using DOMParser or
1265 // document.implementation.createDocument() or such, not an about: page which
1266 // is loaded as a web page.
1267 if (aDocument->IsLoadedAsData()) {
1268 return;
1271 // Check if we should skip the assertion
1272 if (StaticPrefs::dom_security_skip_about_page_has_csp_assert()) {
1273 return;
1276 // Check if we are loading an about: URI at all
1277 nsCOMPtr<nsIURI> documentURI = aDocument->GetDocumentURI();
1278 if (!documentURI->SchemeIs("about")) {
1279 return;
1282 nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp();
1283 bool foundDefaultSrc = false;
1284 bool foundObjectSrc = false;
1285 bool foundUnsafeEval = false;
1286 bool foundUnsafeInline = false;
1287 bool foundScriptSrc = false;
1288 bool foundWorkerSrc = false;
1289 bool foundWebScheme = false;
1290 if (csp) {
1291 uint32_t policyCount = 0;
1292 csp->GetPolicyCount(&policyCount);
1293 nsAutoString parsedPolicyStr;
1294 for (uint32_t i = 0; i < policyCount; ++i) {
1295 csp->GetPolicyString(i, parsedPolicyStr);
1296 if (parsedPolicyStr.Find(u"default-src") >= 0) {
1297 foundDefaultSrc = true;
1299 if (parsedPolicyStr.Find(u"object-src 'none'") >= 0) {
1300 foundObjectSrc = true;
1302 if (parsedPolicyStr.Find(u"'unsafe-eval'") >= 0) {
1303 foundUnsafeEval = true;
1305 if (parsedPolicyStr.Find(u"'unsafe-inline'") >= 0) {
1306 foundUnsafeInline = true;
1308 if (parsedPolicyStr.Find(u"script-src") >= 0) {
1309 foundScriptSrc = true;
1311 if (parsedPolicyStr.Find(u"worker-src") >= 0) {
1312 foundWorkerSrc = true;
1314 if (parsedPolicyStr.Find(u"http:") >= 0 ||
1315 parsedPolicyStr.Find(u"https:") >= 0) {
1316 foundWebScheme = true;
1321 // Check if we should skip the allowlist and assert right away. Please note
1322 // that this pref can and should only be set for automated testing.
1323 if (StaticPrefs::dom_security_skip_about_page_csp_allowlist_and_assert()) {
1324 NS_ASSERTION(foundDefaultSrc, "about: page must have a CSP");
1325 return;
1328 nsAutoCString aboutSpec;
1329 documentURI->GetSpec(aboutSpec);
1330 ToLowerCase(aboutSpec);
1332 // This allowlist contains about: pages that are permanently allowed to
1333 // render without a CSP applied.
1334 static nsLiteralCString sAllowedAboutPagesWithNoCSP[] = {
1335 // about:blank is a special about page -> no CSP
1336 "about:blank"_ns,
1337 // about:srcdoc is a special about page -> no CSP
1338 "about:srcdoc"_ns,
1339 // about:sync-log displays plain text only -> no CSP
1340 "about:sync-log"_ns,
1341 // about:logo just displays the firefox logo -> no CSP
1342 "about:logo"_ns,
1343 // about:sync is a special mozilla-signed developer addon with low usage ->
1344 // no CSP
1345 "about:sync"_ns,
1346 # if defined(ANDROID)
1347 "about:config"_ns,
1348 # endif
1351 for (const nsLiteralCString& allowlistEntry : sAllowedAboutPagesWithNoCSP) {
1352 // please note that we perform a substring match here on purpose,
1353 // so we don't have to deal and parse out all the query arguments
1354 // the various about pages rely on.
1355 if (StringBeginsWith(aboutSpec, allowlistEntry)) {
1356 return;
1360 MOZ_ASSERT(foundDefaultSrc,
1361 "about: page must contain a CSP including default-src");
1362 MOZ_ASSERT(foundObjectSrc,
1363 "about: page must contain a CSP denying object-src");
1365 // preferences and downloads allow legacy inline scripts through hash src.
1366 MOZ_ASSERT(!foundScriptSrc ||
1367 StringBeginsWith(aboutSpec, "about:preferences"_ns) ||
1368 StringBeginsWith(aboutSpec, "about:settings"_ns) ||
1369 StringBeginsWith(aboutSpec, "about:downloads"_ns) ||
1370 StringBeginsWith(aboutSpec, "about:asrouter"_ns) ||
1371 StringBeginsWith(aboutSpec, "about:newtab"_ns) ||
1372 StringBeginsWith(aboutSpec, "about:logins"_ns) ||
1373 StringBeginsWith(aboutSpec, "about:compat"_ns) ||
1374 StringBeginsWith(aboutSpec, "about:welcome"_ns) ||
1375 StringBeginsWith(aboutSpec, "about:profiling"_ns) ||
1376 StringBeginsWith(aboutSpec, "about:studies"_ns) ||
1377 StringBeginsWith(aboutSpec, "about:home"_ns),
1378 "about: page must not contain a CSP including script-src");
1380 MOZ_ASSERT(!foundWorkerSrc,
1381 "about: page must not contain a CSP including worker-src");
1383 // addons, preferences, debugging, ion, devtools all have to allow some
1384 // remote web resources
1385 MOZ_ASSERT(!foundWebScheme ||
1386 StringBeginsWith(aboutSpec, "about:preferences"_ns) ||
1387 StringBeginsWith(aboutSpec, "about:settings"_ns) ||
1388 StringBeginsWith(aboutSpec, "about:addons"_ns) ||
1389 StringBeginsWith(aboutSpec, "about:newtab"_ns) ||
1390 StringBeginsWith(aboutSpec, "about:debugging"_ns) ||
1391 StringBeginsWith(aboutSpec, "about:ion"_ns) ||
1392 StringBeginsWith(aboutSpec, "about:compat"_ns) ||
1393 StringBeginsWith(aboutSpec, "about:logins"_ns) ||
1394 StringBeginsWith(aboutSpec, "about:home"_ns) ||
1395 StringBeginsWith(aboutSpec, "about:welcome"_ns) ||
1396 StringBeginsWith(aboutSpec, "about:devtools"_ns) ||
1397 StringBeginsWith(aboutSpec, "about:pocket-saved"_ns) ||
1398 StringBeginsWith(aboutSpec, "about:pocket-home"_ns),
1399 "about: page must not contain a CSP including a web scheme");
1401 if (aDocument->IsExtensionPage()) {
1402 // Extensions have two CSP policies applied where the baseline CSP
1403 // includes 'unsafe-eval' and 'unsafe-inline', hence we have to skip
1404 // the 'unsafe-eval' and 'unsafe-inline' assertions for extension
1405 // pages.
1406 return;
1409 MOZ_ASSERT(!foundUnsafeEval,
1410 "about: page must not contain a CSP including 'unsafe-eval'");
1412 static nsLiteralCString sLegacyUnsafeInlineAllowList[] = {
1413 // Bug 1579160: Remove 'unsafe-inline' from style-src within
1414 // about:preferences
1415 "about:preferences"_ns,
1416 "about:settings"_ns,
1417 // Bug 1571346: Remove 'unsafe-inline' from style-src within about:addons
1418 "about:addons"_ns,
1419 // Bug 1584485: Remove 'unsafe-inline' from style-src within:
1420 // * about:newtab
1421 // * about:welcome
1422 // * about:home
1423 "about:newtab"_ns,
1424 "about:welcome"_ns,
1425 "about:home"_ns,
1428 for (const nsLiteralCString& aUnsafeInlineEntry :
1429 sLegacyUnsafeInlineAllowList) {
1430 // please note that we perform a substring match here on purpose,
1431 // so we don't have to deal and parse out all the query arguments
1432 // the various about pages rely on.
1433 if (StringBeginsWith(aboutSpec, aUnsafeInlineEntry)) {
1434 return;
1438 MOZ_ASSERT(!foundUnsafeInline,
1439 "about: page must not contain a CSP including 'unsafe-inline'");
1441 #endif
1443 /* static */
1444 bool nsContentSecurityUtils::ValidateScriptFilename(JSContext* cx,
1445 const char* aFilename) {
1446 // If the pref is permissive, allow everything
1447 if (StaticPrefs::security_allow_parent_unrestricted_js_loads()) {
1448 return true;
1451 // If we're not in the parent process allow everything (presently)
1452 if (!XRE_IsE10sParentProcess()) {
1453 return true;
1456 // If we have allowed eval (because of a user configuration or more
1457 // likely a test has requested it), and the script is an eval, allow it.
1458 NS_ConvertUTF8toUTF16 filenameU(aFilename);
1459 if (StaticPrefs::security_allow_eval_with_system_principal() ||
1460 StaticPrefs::security_allow_eval_in_parent_process()) {
1461 if (StringEndsWith(filenameU, u"> eval"_ns)) {
1462 return true;
1466 DetectJsHacks();
1468 if (MOZ_UNLIKELY(!sJSHacksChecked)) {
1469 MOZ_LOG(
1470 sCSMLog, LogLevel::Debug,
1471 ("Allowing a javascript load of %s because "
1472 "we have not yet been able to determine if JS hacks may be present",
1473 aFilename));
1474 return true;
1477 if (MOZ_UNLIKELY(sJSHacksPresent)) {
1478 MOZ_LOG(sCSMLog, LogLevel::Debug,
1479 ("Allowing a javascript load of %s because "
1480 "some JS hacks may be present",
1481 aFilename));
1482 return true;
1485 if (XRE_IsE10sParentProcess() &&
1486 !StaticPrefs::extensions_webextensions_remote()) {
1487 MOZ_LOG(sCSMLog, LogLevel::Debug,
1488 ("Allowing a javascript load of %s because the web extension "
1489 "process is disabled.",
1490 aFilename));
1491 return true;
1494 if (StringBeginsWith(filenameU, u"chrome://"_ns)) {
1495 // If it's a chrome:// url, allow it
1496 return true;
1498 if (StringBeginsWith(filenameU, u"resource://"_ns)) {
1499 // If it's a resource:// url, allow it
1500 return true;
1502 if (StringBeginsWith(filenameU, u"file://"_ns)) {
1503 // We will temporarily allow all file:// URIs through for now
1504 return true;
1506 if (StringBeginsWith(filenameU, u"jar:file://"_ns)) {
1507 // We will temporarily allow all jar URIs through for now
1508 return true;
1510 if (filenameU.Equals(u"about:sync-log"_ns)) {
1511 // about:sync-log runs in the parent process and displays a directory
1512 // listing. The listing has inline javascript that executes on load.
1513 return true;
1516 if (StringBeginsWith(filenameU, u"moz-extension://"_ns)) {
1517 nsCOMPtr<nsIURI> uri;
1518 nsresult rv = NS_NewURI(getter_AddRefs(uri), aFilename);
1519 if (!NS_FAILED(rv) && NS_IsMainThread()) {
1520 mozilla::extensions::URLInfo url(uri);
1521 auto* policy =
1522 ExtensionPolicyService::GetSingleton().GetByHost(url.Host());
1524 if (policy && policy->IsPrivileged()) {
1525 MOZ_LOG(sCSMLog, LogLevel::Debug,
1526 ("Allowing a javascript load of %s because the web extension "
1527 "it is associated with is privileged.",
1528 aFilename));
1529 return true;
1532 } else if (!NS_IsMainThread()) {
1533 WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
1534 if (workerPrivate && workerPrivate->IsPrivilegedAddonGlobal()) {
1535 MOZ_LOG(sCSMLog, LogLevel::Debug,
1536 ("Allowing a javascript load of %s because the web extension "
1537 "it is associated with is privileged.",
1538 aFilename));
1539 return true;
1543 auto kAllowedFilenamesExact = {
1544 // Allow through the injection provided by about:sync addon
1545 u"data:,new function() {\n const { AboutSyncRedirector } = ChromeUtils.import(\"chrome://aboutsync/content/AboutSyncRedirector.js\");\n AboutSyncRedirector.register();\n}"_ns,
1548 for (auto allowedFilename : kAllowedFilenamesExact) {
1549 if (filenameU == allowedFilename) {
1550 return true;
1554 auto kAllowedFilenamesPrefix = {
1555 // Until 371900 is fixed, we need to do something about about:downloads
1556 // and this is the most reasonable. See 1727770
1557 u"about:downloads"_ns,
1558 // We think this is the same problem as about:downloads
1559 u"about:preferences"_ns, u"about:settings"_ns,
1560 // Browser console will give a filename of 'debugger' See 1763943
1561 // Sometimes it's 'debugger eager eval code', other times just 'debugger
1562 // eval code'
1563 u"debugger"_ns};
1565 for (auto allowedFilenamePrefix : kAllowedFilenamesPrefix) {
1566 if (StringBeginsWith(filenameU, allowedFilenamePrefix)) {
1567 return true;
1571 // Log to MOZ_LOG
1572 MOZ_LOG(sCSMLog, LogLevel::Error,
1573 ("ValidateScriptFilename Failed: %s\n", aFilename));
1575 // Send Telemetry
1576 FilenameTypeAndDetails fileNameTypeAndDetails =
1577 FilenameToFilenameType(filenameU, true);
1579 Telemetry::EventID eventType =
1580 Telemetry::EventID::Security_Javascriptload_Parentprocess;
1582 mozilla::Maybe<nsTArray<EventExtraEntry>> extra;
1583 if (fileNameTypeAndDetails.second.isSome()) {
1584 extra = Some<nsTArray<EventExtraEntry>>({EventExtraEntry{
1585 "fileinfo"_ns,
1586 NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value())}});
1587 } else {
1588 extra = Nothing();
1591 if (!sTelemetryEventEnabled.exchange(true)) {
1592 sTelemetryEventEnabled = true;
1593 Telemetry::SetEventRecordingEnabled("security"_ns, true);
1595 Telemetry::RecordEvent(eventType, mozilla::Some(fileNameTypeAndDetails.first),
1596 extra);
1598 #if defined(DEBUG) || defined(FUZZING)
1599 auto crashString = nsContentSecurityUtils::SmartFormatCrashString(
1600 aFilename,
1601 fileNameTypeAndDetails.second.isSome()
1602 ? NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value()).get()
1603 : "(None)",
1604 "Blocking a script load %s from file %s");
1605 MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get());
1606 #elif defined(EARLY_BETA_OR_EARLIER)
1607 // Cause a crash (if we've never crashed before and we can ensure we won't do
1608 // it again.)
1609 // The details in the second arg, passed to UNSAFE_PRINTF, are also included
1610 // in Event Telemetry and have received data review.
1611 if (fileNameTypeAndDetails.second.isSome()) {
1612 PossiblyCrash("js_load_1", aFilename,
1613 NS_ConvertUTF16toUTF8(fileNameTypeAndDetails.second.value()));
1614 } else {
1615 PossiblyCrash("js_load_1", aFilename, "(None)"_ns);
1617 #endif
1619 // Presently we are only enforcing restrictions for the script filename
1620 // on Nightly. On all channels we are reporting Telemetry. In the future we
1621 // will assert in debug builds and return false to prevent execution in
1622 // non-debug builds.
1623 #ifdef NIGHTLY_BUILD
1624 return false;
1625 #else
1626 return true;
1627 #endif
1630 /* static */
1631 void nsContentSecurityUtils::LogMessageToConsole(nsIHttpChannel* aChannel,
1632 const char* aMsg) {
1633 nsCOMPtr<nsIURI> uri;
1634 nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
1635 if (NS_FAILED(rv)) {
1636 return;
1639 uint64_t windowID = 0;
1640 rv = aChannel->GetTopLevelContentWindowId(&windowID);
1641 if (NS_WARN_IF(NS_FAILED(rv))) {
1642 return;
1644 if (!windowID) {
1645 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1646 loadInfo->GetInnerWindowID(&windowID);
1649 nsAutoString localizedMsg;
1650 nsAutoCString spec;
1651 uri->GetSpec(spec);
1652 AutoTArray<nsString, 1> params = {NS_ConvertUTF8toUTF16(spec)};
1653 rv = nsContentUtils::FormatLocalizedString(
1654 nsContentUtils::eSECURITY_PROPERTIES, aMsg, params, localizedMsg);
1655 if (NS_WARN_IF(NS_FAILED(rv))) {
1656 return;
1659 nsContentUtils::ReportToConsoleByWindowID(
1660 localizedMsg, nsIScriptError::warningFlag, "Security"_ns, windowID, uri);
1663 /* static */
1664 long nsContentSecurityUtils::ClassifyDownload(
1665 nsIChannel* aChannel, const nsAutoCString& aMimeTypeGuess) {
1666 MOZ_ASSERT(aChannel, "IsDownloadAllowed without channel?");
1668 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1670 nsCOMPtr<nsIURI> contentLocation;
1671 aChannel->GetURI(getter_AddRefs(contentLocation));
1673 if (StaticPrefs::dom_block_download_insecure()) {
1674 // If we are not dealing with a potentially trustworthy origin, or a URI
1675 // that is safe to be loaded like e.g. data:, then we block the load.
1676 bool isInsecureDownload =
1677 !nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(
1678 contentLocation) &&
1679 !nsMixedContentBlocker::URISafeToBeLoadedInSecureContext(
1680 contentLocation);
1682 Telemetry::Accumulate(mozilla::Telemetry::INSECURE_DOWNLOADS,
1683 isInsecureDownload);
1685 if (isInsecureDownload) {
1686 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
1687 if (httpChannel) {
1688 LogMessageToConsole(httpChannel, "BlockedInsecureDownload");
1690 return nsITransfer::DOWNLOAD_POTENTIALLY_UNSAFE;
1694 if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) {
1695 return nsITransfer::DOWNLOAD_ACCEPTABLE;
1698 uint32_t triggeringFlags = loadInfo->GetTriggeringSandboxFlags();
1699 uint32_t currentflags = loadInfo->GetSandboxFlags();
1701 if ((triggeringFlags & SANDBOXED_ALLOW_DOWNLOADS) ||
1702 (currentflags & SANDBOXED_ALLOW_DOWNLOADS)) {
1703 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
1704 if (httpChannel) {
1705 LogMessageToConsole(httpChannel, "IframeSandboxBlockedDownload");
1707 return nsITransfer::DOWNLOAD_FORBIDDEN;
1709 return nsITransfer::DOWNLOAD_ACCEPTABLE;