Bug 1941046 - Part 4: Send a callback request for impression and clicks of MARS Top...
[gecko.git] / dom / security / nsContentSecurityUtils.cpp
blob8dd7b70f13e3774dc52841bbdc9c7c8f7046bae9
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/glean/DomSecurityMetrics.h"
44 #include "mozilla/StaticPrefs_security.h"
45 #include "LoadInfo.h"
46 #include "mozilla/StaticPrefs_extensions.h"
47 #include "mozilla/StaticPrefs_dom.h"
48 #include "nsIConsoleService.h"
49 #include "nsIStringBundle.h"
51 using namespace mozilla;
52 using namespace mozilla::dom;
54 extern mozilla::LazyLogModule sCSMLog;
55 extern Atomic<bool, mozilla::Relaxed> sJSHacksChecked;
56 extern Atomic<bool, mozilla::Relaxed> sJSHacksPresent;
57 extern Atomic<bool, mozilla::Relaxed> sCSSHacksChecked;
58 extern Atomic<bool, mozilla::Relaxed> sCSSHacksPresent;
60 // Helper function for IsConsideredSameOriginForUIR which makes
61 // Principals of scheme 'http' return Principals of scheme 'https'.
62 static already_AddRefed<nsIPrincipal> MakeHTTPPrincipalHTTPS(
63 nsIPrincipal* aPrincipal) {
64 nsCOMPtr<nsIPrincipal> principal = aPrincipal;
65 // if the principal is not http, then it can also not be upgraded
66 // to https.
67 if (!principal->SchemeIs("http")) {
68 return principal.forget();
71 nsAutoCString spec;
72 aPrincipal->GetAsciiSpec(spec);
73 // replace http with https
74 spec.ReplaceLiteral(0, 4, "https");
76 nsCOMPtr<nsIURI> newURI;
77 nsresult rv = NS_NewURI(getter_AddRefs(newURI), spec);
78 if (NS_WARN_IF(NS_FAILED(rv))) {
79 return nullptr;
82 mozilla::OriginAttributes OA =
83 BasePrincipal::Cast(aPrincipal)->OriginAttributesRef();
85 principal = BasePrincipal::CreateContentPrincipal(newURI, OA);
86 return principal.forget();
89 /* static */
90 bool nsContentSecurityUtils::IsConsideredSameOriginForUIR(
91 nsIPrincipal* aTriggeringPrincipal, nsIPrincipal* aResultPrincipal) {
92 MOZ_ASSERT(aTriggeringPrincipal);
93 MOZ_ASSERT(aResultPrincipal);
94 // we only have to make sure that the following truth table holds:
95 // aTriggeringPrincipal | aResultPrincipal | Result
96 // ----------------------------------------------------------------
97 // http://example.com/foo.html | http://example.com/bar.html | true
98 // http://example.com/foo.html | https://example.com/bar.html | true
99 // https://example.com/foo.html | https://example.com/bar.html | true
100 // https://example.com/foo.html | http://example.com/bar.html | true
102 // fast path if both principals are same-origin
103 if (aTriggeringPrincipal->Equals(aResultPrincipal)) {
104 return true;
107 // in case a principal uses a scheme of 'http' then we just upgrade to
108 // 'https' and use the principal equals comparison operator to check
109 // for same-origin.
110 nsCOMPtr<nsIPrincipal> compareTriggeringPrincipal =
111 MakeHTTPPrincipalHTTPS(aTriggeringPrincipal);
113 nsCOMPtr<nsIPrincipal> compareResultPrincipal =
114 MakeHTTPPrincipalHTTPS(aResultPrincipal);
116 return compareTriggeringPrincipal->Equals(compareResultPrincipal);
120 * Performs a Regular Expression match, optionally returning the results.
121 * This function is not safe to use OMT.
123 * @param aPattern The regex pattern
124 * @param aString The string to compare against
125 * @param aOnlyMatch Whether we want match results or only a true/false for
126 * the match
127 * @param aMatchResult Out param for whether or not the pattern matched
128 * @param aRegexResults Out param for the matches of the regex, if requested
129 * @returns nsresult indicating correct function operation or error
131 nsresult RegexEval(const nsAString& aPattern, const nsAString& aString,
132 bool aOnlyMatch, bool& aMatchResult,
133 nsTArray<nsString>* aRegexResults = nullptr) {
134 MOZ_ASSERT(NS_IsMainThread());
135 aMatchResult = false;
137 mozilla::dom::AutoJSAPI jsapi;
138 jsapi.Init();
140 JSContext* cx = jsapi.cx();
141 mozilla::AutoDisableJSInterruptCallback disabler(cx);
143 // We can use the junk scope here, because we're just using it for regexp
144 // evaluation, not actual script execution, and we disable statics so that the
145 // evaluation does not interact with the execution global.
146 JSAutoRealm ar(cx, xpc::PrivilegedJunkScope());
148 JS::Rooted<JSObject*> regexp(
149 cx, JS::NewUCRegExpObject(cx, aPattern.BeginReading(), aPattern.Length(),
150 JS::RegExpFlag::Unicode));
151 if (!regexp) {
152 return NS_ERROR_ILLEGAL_VALUE;
155 JS::Rooted<JS::Value> regexResult(cx, JS::NullValue());
157 size_t index = 0;
158 if (!JS::ExecuteRegExpNoStatics(cx, regexp, aString.BeginReading(),
159 aString.Length(), &index, aOnlyMatch,
160 &regexResult)) {
161 return NS_ERROR_FAILURE;
164 if (regexResult.isNull()) {
165 // On no match, ExecuteRegExpNoStatics returns Null
166 return NS_OK;
168 if (aOnlyMatch) {
169 // On match, with aOnlyMatch = true, ExecuteRegExpNoStatics returns boolean
170 // true.
171 MOZ_ASSERT(regexResult.isBoolean() && regexResult.toBoolean());
172 aMatchResult = true;
173 return NS_OK;
175 if (aRegexResults == nullptr) {
176 return NS_ERROR_INVALID_ARG;
179 // Now we know we have a result, and we need to extract it so we can read it.
180 uint32_t length;
181 JS::Rooted<JSObject*> regexResultObj(cx, &regexResult.toObject());
182 if (!JS::GetArrayLength(cx, regexResultObj, &length)) {
183 return NS_ERROR_NOT_AVAILABLE;
185 MOZ_LOG(sCSMLog, LogLevel::Verbose, ("Regex Matched %i strings", length));
187 for (uint32_t i = 0; i < length; i++) {
188 JS::Rooted<JS::Value> element(cx);
189 if (!JS_GetElement(cx, regexResultObj, i, &element)) {
190 return NS_ERROR_NO_CONTENT;
193 nsAutoJSString value;
194 if (!value.init(cx, element)) {
195 return NS_ERROR_NO_CONTENT;
198 MOZ_LOG(sCSMLog, LogLevel::Verbose,
199 ("Regex Matching: %i: %s", i, NS_ConvertUTF16toUTF8(value).get()));
200 aRegexResults->AppendElement(value);
203 aMatchResult = true;
204 return NS_OK;
208 * MOZ_CRASH_UNSAFE_PRINTF has a sPrintfCrashReasonSize-sized buffer. We need
209 * to make sure we don't exceed it. These functions perform this check and
210 * munge things for us.
215 * Destructively truncates a string to fit within the limit
217 char* nsContentSecurityUtils::SmartFormatCrashString(const char* str) {
218 return nsContentSecurityUtils::SmartFormatCrashString(strdup(str));
221 char* nsContentSecurityUtils::SmartFormatCrashString(char* str) {
222 auto str_len = strlen(str);
224 if (str_len > sPrintfCrashReasonSize) {
225 str[sPrintfCrashReasonSize - 1] = '\0';
226 str_len = strlen(str);
228 MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize > str_len);
230 return str;
234 * Destructively truncates two strings to fit within the limit.
235 * format_string is a format string containing two %s entries
236 * The second string will be truncated to the _last_ 25 characters
237 * The first string will be truncated to the remaining limit.
239 nsCString nsContentSecurityUtils::SmartFormatCrashString(
240 const char* part1, const char* part2, const char* format_string) {
241 return SmartFormatCrashString(strdup(part1), strdup(part2), format_string);
244 nsCString nsContentSecurityUtils::SmartFormatCrashString(
245 char* part1, char* part2, const char* format_string) {
246 auto part1_len = strlen(part1);
247 auto part2_len = strlen(part2);
249 auto constant_len = strlen(format_string) - 4;
251 if (part1_len + part2_len + constant_len > sPrintfCrashReasonSize) {
252 if (part2_len > 25) {
253 part2 += (part2_len - 25);
255 part2_len = strlen(part2);
257 part1[sPrintfCrashReasonSize - (constant_len + part2_len + 1)] = '\0';
258 part1_len = strlen(part1);
260 MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize >
261 constant_len + part1_len + part2_len);
263 auto parts = nsPrintfCString(format_string, part1, part2);
264 return std::move(parts);
268 * Telemetry Events extra data only supports 80 characters, so we optimize the
269 * filename to be smaller and collect more data.
271 nsCString OptimizeFileName(const nsAString& aFileName) {
272 nsCString optimizedName;
273 CopyUTF16toUTF8(aFileName, optimizedName);
275 MOZ_LOG(sCSMLog, LogLevel::Verbose,
276 ("Optimizing FileName: %s", optimizedName.get()));
278 optimizedName.ReplaceSubstring(".xpi!"_ns, "!"_ns);
279 optimizedName.ReplaceSubstring("shield.mozilla.org!"_ns, "s!"_ns);
280 optimizedName.ReplaceSubstring("mozilla.org!"_ns, "m!"_ns);
281 if (optimizedName.Length() > 80) {
282 optimizedName.Truncate(80);
285 MOZ_LOG(sCSMLog, LogLevel::Verbose,
286 ("Optimized FileName: %s", optimizedName.get()));
287 return optimizedName;
291 * FilenameToFilenameType takes a fileName and returns a Pair of strings.
292 * The First entry is a string indicating the type of fileName
293 * The Second entry is a Maybe<string> that can contain additional details to
294 * report.
296 * The reason we use strings (instead of an int/enum) is because the Telemetry
297 * Events API only accepts strings.
299 * Function is a static member of the class to enable gtests.
302 /* static */
303 FilenameTypeAndDetails nsContentSecurityUtils::FilenameToFilenameType(
304 const nsACString& fileName, bool collectAdditionalExtensionData) {
305 // These are strings because the Telemetry Events API only accepts strings
306 static constexpr auto kChromeURI = "chromeuri"_ns;
307 static constexpr auto kResourceURI = "resourceuri"_ns;
308 static constexpr auto kBlobUri = "bloburi"_ns;
309 static constexpr auto kDataUri = "dataurl"_ns;
310 static constexpr auto kAboutUri = "abouturi"_ns;
311 static constexpr auto kDataUriWebExtCStyle =
312 "dataurl-extension-contentstyle"_ns;
313 static constexpr auto kSingleString = "singlestring"_ns;
314 static constexpr auto kMozillaExtensionFile = "mozillaextension_file"_ns;
315 static constexpr auto kOtherExtensionFile = "otherextension_file"_ns;
316 static constexpr auto kExtensionURI = "extension_uri"_ns;
317 static constexpr auto kSuspectedUserChromeJS = "suspectedUserChromeJS"_ns;
318 #if defined(XP_WIN)
319 static constexpr auto kSanitizedWindowsURL = "sanitizedWindowsURL"_ns;
320 static constexpr auto kSanitizedWindowsPath = "sanitizedWindowsPath"_ns;
321 #endif
322 static constexpr auto kOther = "other"_ns;
323 static constexpr auto kOtherWorker = "other-on-worker"_ns;
324 static constexpr auto kRegexFailure = "regexfailure"_ns;
326 static constexpr auto kUCJSRegex = u"(.+).uc.js\\?*[0-9]*$"_ns;
327 static constexpr auto kExtensionRegex = u"extensions/(.+)@(.+)!(.+)$"_ns;
328 static constexpr auto kSingleFileRegex = u"^[a-zA-Z0-9.?]+$"_ns;
330 if (fileName.IsEmpty()) {
331 return FilenameTypeAndDetails(kOther, Nothing());
334 // resource:// and chrome://
335 // These don't contain any user (profile) paths.
336 if (StringBeginsWith(fileName, "chrome://"_ns)) {
337 if (StringBeginsWith(fileName, "chrome://userscripts/"_ns) ||
338 StringBeginsWith(fileName, "chrome://userchromejs/"_ns) ||
339 StringBeginsWith(fileName, "chrome://tabmix"_ns) ||
340 StringBeginsWith(fileName, "chrome://searchwp/"_ns)) {
341 return FilenameTypeAndDetails(kSuspectedUserChromeJS,
342 Some(nsCString(fileName)));
344 return FilenameTypeAndDetails(kChromeURI, Some(nsCString(fileName)));
346 if (StringBeginsWith(fileName, "resource://"_ns)) {
347 if (StringBeginsWith(fileName, "resource://usl-ucjs/"_ns) ||
348 StringBeginsWith(fileName, "resource://sfm-ucjs/"_ns) ||
349 StringBeginsWith(fileName, "resource://cpmanager-legacy/"_ns)) {
350 return FilenameTypeAndDetails(kSuspectedUserChromeJS,
351 Some(nsCString(fileName)));
353 return FilenameTypeAndDetails(kResourceURI, Some(nsCString(fileName)));
356 // blob: and data:
357 if (StringBeginsWith(fileName, "blob:"_ns)) {
358 return FilenameTypeAndDetails(kBlobUri, Nothing());
360 if (StringBeginsWith(fileName, "data:text/css;extension=style;"_ns)) {
361 return FilenameTypeAndDetails(kDataUriWebExtCStyle, Nothing());
363 if (StringBeginsWith(fileName, "data:"_ns)) {
364 return FilenameTypeAndDetails(kDataUri, Nothing());
367 // Can't do regex matching off-main-thread
368 if (NS_IsMainThread()) {
369 NS_ConvertUTF8toUTF16 fileNameA(fileName);
370 // Extension as loaded via a file://
371 bool regexMatch;
372 nsTArray<nsString> regexResults;
373 nsresult rv =
374 RegexEval(kExtensionRegex, fileNameA,
375 /* aOnlyMatch = */ false, regexMatch, &regexResults);
376 if (NS_FAILED(rv)) {
377 return FilenameTypeAndDetails(kRegexFailure, Nothing());
379 if (regexMatch) {
380 nsCString type = StringEndsWith(regexResults[2], u"mozilla.org.xpi"_ns)
381 ? kMozillaExtensionFile
382 : kOtherExtensionFile;
383 const auto& extensionNameAndPath =
384 Substring(regexResults[0], std::size("extensions/") - 1);
385 return FilenameTypeAndDetails(
386 type, Some(OptimizeFileName(extensionNameAndPath)));
389 // Single File
390 rv = RegexEval(kSingleFileRegex, fileNameA, /* aOnlyMatch = */ true,
391 regexMatch);
392 if (NS_FAILED(rv)) {
393 return FilenameTypeAndDetails(kRegexFailure, Nothing());
395 if (regexMatch) {
396 return FilenameTypeAndDetails(kSingleString, Some(nsCString(fileName)));
399 // Suspected userChromeJS script
400 rv = RegexEval(kUCJSRegex, fileNameA, /* aOnlyMatch = */ true, regexMatch);
401 if (NS_FAILED(rv)) {
402 return FilenameTypeAndDetails(kRegexFailure, Nothing());
404 if (regexMatch) {
405 return FilenameTypeAndDetails(kSuspectedUserChromeJS, Nothing());
409 // Something loaded via an about:// URI.
410 if (StringBeginsWith(fileName, "about:"_ns)) {
411 // Remove any querystrings and such
412 long int desired_length = fileName.Length();
413 long int possible_new_length = 0;
415 possible_new_length = fileName.FindChar('?');
416 if (possible_new_length != -1 && possible_new_length < desired_length) {
417 desired_length = possible_new_length;
420 possible_new_length = fileName.FindChar('#');
421 if (possible_new_length != -1 && possible_new_length < desired_length) {
422 desired_length = possible_new_length;
425 auto subFileName = Substring(fileName, 0, desired_length);
427 return FilenameTypeAndDetails(kAboutUri, Some(nsCString(subFileName)));
430 // Something loaded via a moz-extension:// URI.
431 if (StringBeginsWith(fileName, "moz-extension://"_ns)) {
432 if (!collectAdditionalExtensionData) {
433 return FilenameTypeAndDetails(kExtensionURI, Nothing());
436 nsAutoCString sanitizedPathAndScheme;
437 sanitizedPathAndScheme.Append("moz-extension://["_ns);
439 nsCOMPtr<nsIURI> uri;
440 nsresult rv = NS_NewURI(getter_AddRefs(uri), fileName);
441 if (NS_FAILED(rv)) {
442 // Return after adding ://[ so we know we failed here.
443 return FilenameTypeAndDetails(kExtensionURI,
444 Some(sanitizedPathAndScheme));
447 mozilla::extensions::URLInfo url(uri);
448 if (NS_IsMainThread()) {
449 // EPS is only usable on main thread
450 auto* policy =
451 ExtensionPolicyService::GetSingleton().GetByHost(url.Host());
452 if (policy) {
453 nsString addOnId;
454 policy->GetId(addOnId);
456 sanitizedPathAndScheme.Append(NS_ConvertUTF16toUTF8(addOnId));
457 sanitizedPathAndScheme.Append(": "_ns);
458 sanitizedPathAndScheme.Append(NS_ConvertUTF16toUTF8(policy->Name()));
459 sanitizedPathAndScheme.Append("]"_ns);
461 if (policy->IsPrivileged()) {
462 sanitizedPathAndScheme.Append("P=1"_ns);
463 } else {
464 sanitizedPathAndScheme.Append("P=0"_ns);
466 } else {
467 sanitizedPathAndScheme.Append("failed finding addon by host]"_ns);
469 } else {
470 sanitizedPathAndScheme.Append("can't get addon off main thread]"_ns);
473 sanitizedPathAndScheme.Append(url.FilePath());
474 return FilenameTypeAndDetails(kExtensionURI, Some(sanitizedPathAndScheme));
477 #if defined(XP_WIN)
478 auto flags = mozilla::widget::WinUtils::PathTransformFlags::Default |
479 mozilla::widget::WinUtils::PathTransformFlags::RequireFilePath;
480 const NS_ConvertUTF8toUTF16 fileNameA(fileName);
481 nsAutoString strSanitizedPath(fileNameA);
482 if (widget::WinUtils::PreparePathForTelemetry(strSanitizedPath, flags)) {
483 DWORD cchDecodedUrl = INTERNET_MAX_URL_LENGTH;
484 WCHAR szOut[INTERNET_MAX_URL_LENGTH];
485 HRESULT hr;
486 SAFECALL_URLMON_FUNC(CoInternetParseUrl, fileNameA.get(), PARSE_SCHEMA, 0,
487 szOut, INTERNET_MAX_URL_LENGTH, &cchDecodedUrl, 0);
488 if (hr == S_OK && cchDecodedUrl) {
489 nsAutoString sanitizedPathAndScheme;
490 sanitizedPathAndScheme.Append(szOut);
491 if (sanitizedPathAndScheme == u"file"_ns) {
492 sanitizedPathAndScheme.Append(u"://.../"_ns);
493 sanitizedPathAndScheme.Append(strSanitizedPath);
495 return FilenameTypeAndDetails(
496 kSanitizedWindowsURL,
497 Some(NS_ConvertUTF16toUTF8(sanitizedPathAndScheme)));
498 } else {
499 return FilenameTypeAndDetails(
500 kSanitizedWindowsPath, Some(NS_ConvertUTF16toUTF8(strSanitizedPath)));
503 #endif
505 if (!NS_IsMainThread()) {
506 return FilenameTypeAndDetails(kOtherWorker, Nothing());
508 return FilenameTypeAndDetails(kOther, Nothing());
511 #if defined(EARLY_BETA_OR_EARLIER)
512 // Crash String must be safe from a telemetry point of view.
513 // This will be ensured when this function is used.
514 void PossiblyCrash(const char* aPrefSuffix, const char* aUnsafeCrashString,
515 const nsCString& aSafeCrashString) {
516 if (MOZ_UNLIKELY(!XRE_IsParentProcess())) {
517 // We only crash in the parent (unfortunately) because it's
518 // the only place we can be sure that our only-crash-once
519 // pref-writing works.
520 return;
522 if (!NS_IsMainThread()) {
523 // Setting a pref off the main thread causes ContentParent to observe the
524 // pref set, resulting in a Release Assertion when it tries to update the
525 // child off main thread. So don't do any of this off main thread. (Which
526 // is a bit of a blind spot for this purpose...)
527 return;
530 nsCString previous_crashes("security.crash_tracking.");
531 previous_crashes.Append(aPrefSuffix);
532 previous_crashes.Append(".prevCrashes");
534 nsCString max_crashes("security.crash_tracking.");
535 max_crashes.Append(aPrefSuffix);
536 max_crashes.Append(".maxCrashes");
538 int32_t numberOfPreviousCrashes = 0;
539 numberOfPreviousCrashes = Preferences::GetInt(previous_crashes.get(), 0);
541 int32_t maxAllowableCrashes = 0;
542 maxAllowableCrashes = Preferences::GetInt(max_crashes.get(), 0);
544 if (numberOfPreviousCrashes >= maxAllowableCrashes) {
545 return;
548 nsresult rv =
549 Preferences::SetInt(previous_crashes.get(), ++numberOfPreviousCrashes);
550 if (NS_FAILED(rv)) {
551 return;
554 nsCOMPtr<nsIPrefService> prefsCom = Preferences::GetService();
555 Preferences* prefs = static_cast<Preferences*>(prefsCom.get());
557 if (!prefs->AllowOffMainThreadSave()) {
558 // Do not crash if we can't save prefs off the main thread
559 return;
562 rv = prefs->SavePrefFileBlocking();
563 if (!NS_FAILED(rv)) {
564 // We can only use this in local builds where we don't send stuff up to the
565 // crash reporter because it has user private data.
566 // MOZ_CRASH_UNSAFE_PRINTF("%s",
567 // nsContentSecurityUtils::SmartFormatCrashString(aUnsafeCrashString));
568 MOZ_CRASH_UNSAFE_PRINTF(
569 "%s",
570 nsContentSecurityUtils::SmartFormatCrashString(aSafeCrashString.get()));
573 #endif
575 class EvalUsageNotificationRunnable final : public Runnable {
576 public:
577 EvalUsageNotificationRunnable(bool aIsSystemPrincipal,
578 const nsACString& aFileName, uint64_t aWindowID,
579 uint32_t aLineNumber, uint32_t aColumnNumber)
580 : mozilla::Runnable("EvalUsageNotificationRunnable"),
581 mIsSystemPrincipal(aIsSystemPrincipal),
582 mFileName(aFileName),
583 mWindowID(aWindowID),
584 mLineNumber(aLineNumber),
585 mColumnNumber(aColumnNumber) {}
587 NS_IMETHOD Run() override {
588 nsContentSecurityUtils::NotifyEvalUsage(
589 mIsSystemPrincipal, mFileName, mWindowID, mLineNumber, mColumnNumber);
590 return NS_OK;
593 void Revoke() {}
595 private:
596 bool mIsSystemPrincipal;
597 nsCString mFileName;
598 uint64_t mWindowID;
599 uint32_t mLineNumber;
600 uint32_t mColumnNumber;
603 /* static */
604 bool nsContentSecurityUtils::IsEvalAllowed(JSContext* cx,
605 bool aIsSystemPrincipal,
606 const nsAString& aScript) {
607 // This allowlist contains files that are permanently allowed to use
608 // eval()-like functions. It will ideally be restricted to files that are
609 // exclusively used in testing contexts.
610 static nsLiteralCString evalAllowlist[] = {
611 // Test-only third-party library
612 "resource://testing-common/sinon-7.2.7.js"_ns,
613 // Test-only utility
614 "resource://testing-common/content-task.js"_ns,
616 // Tracked by Bug 1584605
617 "resource://gre/modules/translation/cld-worker.js"_ns,
619 // require.js implements a script loader for workers. It uses eval
620 // to load the script; but injection is only possible in situations
621 // that you could otherwise control script that gets executed, so
622 // it is okay to allow eval() as it adds no additional attack surface.
623 // Bug 1584564 tracks requiring safe usage of require.js
624 "resource://gre/modules/workers/require.js"_ns,
626 // The profiler's symbolication code uses a wasm module to extract symbols
627 // from the binary files result of local builds.
628 // See bug 1777479
629 "resource://devtools/client/performance-new/shared/symbolication.sys.mjs"_ns,
631 // The Browser Toolbox/Console
632 "debugger"_ns,
635 // We also permit two specific idioms in eval()-like contexts. We'd like to
636 // elminate these too; but there are in-the-wild Mozilla privileged extensions
637 // that use them.
638 static constexpr auto sAllowedEval1 = u"this"_ns;
639 static constexpr auto sAllowedEval2 =
640 u"function anonymous(\n) {\nreturn this\n}"_ns;
642 if (MOZ_LIKELY(!aIsSystemPrincipal && !XRE_IsE10sParentProcess())) {
643 // We restrict eval in the system principal and parent process.
644 // Other uses (like web content and null principal) are allowed.
645 return true;
648 if (JS::ContextOptionsRef(cx).disableEvalSecurityChecks()) {
649 MOZ_LOG(sCSMLog, LogLevel::Debug,
650 ("Allowing eval() because this JSContext was set to allow it"));
651 return true;
654 if (aIsSystemPrincipal &&
655 StaticPrefs::security_allow_eval_with_system_principal()) {
656 MOZ_LOG(sCSMLog, LogLevel::Debug,
657 ("Allowing eval() with System Principal because allowing pref is "
658 "enabled"));
659 return true;
662 if (XRE_IsE10sParentProcess() &&
663 StaticPrefs::security_allow_eval_in_parent_process()) {
664 MOZ_LOG(sCSMLog, LogLevel::Debug,
665 ("Allowing eval() in parent process because allowing pref is "
666 "enabled"));
667 return true;
670 DetectJsHacks();
671 if (MOZ_UNLIKELY(sJSHacksPresent)) {
672 MOZ_LOG(
673 sCSMLog, LogLevel::Debug,
674 ("Allowing eval() %s because some "
675 "JS hacks may be present.",
676 (aIsSystemPrincipal ? "with System Principal" : "in parent process")));
677 return true;
680 if (XRE_IsE10sParentProcess() &&
681 !StaticPrefs::extensions_webextensions_remote()) {
682 MOZ_LOG(sCSMLog, LogLevel::Debug,
683 ("Allowing eval() in parent process because the web extension "
684 "process is disabled"));
685 return true;
688 // We permit these two common idioms to get access to the global JS object
689 if (!aScript.IsEmpty() &&
690 (aScript == sAllowedEval1 || aScript == sAllowedEval2)) {
691 MOZ_LOG(
692 sCSMLog, LogLevel::Debug,
693 ("Allowing eval() %s because a key string is "
694 "provided",
695 (aIsSystemPrincipal ? "with System Principal" : "in parent process")));
696 return true;
699 // Check the allowlist for the provided filename. getFilename is a helper
700 // function
701 auto location = JSCallingLocation::Get(cx);
702 const nsCString& fileName = location.FileName();
703 for (const nsLiteralCString& allowlistEntry : evalAllowlist) {
704 // checking if current filename begins with entry, because JS Engine
705 // gives us additional stuff for code inside eval or Function ctor
706 // e.g., "require.js > Function"
707 if (StringBeginsWith(fileName, allowlistEntry)) {
708 MOZ_LOG(sCSMLog, LogLevel::Debug,
709 ("Allowing eval() %s because the containing "
710 "file is in the allowlist",
711 (aIsSystemPrincipal ? "with System Principal"
712 : "in parent process")));
713 return true;
717 // Send Telemetry and Log to the Console
718 uint64_t windowID = nsJSUtils::GetCurrentlyRunningCodeInnerWindowID(cx);
719 if (NS_IsMainThread()) {
720 nsContentSecurityUtils::NotifyEvalUsage(aIsSystemPrincipal, fileName,
721 windowID, location.mLine,
722 location.mColumn);
723 } else {
724 auto runnable = new EvalUsageNotificationRunnable(
725 aIsSystemPrincipal, fileName, windowID, location.mLine,
726 location.mColumn);
727 NS_DispatchToMainThread(runnable);
730 // Log to MOZ_LOG
731 MOZ_LOG(sCSMLog, LogLevel::Error,
732 ("Blocking eval() %s from file %s and script "
733 "provided %s",
734 (aIsSystemPrincipal ? "with System Principal" : "in parent process"),
735 fileName.get(), NS_ConvertUTF16toUTF8(aScript).get()));
737 // Maybe Crash
738 #if defined(DEBUG) || defined(FUZZING)
739 auto crashString = nsContentSecurityUtils::SmartFormatCrashString(
740 NS_ConvertUTF16toUTF8(aScript).get(), fileName.get(),
741 (aIsSystemPrincipal
742 ? "Blocking eval() with System Principal with script %s from file %s"
743 : "Blocking eval() in parent process with script %s from file %s"));
744 MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get());
745 #endif
747 return false;
750 /* static */
751 void nsContentSecurityUtils::NotifyEvalUsage(bool aIsSystemPrincipal,
752 const nsACString& aFileName,
753 uint64_t aWindowID,
754 uint32_t aLineNumber,
755 uint32_t aColumnNumber) {
756 FilenameTypeAndDetails fileNameTypeAndDetails =
757 FilenameToFilenameType(aFileName, false);
758 auto fileinfo = fileNameTypeAndDetails.second;
759 auto value = Some(fileNameTypeAndDetails.first);
760 if (aIsSystemPrincipal) {
761 glean::security::EvalUsageSystemContextExtra extra = {
762 .fileinfo = fileinfo,
763 .value = value,
765 glean::security::eval_usage_system_context.Record(Some(extra));
766 } else {
767 glean::security::EvalUsageParentProcessExtra extra = {
768 .fileinfo = fileinfo,
769 .value = value,
771 glean::security::eval_usage_parent_process.Record(Some(extra));
774 // Report an error to console
775 nsCOMPtr<nsIConsoleService> console(
776 do_GetService(NS_CONSOLESERVICE_CONTRACTID));
777 if (!console) {
778 return;
780 nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
781 if (!error) {
782 return;
784 nsCOMPtr<nsIStringBundle> bundle;
785 nsCOMPtr<nsIStringBundleService> stringService =
786 mozilla::components::StringBundle::Service();
787 if (!stringService) {
788 return;
790 stringService->CreateBundle(
791 "chrome://global/locale/security/security.properties",
792 getter_AddRefs(bundle));
793 if (!bundle) {
794 return;
796 nsAutoString message;
797 NS_ConvertUTF8toUTF16 fileNameA(aFileName);
798 AutoTArray<nsString, 1> formatStrings = {fileNameA};
799 nsresult rv = bundle->FormatStringFromName("RestrictBrowserEvalUsage",
800 formatStrings, message);
801 if (NS_FAILED(rv)) {
802 return;
805 rv = error->InitWithWindowID(message, aFileName, aLineNumber, aColumnNumber,
806 nsIScriptError::errorFlag, "BrowserEvalUsage",
807 aWindowID, true /* From chrome context */);
808 if (NS_FAILED(rv)) {
809 return;
811 console->LogMessage(error);
814 // If we detect that one of the relevant prefs has been changed, reset
815 // sJSHacksChecked to cause us to re-evaluate all the pref values.
816 // This will stop us from crashing because a user enabled one of these
817 // prefs during a session and then triggered the JavaScript load mitigation
818 // (which can cause a crash).
819 class JSHackPrefObserver final {
820 public:
821 JSHackPrefObserver() = default;
822 static void PrefChanged(const char* aPref, void* aData);
824 protected:
825 ~JSHackPrefObserver() = default;
828 // static
829 void JSHackPrefObserver::PrefChanged(const char* aPref, void* aData) {
830 sJSHacksChecked = false;
833 static bool sJSHackObserverAdded = false;
835 /* static */
836 void nsContentSecurityUtils::DetectJsHacks() {
837 // We can only perform the check of this preference on the Main Thread
838 // (because a String-based preference check is only safe on Main Thread.)
839 // In theory, it would be possible that a separate thread could get here
840 // before the main thread, resulting in the other thread not being able to
841 // perform this check, but the odds of that are small (and probably zero.)
842 if (!NS_IsMainThread()) {
843 return;
846 // If the pref service isn't available, do nothing and re-do this later.
847 if (!Preferences::IsServiceAvailable()) {
848 return;
851 // No need to check again.
852 if (MOZ_LIKELY(sJSHacksChecked || sJSHacksPresent)) {
853 return;
856 static const char* kObservedPrefs[] = {
857 "xpinstall.signatures.required", "general.config.filename",
858 "autoadmin.global_config_url", "autoadmin.failover_to_cached", nullptr};
859 if (MOZ_UNLIKELY(!sJSHackObserverAdded)) {
860 Preferences::RegisterCallbacks(JSHackPrefObserver::PrefChanged,
861 kObservedPrefs);
862 sJSHackObserverAdded = true;
865 nsresult rv;
866 sJSHacksChecked = true;
868 // This preference is required by bootstrapLoader.xpi, which is an
869 // alternate way to load legacy-style extensions. It only works on
870 // DevEdition/Nightly.
871 bool xpinstallSignatures;
872 rv = Preferences::GetBool("xpinstall.signatures.required",
873 &xpinstallSignatures, PrefValueKind::Default);
874 if (!NS_FAILED(rv) && !xpinstallSignatures) {
875 sJSHacksPresent = true;
876 return;
878 rv = Preferences::GetBool("xpinstall.signatures.required",
879 &xpinstallSignatures, PrefValueKind::User);
880 if (!NS_FAILED(rv) && !xpinstallSignatures) {
881 sJSHacksPresent = true;
882 return;
885 if (Preferences::HasDefaultValue("general.config.filename")) {
886 sJSHacksPresent = true;
887 return;
889 if (Preferences::HasUserValue("general.config.filename")) {
890 sJSHacksPresent = true;
891 return;
893 if (Preferences::HasDefaultValue("autoadmin.global_config_url")) {
894 sJSHacksPresent = true;
895 return;
897 if (Preferences::HasUserValue("autoadmin.global_config_url")) {
898 sJSHacksPresent = true;
899 return;
902 bool failOverToCache;
903 rv = Preferences::GetBool("autoadmin.failover_to_cached", &failOverToCache,
904 PrefValueKind::Default);
905 if (!NS_FAILED(rv) && failOverToCache) {
906 sJSHacksPresent = true;
907 return;
909 rv = Preferences::GetBool("autoadmin.failover_to_cached", &failOverToCache,
910 PrefValueKind::User);
911 if (!NS_FAILED(rv) && failOverToCache) {
912 sJSHacksPresent = true;
916 /* static */
917 void nsContentSecurityUtils::DetectCssHacks() {
918 // We can only perform the check of this preference on the Main Thread
919 // It's possible that this function may therefore race and we expect the
920 // caller to ensure that the checks have actually happened.
921 if (!NS_IsMainThread()) {
922 return;
925 // If the pref service isn't available, do nothing and re-do this later.
926 if (!Preferences::IsServiceAvailable()) {
927 return;
930 // No need to check again.
931 if (MOZ_LIKELY(sCSSHacksChecked || sCSSHacksPresent)) {
932 return;
935 // This preference is a bool to see if userChrome css is loaded
936 bool customStylesPresent = Preferences::GetBool(
937 "toolkit.legacyUserProfileCustomizations.stylesheets", false);
938 if (customStylesPresent) {
939 sCSSHacksPresent = true;
942 sCSSHacksChecked = true;
945 /* static */
946 nsresult nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart(
947 nsIChannel* aChannel, nsIHttpChannel** aHttpChannel) {
948 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
949 if (httpChannel) {
950 httpChannel.forget(aHttpChannel);
951 return NS_OK;
954 nsCOMPtr<nsIMultiPartChannel> multipart = do_QueryInterface(aChannel);
955 if (!multipart) {
956 *aHttpChannel = nullptr;
957 return NS_OK;
960 nsCOMPtr<nsIChannel> baseChannel;
961 nsresult rv = multipart->GetBaseChannel(getter_AddRefs(baseChannel));
962 if (NS_WARN_IF(NS_FAILED(rv))) {
963 return rv;
966 httpChannel = do_QueryInterface(baseChannel);
967 httpChannel.forget(aHttpChannel);
969 return NS_OK;
972 nsresult CheckCSPFrameAncestorPolicy(nsIChannel* aChannel,
973 nsIContentSecurityPolicy** aOutCSP) {
974 MOZ_ASSERT(aChannel);
976 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
977 ExtContentPolicyType contentType = loadInfo->GetExternalContentPolicyType();
978 // frame-ancestor check only makes sense for subdocument and object loads,
979 // if this is not a load of such type, there is nothing to do here.
980 if (contentType != ExtContentPolicy::TYPE_SUBDOCUMENT &&
981 contentType != ExtContentPolicy::TYPE_OBJECT) {
982 return NS_OK;
985 // CSP can only hang off an http channel, if this channel is not
986 // an http channel then there is nothing to do here,
987 // except with add-ons, where the CSP is stored in a WebExtensionPolicy.
988 nsCOMPtr<nsIHttpChannel> httpChannel;
989 nsresult rv = nsContentSecurityUtils::GetHttpChannelFromPotentialMultiPart(
990 aChannel, getter_AddRefs(httpChannel));
991 if (NS_WARN_IF(NS_FAILED(rv))) {
992 return rv;
995 nsAutoCString tCspHeaderValue, tCspROHeaderValue;
996 if (httpChannel) {
997 Unused << httpChannel->GetResponseHeader("content-security-policy"_ns,
998 tCspHeaderValue);
1000 Unused << httpChannel->GetResponseHeader(
1001 "content-security-policy-report-only"_ns, tCspROHeaderValue);
1003 // if there are no CSP values, then there is nothing to do here.
1004 if (tCspHeaderValue.IsEmpty() && tCspROHeaderValue.IsEmpty()) {
1005 return NS_OK;
1009 nsCOMPtr<nsIPrincipal> resultPrincipal;
1010 rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
1011 aChannel, getter_AddRefs(resultPrincipal));
1012 NS_ENSURE_SUCCESS(rv, rv);
1014 RefPtr<extensions::WebExtensionPolicy> addonPolicy;
1015 if (!httpChannel) {
1016 addonPolicy = BasePrincipal::Cast(resultPrincipal)->AddonPolicy();
1017 if (!addonPolicy) {
1018 // Neither a HTTP channel, nor a moz-extension:-resource.
1019 // CSP is not supported.
1020 return NS_OK;
1024 RefPtr<nsCSPContext> csp = new nsCSPContext();
1025 // This CSPContext is only used for checking frame-ancestors, we
1026 // will parse the CSP again anyway. (Unless this blocks the load, but
1027 // parser warnings aren't really important in that case)
1028 csp->SuppressParserLogMessages();
1030 nsCOMPtr<nsIURI> selfURI;
1031 nsAutoCString referrerSpec;
1032 if (httpChannel) {
1033 aChannel->GetURI(getter_AddRefs(selfURI));
1034 nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo();
1035 if (referrerInfo) {
1036 referrerInfo->GetComputedReferrerSpec(referrerSpec);
1038 } else {
1039 // aChannel::GetURI would return the jar: or file:-URI for extensions.
1040 // Use the "final" URI to get the actual moz-extension:-URL.
1041 NS_GetFinalChannelURI(aChannel, getter_AddRefs(selfURI));
1044 uint64_t innerWindowID = loadInfo->GetInnerWindowID();
1046 rv = csp->SetRequestContextWithPrincipal(resultPrincipal, selfURI,
1047 referrerSpec, innerWindowID);
1048 if (NS_WARN_IF(NS_FAILED(rv))) {
1049 return rv;
1052 if (addonPolicy) {
1053 csp->AppendPolicy(addonPolicy->BaseCSP(), false, false);
1054 csp->AppendPolicy(addonPolicy->ExtensionPageCSP(), false, false);
1055 } else {
1056 NS_ConvertASCIItoUTF16 cspHeaderValue(tCspHeaderValue);
1057 NS_ConvertASCIItoUTF16 cspROHeaderValue(tCspROHeaderValue);
1059 // ----- if there's a full-strength CSP header, apply it.
1060 if (!cspHeaderValue.IsEmpty()) {
1061 rv = CSP_AppendCSPFromHeader(csp, cspHeaderValue, false);
1062 NS_ENSURE_SUCCESS(rv, rv);
1065 // ----- if there's a report-only CSP header, apply it.
1066 if (!cspROHeaderValue.IsEmpty()) {
1067 rv = CSP_AppendCSPFromHeader(csp, cspROHeaderValue, true);
1068 NS_ENSURE_SUCCESS(rv, rv);
1072 // ----- Enforce frame-ancestor policy on any applied policies
1073 bool safeAncestry = false;
1074 // PermitsAncestry sends violation reports when necessary
1075 rv = csp->PermitsAncestry(loadInfo, &safeAncestry);
1077 if (NS_FAILED(rv) || !safeAncestry) {
1078 // stop! ERROR page!
1079 return NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION;
1082 // return the CSP for x-frame-options check
1083 csp.forget(aOutCSP);
1085 return NS_OK;
1088 void EnforceCSPFrameAncestorPolicy(nsIChannel* aChannel,
1089 const nsresult& aError) {
1090 if (aError == NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION) {
1091 aChannel->Cancel(NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION);
1095 void EnforceXFrameOptionsCheck(nsIChannel* aChannel,
1096 nsIContentSecurityPolicy* aCsp) {
1097 MOZ_ASSERT(aChannel);
1098 bool isFrameOptionsIgnored = false;
1099 // check for XFO options
1100 // XFO checks can be skipped if there are frame ancestors
1101 if (!FramingChecker::CheckFrameOptions(aChannel, aCsp,
1102 isFrameOptionsIgnored)) {
1103 // stop! ERROR page!
1104 aChannel->Cancel(NS_ERROR_XFO_VIOLATION);
1107 if (isFrameOptionsIgnored) {
1108 // log warning to console that xfo is ignored because of CSP
1109 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1110 uint64_t innerWindowID = loadInfo->GetInnerWindowID();
1111 bool privateWindow = loadInfo->GetOriginAttributes().IsPrivateBrowsing();
1112 AutoTArray<nsString, 2> params = {u"x-frame-options"_ns,
1113 u"frame-ancestors"_ns};
1114 CSP_LogLocalizedStr("IgnoringSrcBecauseOfDirective", params,
1115 ""_ns, // no sourcefile
1116 u""_ns, // no scriptsample
1117 0, // no linenumber
1118 1, // no columnnumber
1119 nsIScriptError::warningFlag,
1120 "IgnoringSrcBecauseOfDirective"_ns, innerWindowID,
1121 privateWindow);
1125 /* static */
1126 void nsContentSecurityUtils::PerformCSPFrameAncestorAndXFOCheck(
1127 nsIChannel* aChannel) {
1128 nsCOMPtr<nsIContentSecurityPolicy> csp;
1129 nsresult rv = CheckCSPFrameAncestorPolicy(aChannel, getter_AddRefs(csp));
1131 if (NS_FAILED(rv)) {
1132 EnforceCSPFrameAncestorPolicy(aChannel, rv);
1133 return;
1136 // X-Frame-Options needs to be enforced after CSP frame-ancestors
1137 // checks because if frame-ancestors is present, then x-frame-options
1138 // will be discarded
1139 EnforceXFrameOptionsCheck(aChannel, csp);
1141 /* static */
1142 bool nsContentSecurityUtils::CheckCSPFrameAncestorAndXFO(nsIChannel* aChannel) {
1143 nsCOMPtr<nsIContentSecurityPolicy> csp;
1144 nsresult rv = CheckCSPFrameAncestorPolicy(aChannel, getter_AddRefs(csp));
1146 if (NS_FAILED(rv)) {
1147 return false;
1150 bool isFrameOptionsIgnored = false;
1152 return FramingChecker::CheckFrameOptions(aChannel, csp,
1153 isFrameOptionsIgnored);
1156 // https://w3c.github.io/webappsec-csp/#is-element-nonceable
1157 /* static */
1158 nsString nsContentSecurityUtils::GetIsElementNonceableNonce(
1159 const Element& aElement) {
1160 // Step 1. If element does not have an attribute named "nonce", return "Not
1161 // Nonceable".
1162 nsString nonce;
1163 if (nsString* cspNonce =
1164 static_cast<nsString*>(aElement.GetProperty(nsGkAtoms::nonce))) {
1165 nonce = *cspNonce;
1167 if (nonce.IsEmpty()) {
1168 return nonce;
1171 // Step 2. If element is a script element, then for each attribute of
1172 // element’s attribute list:
1173 if (nsCOMPtr<nsIScriptElement> script =
1174 do_QueryInterface(const_cast<Element*>(&aElement))) {
1175 auto containsScriptOrStyle = [](const nsAString& aStr) {
1176 return aStr.LowerCaseFindASCII("<script") != kNotFound ||
1177 aStr.LowerCaseFindASCII("<style") != kNotFound;
1180 nsString value;
1181 uint32_t i = 0;
1182 while (BorrowedAttrInfo info = aElement.GetAttrInfoAt(i++)) {
1183 // Step 2.1. If attribute’s name contains an ASCII case-insensitive match
1184 // for "<script" or "<style", return "Not Nonceable".
1185 const nsAttrName* name = info.mName;
1186 if (nsAtom* prefix = name->GetPrefix()) {
1187 if (containsScriptOrStyle(nsDependentAtomString(prefix))) {
1188 return EmptyString();
1191 if (containsScriptOrStyle(nsDependentAtomString(name->LocalName()))) {
1192 return EmptyString();
1195 // Step 2.2. If attribute’s value contains an ASCII case-insensitive match
1196 // for "<script" or "<style", return "Not Nonceable".
1197 info.mValue->ToString(value);
1198 if (containsScriptOrStyle(value)) {
1199 return EmptyString();
1204 // Step 3. If element had a duplicate-attribute parse error during
1205 // tokenization, return "Not Nonceable".
1206 if (aElement.HasFlag(ELEMENT_PARSER_HAD_DUPLICATE_ATTR_ERROR)) {
1207 return EmptyString();
1210 // Step 4. Return "Nonceable".
1211 return nonce;
1214 #if defined(DEBUG)
1215 /* static */
1216 void nsContentSecurityUtils::AssertAboutPageHasCSP(Document* aDocument) {
1217 // We want to get to a point where all about: pages ship with a CSP. This
1218 // assertion ensures that we can not deploy new about: pages without a CSP.
1219 // Please note that any about: page should not use inline JS or inline CSS,
1220 // and instead should load JS and CSS from an external file (*.js, *.css)
1221 // which allows us to apply a strong CSP omitting 'unsafe-inline'. Ideally,
1222 // the CSP allows precisely the resources that need to be loaded; but it
1223 // should at least be as strong as:
1224 // <meta http-equiv="Content-Security-Policy" content="default-src chrome:;
1225 // object-src 'none'"/>
1227 // This is a data document, created using DOMParser or
1228 // document.implementation.createDocument() or such, not an about: page which
1229 // is loaded as a web page.
1230 if (aDocument->IsLoadedAsData()) {
1231 return;
1234 // Check if we should skip the assertion
1235 if (StaticPrefs::dom_security_skip_about_page_has_csp_assert()) {
1236 return;
1239 // Check if we are loading an about: URI at all
1240 nsCOMPtr<nsIURI> documentURI = aDocument->GetDocumentURI();
1241 if (!documentURI->SchemeIs("about")) {
1242 return;
1245 nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp();
1246 bool foundDefaultSrc = false;
1247 bool foundObjectSrc = false;
1248 bool foundUnsafeEval = false;
1249 bool foundUnsafeInline = false;
1250 bool foundScriptSrc = false;
1251 bool foundWorkerSrc = false;
1252 bool foundWebScheme = false;
1253 if (csp) {
1254 uint32_t policyCount = 0;
1255 csp->GetPolicyCount(&policyCount);
1256 nsAutoString parsedPolicyStr;
1257 for (uint32_t i = 0; i < policyCount; ++i) {
1258 csp->GetPolicyString(i, parsedPolicyStr);
1259 if (parsedPolicyStr.Find(u"default-src") >= 0) {
1260 foundDefaultSrc = true;
1262 if (parsedPolicyStr.Find(u"object-src 'none'") >= 0) {
1263 foundObjectSrc = true;
1265 if (parsedPolicyStr.Find(u"'unsafe-eval'") >= 0) {
1266 foundUnsafeEval = true;
1268 if (parsedPolicyStr.Find(u"'unsafe-inline'") >= 0) {
1269 foundUnsafeInline = true;
1271 if (parsedPolicyStr.Find(u"script-src") >= 0) {
1272 foundScriptSrc = true;
1274 if (parsedPolicyStr.Find(u"worker-src") >= 0) {
1275 foundWorkerSrc = true;
1277 if (parsedPolicyStr.Find(u"http:") >= 0 ||
1278 parsedPolicyStr.Find(u"https:") >= 0) {
1279 foundWebScheme = true;
1284 // Check if we should skip the allowlist and assert right away. Please note
1285 // that this pref can and should only be set for automated testing.
1286 if (StaticPrefs::dom_security_skip_about_page_csp_allowlist_and_assert()) {
1287 NS_ASSERTION(foundDefaultSrc, "about: page must have a CSP");
1288 return;
1291 nsAutoCString aboutSpec;
1292 documentURI->GetSpec(aboutSpec);
1293 ToLowerCase(aboutSpec);
1295 // This allowlist contains about: pages that are permanently allowed to
1296 // render without a CSP applied.
1297 static nsLiteralCString sAllowedAboutPagesWithNoCSP[] = {
1298 // about:blank is a special about page -> no CSP
1299 "about:blank"_ns,
1300 // about:srcdoc is a special about page -> no CSP
1301 "about:srcdoc"_ns,
1302 // about:sync-log displays plain text only -> no CSP
1303 "about:sync-log"_ns,
1304 // about:logo just displays the firefox logo -> no CSP
1305 "about:logo"_ns,
1306 // about:sync is a special mozilla-signed developer addon with low usage
1307 // ->
1308 // no CSP
1309 "about:sync"_ns,
1310 # if defined(ANDROID)
1311 "about:config"_ns,
1312 # endif
1315 for (const nsLiteralCString& allowlistEntry : sAllowedAboutPagesWithNoCSP) {
1316 // please note that we perform a substring match here on purpose,
1317 // so we don't have to deal and parse out all the query arguments
1318 // the various about pages rely on.
1319 if (StringBeginsWith(aboutSpec, allowlistEntry)) {
1320 return;
1324 MOZ_ASSERT(foundDefaultSrc,
1325 "about: page must contain a CSP including default-src");
1326 MOZ_ASSERT(foundObjectSrc,
1327 "about: page must contain a CSP denying object-src");
1329 // preferences and downloads allow legacy inline scripts through hash src.
1330 MOZ_ASSERT(
1331 !foundScriptSrc || StringBeginsWith(aboutSpec, "about:preferences"_ns) ||
1332 StringBeginsWith(aboutSpec, "about:settings"_ns) ||
1333 StringBeginsWith(aboutSpec, "about:downloads"_ns) ||
1334 StringBeginsWith(aboutSpec, "about:fingerprintingprotection"_ns) ||
1335 StringBeginsWith(aboutSpec, "about:asrouter"_ns) ||
1336 StringBeginsWith(aboutSpec, "about:newtab"_ns) ||
1337 StringBeginsWith(aboutSpec, "about:logins"_ns) ||
1338 StringBeginsWith(aboutSpec, "about:compat"_ns) ||
1339 StringBeginsWith(aboutSpec, "about:welcome"_ns) ||
1340 StringBeginsWith(aboutSpec, "about:profiling"_ns) ||
1341 StringBeginsWith(aboutSpec, "about:studies"_ns) ||
1342 StringBeginsWith(aboutSpec, "about:home"_ns),
1343 "about: page must not contain a CSP including script-src");
1345 MOZ_ASSERT(!foundWorkerSrc,
1346 "about: page must not contain a CSP including worker-src");
1348 // addons, preferences, debugging, ion, devtools all have to allow some
1349 // remote web resources
1350 MOZ_ASSERT(!foundWebScheme ||
1351 StringBeginsWith(aboutSpec, "about:preferences"_ns) ||
1352 StringBeginsWith(aboutSpec, "about:settings"_ns) ||
1353 StringBeginsWith(aboutSpec, "about:addons"_ns) ||
1354 StringBeginsWith(aboutSpec, "about:newtab"_ns) ||
1355 StringBeginsWith(aboutSpec, "about:debugging"_ns) ||
1356 StringBeginsWith(aboutSpec, "about:ion"_ns) ||
1357 StringBeginsWith(aboutSpec, "about:compat"_ns) ||
1358 StringBeginsWith(aboutSpec, "about:logins"_ns) ||
1359 StringBeginsWith(aboutSpec, "about:home"_ns) ||
1360 StringBeginsWith(aboutSpec, "about:welcome"_ns) ||
1361 StringBeginsWith(aboutSpec, "about:devtools"_ns) ||
1362 StringBeginsWith(aboutSpec, "about:pocket-saved"_ns) ||
1363 StringBeginsWith(aboutSpec, "about:pocket-home"_ns),
1364 "about: page must not contain a CSP including a web scheme");
1366 if (aDocument->IsExtensionPage()) {
1367 // Extensions have two CSP policies applied where the baseline CSP
1368 // includes 'unsafe-eval' and 'unsafe-inline', hence we have to skip
1369 // the 'unsafe-eval' and 'unsafe-inline' assertions for extension
1370 // pages.
1371 return;
1374 MOZ_ASSERT(!foundUnsafeEval,
1375 "about: page must not contain a CSP including 'unsafe-eval'");
1377 static nsLiteralCString sLegacyUnsafeInlineAllowList[] = {
1378 // Bug 1579160: Remove 'unsafe-inline' from style-src within
1379 // about:preferences
1380 "about:preferences"_ns,
1381 "about:settings"_ns,
1382 // Bug 1571346: Remove 'unsafe-inline' from style-src within about:addons
1383 "about:addons"_ns,
1384 // Bug 1584485: Remove 'unsafe-inline' from style-src within:
1385 // * about:newtab
1386 // * about:welcome
1387 // * about:home
1388 "about:newtab"_ns,
1389 "about:welcome"_ns,
1390 "about:home"_ns,
1393 for (const nsLiteralCString& aUnsafeInlineEntry :
1394 sLegacyUnsafeInlineAllowList) {
1395 // please note that we perform a substring match here on purpose,
1396 // so we don't have to deal and parse out all the query arguments
1397 // the various about pages rely on.
1398 if (StringBeginsWith(aboutSpec, aUnsafeInlineEntry)) {
1399 return;
1403 MOZ_ASSERT(!foundUnsafeInline,
1404 "about: page must not contain a CSP including 'unsafe-inline'");
1406 #endif
1408 /* static */
1409 bool nsContentSecurityUtils::ValidateScriptFilename(JSContext* cx,
1410 const char* aFilename) {
1411 // If the pref is permissive, allow everything
1412 if (StaticPrefs::security_allow_parent_unrestricted_js_loads()) {
1413 return true;
1416 // If we're not in the parent process allow everything (presently)
1417 if (!XRE_IsE10sParentProcess()) {
1418 return true;
1421 // If we have allowed eval (because of a user configuration or more
1422 // likely a test has requested it), and the script is an eval, allow it.
1423 nsDependentCString filename(aFilename);
1424 if (StaticPrefs::security_allow_eval_with_system_principal() ||
1425 StaticPrefs::security_allow_eval_in_parent_process()) {
1426 if (StringEndsWith(filename, "> eval"_ns)) {
1427 return true;
1431 DetectJsHacks();
1433 if (MOZ_UNLIKELY(!sJSHacksChecked)) {
1434 MOZ_LOG(
1435 sCSMLog, LogLevel::Debug,
1436 ("Allowing a javascript load of %s because "
1437 "we have not yet been able to determine if JS hacks may be present",
1438 aFilename));
1439 return true;
1442 if (MOZ_UNLIKELY(sJSHacksPresent)) {
1443 MOZ_LOG(sCSMLog, LogLevel::Debug,
1444 ("Allowing a javascript load of %s because "
1445 "some JS hacks may be present",
1446 aFilename));
1447 return true;
1450 if (XRE_IsE10sParentProcess() &&
1451 !StaticPrefs::extensions_webextensions_remote()) {
1452 MOZ_LOG(sCSMLog, LogLevel::Debug,
1453 ("Allowing a javascript load of %s because the web extension "
1454 "process is disabled.",
1455 aFilename));
1456 return true;
1459 if (StringBeginsWith(filename, "chrome://"_ns)) {
1460 // If it's a chrome:// url, allow it
1461 return true;
1463 if (StringBeginsWith(filename, "resource://"_ns)) {
1464 // If it's a resource:// url, allow it
1465 return true;
1467 if (StringBeginsWith(filename, "file://"_ns)) {
1468 // We will temporarily allow all file:// URIs through for now
1469 return true;
1471 if (StringBeginsWith(filename, "jar:file://"_ns)) {
1472 // We will temporarily allow all jar URIs through for now
1473 return true;
1475 if (filename.Equals("about:sync-log"_ns)) {
1476 // about:sync-log runs in the parent process and displays a directory
1477 // listing. The listing has inline javascript that executes on load.
1478 return true;
1481 if (StringBeginsWith(filename, "moz-extension://"_ns)) {
1482 nsCOMPtr<nsIURI> uri;
1483 nsresult rv = NS_NewURI(getter_AddRefs(uri), aFilename);
1484 if (!NS_FAILED(rv) && NS_IsMainThread()) {
1485 mozilla::extensions::URLInfo url(uri);
1486 auto* policy =
1487 ExtensionPolicyService::GetSingleton().GetByHost(url.Host());
1489 if (policy && policy->IsPrivileged()) {
1490 MOZ_LOG(sCSMLog, LogLevel::Debug,
1491 ("Allowing a javascript load of %s because the web extension "
1492 "it is associated with is privileged.",
1493 aFilename));
1494 return true;
1497 } else if (!NS_IsMainThread()) {
1498 WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
1499 if (workerPrivate && workerPrivate->IsPrivilegedAddonGlobal()) {
1500 MOZ_LOG(sCSMLog, LogLevel::Debug,
1501 ("Allowing a javascript load of %s because the web extension "
1502 "it is associated with is privileged.",
1503 aFilename));
1504 return true;
1508 auto kAllowedFilenamesPrefix = {
1509 // Until 371900 is fixed, we need to do something about about:downloads
1510 // and this is the most reasonable. See 1727770
1511 "about:downloads"_ns,
1512 // We think this is the same problem as about:downloads
1513 "about:preferences"_ns, "about:settings"_ns,
1514 // Browser console will give a filename of 'debugger' See 1763943
1515 // Sometimes it's 'debugger eager eval code', other times just 'debugger
1516 // eval code'
1517 "debugger"_ns};
1519 for (auto allowedFilenamePrefix : kAllowedFilenamesPrefix) {
1520 if (StringBeginsWith(filename, allowedFilenamePrefix)) {
1521 return true;
1525 // Log to MOZ_LOG
1526 MOZ_LOG(sCSMLog, LogLevel::Error,
1527 ("ValidateScriptFilename Failed: %s\n", aFilename));
1529 FilenameTypeAndDetails fileNameTypeAndDetails =
1530 FilenameToFilenameType(filename, true);
1532 glean::security::JavascriptLoadParentProcessExtra extra = {
1533 .fileinfo = fileNameTypeAndDetails.second,
1534 .value = Some(fileNameTypeAndDetails.first),
1536 glean::security::javascript_load_parent_process.Record(Some(extra));
1538 #if defined(DEBUG) || defined(FUZZING)
1539 auto crashString = nsContentSecurityUtils::SmartFormatCrashString(
1540 aFilename,
1541 fileNameTypeAndDetails.second.isSome()
1542 ? fileNameTypeAndDetails.second.value().get()
1543 : "(None)",
1544 "Blocking a script load %s from file %s");
1545 MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get());
1546 #elif defined(EARLY_BETA_OR_EARLIER)
1547 // Cause a crash (if we've never crashed before and we can ensure we won't do
1548 // it again.)
1549 // The details in the second arg, passed to UNSAFE_PRINTF, are also included
1550 // in Event Telemetry and have received data review.
1551 if (fileNameTypeAndDetails.second.isSome()) {
1552 PossiblyCrash("js_load_1", aFilename,
1553 fileNameTypeAndDetails.second.value());
1554 } else {
1555 PossiblyCrash("js_load_1", aFilename, "(None)"_ns);
1557 #endif
1559 // Presently we are only enforcing restrictions for the script filename
1560 // on Nightly. On all channels we are reporting Telemetry. In the future we
1561 // will assert in debug builds and return false to prevent execution in
1562 // non-debug builds.
1563 #ifdef NIGHTLY_BUILD
1564 return false;
1565 #else
1566 return true;
1567 #endif
1570 /* static */
1571 void nsContentSecurityUtils::LogMessageToConsole(nsIHttpChannel* aChannel,
1572 const char* aMsg) {
1573 nsCOMPtr<nsIURI> uri;
1574 nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
1575 if (NS_FAILED(rv)) {
1576 return;
1579 uint64_t windowID = 0;
1580 rv = aChannel->GetTopLevelContentWindowId(&windowID);
1581 if (NS_WARN_IF(NS_FAILED(rv))) {
1582 return;
1584 if (!windowID) {
1585 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1586 loadInfo->GetInnerWindowID(&windowID);
1589 nsAutoString localizedMsg;
1590 nsAutoCString spec;
1591 uri->GetSpec(spec);
1592 AutoTArray<nsString, 1> params = {NS_ConvertUTF8toUTF16(spec)};
1593 rv = nsContentUtils::FormatLocalizedString(
1594 nsContentUtils::eSECURITY_PROPERTIES, aMsg, params, localizedMsg);
1595 if (NS_WARN_IF(NS_FAILED(rv))) {
1596 return;
1599 nsContentUtils::ReportToConsoleByWindowID(
1600 localizedMsg, nsIScriptError::warningFlag, "Security"_ns, windowID,
1601 SourceLocation{uri.get()});
1604 /* static */
1605 long nsContentSecurityUtils::ClassifyDownload(
1606 nsIChannel* aChannel, const nsAutoCString& aMimeTypeGuess) {
1607 MOZ_ASSERT(aChannel, "IsDownloadAllowed without channel?");
1609 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1611 nsCOMPtr<nsIURI> contentLocation;
1612 aChannel->GetURI(getter_AddRefs(contentLocation));
1614 nsCOMPtr<nsIPrincipal> loadingPrincipal = loadInfo->GetLoadingPrincipal();
1615 if (!loadingPrincipal) {
1616 loadingPrincipal = loadInfo->TriggeringPrincipal();
1618 // Creating a fake Loadinfo that is just used for the MCB check.
1619 nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new mozilla::net::LoadInfo(
1620 loadingPrincipal, loadInfo->TriggeringPrincipal(), nullptr,
1621 nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
1622 nsIContentPolicy::TYPE_FETCH);
1623 // Disable HTTPS-Only checks for that loadinfo. This is required because
1624 // otherwise nsMixedContentBlocker::ShouldLoad would assume that the request
1625 // is safe, because HTTPS-Only is handling it.
1626 secCheckLoadInfo->SetHttpsOnlyStatus(nsILoadInfo::HTTPS_ONLY_EXEMPT);
1628 int16_t decission = nsIContentPolicy::ACCEPT;
1629 nsMixedContentBlocker::ShouldLoad(false, // aHadInsecureImageRedirect
1630 contentLocation, // aContentLocation,
1631 secCheckLoadInfo, // aLoadinfo
1632 false, // aReportError
1633 &decission // aDecision
1636 if (StaticPrefs::dom_block_download_insecure() &&
1637 decission != nsIContentPolicy::ACCEPT) {
1638 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
1639 if (httpChannel) {
1640 LogMessageToConsole(httpChannel, "MixedContentBlockedDownload");
1642 return nsITransfer::DOWNLOAD_POTENTIALLY_UNSAFE;
1645 if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) {
1646 return nsITransfer::DOWNLOAD_ACCEPTABLE;
1649 uint32_t triggeringFlags = loadInfo->GetTriggeringSandboxFlags();
1650 uint32_t currentflags = loadInfo->GetSandboxFlags();
1652 if ((triggeringFlags & SANDBOXED_ALLOW_DOWNLOADS) ||
1653 (currentflags & SANDBOXED_ALLOW_DOWNLOADS)) {
1654 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
1655 if (httpChannel) {
1656 LogMessageToConsole(httpChannel, "IframeSandboxBlockedDownload");
1658 return nsITransfer::DOWNLOAD_FORBIDDEN;
1660 return nsITransfer::DOWNLOAD_ACCEPTABLE;