зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1570681 - Move Eval testing logic from nsContentSecurityManager to nsContentSecurityUtils r=ckerschb
Differential Revision: https://phabricator.services.mozilla.com/D45484 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
1700229ac1
Коммит
9621f537b0
|
@ -30,7 +30,7 @@
|
|||
#include "nsString.h"
|
||||
#include "nsCRT.h"
|
||||
#include "nsCRTGlue.h"
|
||||
#include "nsContentSecurityManager.h"
|
||||
#include "nsContentSecurityUtils.h"
|
||||
#include "nsDocShell.h"
|
||||
#include "nsError.h"
|
||||
#include "nsGlobalWindowInner.h"
|
||||
|
@ -443,8 +443,8 @@ bool nsScriptSecurityManager::ContentSecurityPolicyPermitsJSAction(
|
|||
}
|
||||
|
||||
#if !defined(ANDROID) && (defined(NIGHTLY_BUILD) || defined(DEBUG))
|
||||
nsContentSecurityManager::AssertEvalNotRestricted(cx, subjectPrincipal,
|
||||
scriptSample);
|
||||
nsContentSecurityUtils::AssertEvalNotRestricted(cx, subjectPrincipal,
|
||||
scriptSample);
|
||||
#endif
|
||||
|
||||
if (NS_FAILED(rv)) {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
#include "mozilla/ErrorResult.h"
|
||||
#include "nsGlobalWindowInner.h"
|
||||
#include "mozilla/dom/Document.h"
|
||||
#include "nsContentSecurityManager.h"
|
||||
#include "nsContentSecurityUtils.h"
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsJSUtils.h"
|
||||
|
@ -33,8 +33,8 @@ nsresult CheckInternal(nsIContentSecurityPolicy* aCSP,
|
|||
|
||||
#if !defined(ANDROID) && (defined(NIGHTLY_BUILD) || defined(DEBUG))
|
||||
JSContext* cx = nsContentUtils::GetCurrentJSContext();
|
||||
nsContentSecurityManager::AssertEvalNotRestricted(cx, aSubjectPrincipal,
|
||||
aExpression);
|
||||
nsContentSecurityUtils::AssertEvalNotRestricted(cx, aSubjectPrincipal,
|
||||
aExpression);
|
||||
#endif
|
||||
|
||||
// The value is set at any "return", but better to have a default value here.
|
||||
|
|
|
@ -169,358 +169,6 @@ bool nsContentSecurityManager::AllowInsecureRedirectToDataURI(
|
|||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Performs a Regular Expression match, optionally returning the results.
|
||||
*
|
||||
* @param aPattern The regex pattern
|
||||
* @param aString The string to compare against
|
||||
* @param aOnlyMatch Whether we want match results or only a true/false for
|
||||
* the match
|
||||
* @param aMatchResult Out param for whether or not the pattern matched
|
||||
* @param aRegexResults Out param for the matches of the regex, if requested
|
||||
* @returns nsresult indicating correct function operation or error
|
||||
*/
|
||||
nsresult RegexEval(const nsAString& aPattern, const nsAString& aString,
|
||||
bool aOnlyMatch, bool& aMatchResult,
|
||||
nsTArray<nsString>* aRegexResults = nullptr) {
|
||||
aMatchResult = false;
|
||||
|
||||
AutoJSAPI jsapi;
|
||||
jsapi.Init();
|
||||
|
||||
JSContext* cx = jsapi.cx();
|
||||
AutoDisableJSInterruptCallback disabler(cx);
|
||||
|
||||
JSAutoRealm ar(cx, xpc::UnprivilegedJunkScope());
|
||||
|
||||
JS::RootedObject regexp(
|
||||
cx, JS::NewUCRegExpObject(cx, aPattern.BeginReading(), aPattern.Length(),
|
||||
JS::RegExpFlag::Unicode));
|
||||
if (!regexp) {
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
|
||||
JS::RootedValue regexResult(cx, JS::NullValue());
|
||||
|
||||
size_t index = 0;
|
||||
if (!JS::ExecuteRegExpNoStatics(cx, regexp, aString.BeginReading(),
|
||||
aString.Length(), &index, aOnlyMatch,
|
||||
®exResult)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
if (regexResult.isNull()) {
|
||||
// On no match, ExecuteRegExpNoStatics returns Null
|
||||
return NS_OK;
|
||||
}
|
||||
if (aOnlyMatch) {
|
||||
// On match, with aOnlyMatch = true, ExecuteRegExpNoStatics returns boolean
|
||||
// true.
|
||||
MOZ_ASSERT(regexResult.isBoolean() && regexResult.toBoolean());
|
||||
aMatchResult = true;
|
||||
return NS_OK;
|
||||
}
|
||||
if (aRegexResults == nullptr) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
// Now we know we have a result, and we need to extract it so we can read it.
|
||||
uint32_t length;
|
||||
JS::RootedObject regexResultObj(cx, ®exResult.toObject());
|
||||
if (!JS_GetArrayLength(cx, regexResultObj, &length)) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
MOZ_LOG(sCSMLog, LogLevel::Verbose, ("Regex Matched %i strings", length));
|
||||
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
JS::RootedValue element(cx);
|
||||
if (!JS_GetElement(cx, regexResultObj, i, &element)) {
|
||||
return NS_ERROR_NO_CONTENT;
|
||||
}
|
||||
|
||||
nsAutoJSString value;
|
||||
if (!value.init(cx, element)) {
|
||||
return NS_ERROR_NO_CONTENT;
|
||||
}
|
||||
|
||||
MOZ_LOG(sCSMLog, LogLevel::Verbose,
|
||||
("Regex Matching: %i: %s", i, NS_ConvertUTF16toUTF8(value).get()));
|
||||
aRegexResults->AppendElement(value);
|
||||
}
|
||||
|
||||
aMatchResult = true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Telemetry Events extra data only supports 80 characters, so we optimize the
|
||||
* filename to be smaller and collect more data.
|
||||
*/
|
||||
nsString OptimizeFileName(const nsAString& aFileName) {
|
||||
nsString optimizedName(aFileName);
|
||||
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Verbose,
|
||||
("Optimizing FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get()));
|
||||
|
||||
optimizedName.ReplaceSubstring(NS_LITERAL_STRING(".xpi!"),
|
||||
NS_LITERAL_STRING("!"));
|
||||
optimizedName.ReplaceSubstring(NS_LITERAL_STRING("shield.mozilla.org!"),
|
||||
NS_LITERAL_STRING("s!"));
|
||||
optimizedName.ReplaceSubstring(NS_LITERAL_STRING("mozilla.org!"),
|
||||
NS_LITERAL_STRING("m!"));
|
||||
if (optimizedName.Length() > 80) {
|
||||
optimizedName.Truncate(80);
|
||||
}
|
||||
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Verbose,
|
||||
("Optimized FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get()));
|
||||
return optimizedName;
|
||||
}
|
||||
|
||||
/*
|
||||
* FilenameToEvalType takes a fileName and returns a Pair of strings.
|
||||
* The First entry is a string indicating the type of fileName
|
||||
* The Second entry is a Maybe<string> that can contain additional details to
|
||||
* report.
|
||||
*
|
||||
* The reason we use strings (instead of an int/enum) is because the Telemetry
|
||||
* Events API only accepts strings.
|
||||
*
|
||||
* Function is a static member of the class to enable gtests.
|
||||
*/
|
||||
|
||||
/* static */
|
||||
FilenameType nsContentSecurityManager::FilenameToEvalType(
|
||||
const nsString& fileName) {
|
||||
// These are strings because the Telemetry Events API only accepts strings
|
||||
static NS_NAMED_LITERAL_CSTRING(kChromeURI, "chromeuri");
|
||||
static NS_NAMED_LITERAL_CSTRING(kResourceURI, "resourceuri");
|
||||
static NS_NAMED_LITERAL_CSTRING(kSingleString, "singlestring");
|
||||
static NS_NAMED_LITERAL_CSTRING(kMozillaExtension, "mozillaextension");
|
||||
static NS_NAMED_LITERAL_CSTRING(kOtherExtension, "otherextension");
|
||||
static NS_NAMED_LITERAL_CSTRING(kSuspectedUserChromeJS,
|
||||
"suspectedUserChromeJS");
|
||||
static NS_NAMED_LITERAL_CSTRING(kOther, "other");
|
||||
static NS_NAMED_LITERAL_CSTRING(kRegexFailure, "regexfailure");
|
||||
|
||||
static NS_NAMED_LITERAL_STRING(kUCJSRegex, "(.+).uc.js\\?*[0-9]*$");
|
||||
static NS_NAMED_LITERAL_STRING(kExtensionRegex, "extensions/(.+)@(.+)!(.+)$");
|
||||
static NS_NAMED_LITERAL_STRING(kSingleFileRegex, "^[a-zA-Z0-9.?]+$");
|
||||
|
||||
// resource:// and chrome://
|
||||
if (StringBeginsWith(fileName, NS_LITERAL_STRING("chrome://"))) {
|
||||
return FilenameType(kChromeURI, Some(fileName));
|
||||
}
|
||||
if (StringBeginsWith(fileName, NS_LITERAL_STRING("resource://"))) {
|
||||
return FilenameType(kResourceURI, Some(fileName));
|
||||
}
|
||||
|
||||
// Extension
|
||||
bool regexMatch;
|
||||
nsTArray<nsString> regexResults;
|
||||
nsresult rv = RegexEval(kExtensionRegex, fileName, /* aOnlyMatch = */ false,
|
||||
regexMatch, ®exResults);
|
||||
if (NS_FAILED(rv)) {
|
||||
return FilenameType(kRegexFailure, Nothing());
|
||||
}
|
||||
if (regexMatch) {
|
||||
nsCString type =
|
||||
StringEndsWith(regexResults[2], NS_LITERAL_STRING("mozilla.org.xpi"))
|
||||
? kMozillaExtension
|
||||
: kOtherExtension;
|
||||
auto& extensionNameAndPath =
|
||||
Substring(regexResults[0], ArrayLength("extensions/") - 1);
|
||||
return FilenameType(type, Some(OptimizeFileName(extensionNameAndPath)));
|
||||
}
|
||||
|
||||
// Single File
|
||||
rv = RegexEval(kSingleFileRegex, fileName, /* aOnlyMatch = */ true,
|
||||
regexMatch);
|
||||
if (NS_FAILED(rv)) {
|
||||
return FilenameType(kRegexFailure, Nothing());
|
||||
}
|
||||
if (regexMatch) {
|
||||
return FilenameType(kSingleString, Some(fileName));
|
||||
}
|
||||
|
||||
// Suspected userChromeJS script
|
||||
rv = RegexEval(kUCJSRegex, fileName, /* aOnlyMatch = */ true, regexMatch);
|
||||
if (NS_FAILED(rv)) {
|
||||
return FilenameType(kRegexFailure, Nothing());
|
||||
}
|
||||
if (regexMatch) {
|
||||
return FilenameType(kSuspectedUserChromeJS, Nothing());
|
||||
}
|
||||
|
||||
return FilenameType(kOther, Nothing());
|
||||
}
|
||||
|
||||
/* static */
|
||||
void nsContentSecurityManager::AssertEvalNotRestricted(
|
||||
JSContext* cx, nsIPrincipal* aSubjectPrincipal, const nsAString& aScript) {
|
||||
// This allowlist contains files that are permanently allowed to use
|
||||
// eval()-like functions. It is supposed to be restricted to files that are
|
||||
// exclusively used in testing contexts.
|
||||
static nsLiteralCString evalAllowlist[] = {
|
||||
// Test-only third-party library
|
||||
NS_LITERAL_CSTRING("resource://testing-common/sinon-7.2.7.js"),
|
||||
// Test-only third-party library
|
||||
NS_LITERAL_CSTRING("resource://testing-common/ajv-4.1.1.js"),
|
||||
// Test-only utility
|
||||
NS_LITERAL_CSTRING("resource://testing-common/content-task.js"),
|
||||
|
||||
// The Browser Toolbox/Console
|
||||
NS_LITERAL_CSTRING("debugger"),
|
||||
};
|
||||
|
||||
// We also permit two specific idioms in eval()-like contexts. We'd like to
|
||||
// elminate these too; but there are in-the-wild Mozilla privileged extensions
|
||||
// that use them.
|
||||
static NS_NAMED_LITERAL_STRING(sAllowedEval1, "this");
|
||||
static NS_NAMED_LITERAL_STRING(sAllowedEval2,
|
||||
"function anonymous(\n) {\nreturn this\n}");
|
||||
|
||||
bool systemPrincipal = aSubjectPrincipal->IsSystemPrincipal();
|
||||
if (systemPrincipal &&
|
||||
StaticPrefs::security_allow_eval_with_system_principal()) {
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() %s because allowing pref is "
|
||||
"enabled",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process")));
|
||||
return;
|
||||
}
|
||||
|
||||
if (XRE_IsE10sParentProcess() &&
|
||||
StaticPrefs::security_allow_eval_in_parent_process()) {
|
||||
MOZ_LOG(sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() in parent process because allowing pref is "
|
||||
"enabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!systemPrincipal && !XRE_IsE10sParentProcess()) {
|
||||
// Usage of eval we are unconcerned with.
|
||||
return;
|
||||
}
|
||||
|
||||
// This preference is a file used for autoconfiguration of Firefox
|
||||
// by administrators. It has also been (ab)used by the userChromeJS
|
||||
// project to run legacy-style 'extensions', some of which use eval,
|
||||
// all of which run in the System Principal context.
|
||||
nsAutoString jsConfigPref;
|
||||
Preferences::GetString("general.config.filename", jsConfigPref);
|
||||
if (!jsConfigPref.IsEmpty()) {
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() %s because of "
|
||||
"general.config.filename",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process")));
|
||||
return;
|
||||
}
|
||||
|
||||
// This preference is better known as userchrome.css which allows
|
||||
// customization of the Firefox UI. Believe it or not, you can also
|
||||
// use XBL bindings to get it to run Javascript in the same manner
|
||||
// as userChromeJS above, so even though 99.9% of people using
|
||||
// userchrome.css aren't doing that, we're still going to need to
|
||||
// disable the eval() assertion for them.
|
||||
if (Preferences::GetBool(
|
||||
"toolkit.legacyUserProfileCustomizations.stylesheets")) {
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() %s because of "
|
||||
"toolkit.legacyUserProfileCustomizations.stylesheets",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process")));
|
||||
return;
|
||||
}
|
||||
|
||||
// We permit these two common idioms to get access to the global JS object
|
||||
if (!aScript.IsEmpty() &&
|
||||
(aScript == sAllowedEval1 || aScript == sAllowedEval2)) {
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() %s because a key string is "
|
||||
"provided",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process")));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the allowlist for the provided filename. getFilename is a helper
|
||||
// function
|
||||
auto getFilename = [](JSContext* cx) -> const nsCString {
|
||||
JS::AutoFilename scriptFilename;
|
||||
if (JS::DescribeScriptedCaller(cx, &scriptFilename)) {
|
||||
nsDependentCSubstring fileName_(scriptFilename.get(),
|
||||
strlen(scriptFilename.get()));
|
||||
ToLowerCase(fileName_);
|
||||
// Extract file name alone if scriptFilename contains line number
|
||||
// separated by multiple space delimiters in few cases.
|
||||
int32_t fileNameIndex = fileName_.FindChar(' ');
|
||||
if (fileNameIndex != -1) {
|
||||
fileName_.SetLength(fileNameIndex);
|
||||
}
|
||||
|
||||
nsAutoCString fileName(fileName_);
|
||||
return std::move(fileName);
|
||||
}
|
||||
return NS_LITERAL_CSTRING("unknown-file");
|
||||
};
|
||||
|
||||
nsCString fileName = getFilename(cx);
|
||||
for (const nsLiteralCString& allowlistEntry : evalAllowlist) {
|
||||
if (fileName.Equals(allowlistEntry)) {
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() %s because the containing "
|
||||
"file is in the allowlist",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send Telemetry
|
||||
Telemetry::EventID eventType =
|
||||
systemPrincipal ? Telemetry::EventID::Security_Evalusage_Systemcontext
|
||||
: Telemetry::EventID::Security_Evalusage_Parentprocess;
|
||||
|
||||
FilenameType fileNameType =
|
||||
FilenameToEvalType(NS_ConvertUTF8toUTF16(fileName));
|
||||
mozilla::Maybe<nsTArray<EventExtraEntry>> extra;
|
||||
if (fileNameType.second().isSome()) {
|
||||
extra = Some<nsTArray<EventExtraEntry>>({EventExtraEntry{
|
||||
NS_LITERAL_CSTRING("fileinfo"),
|
||||
NS_ConvertUTF16toUTF8(fileNameType.second().value())}});
|
||||
} else {
|
||||
extra = Nothing();
|
||||
}
|
||||
if (!sTelemetryEventEnabled.exchange(true)) {
|
||||
sTelemetryEventEnabled = true;
|
||||
Telemetry::SetEventRecordingEnabled(NS_LITERAL_CSTRING("security"), true);
|
||||
}
|
||||
Telemetry::RecordEvent(eventType, mozilla::Some(fileNameType.first()), extra);
|
||||
|
||||
// Crash or Log
|
||||
#ifdef DEBUG
|
||||
MOZ_CRASH_UNSAFE_PRINTF(
|
||||
"Blocking eval() %s from file %s and script provided "
|
||||
"%s",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process"),
|
||||
fileName.get(), NS_ConvertUTF16toUTF8(aScript).get());
|
||||
#else
|
||||
MOZ_LOG(sCSMLog, LogLevel::Warning,
|
||||
("Blocking eval() %s from file %s and script "
|
||||
"provided %s",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process"),
|
||||
fileName.get(), NS_ConvertUTF16toUTF8(aScript).get()));
|
||||
#endif
|
||||
|
||||
// In the future, we will change this function to return false and abort JS
|
||||
// execution without crashing the process. For now, just collect data.
|
||||
}
|
||||
|
||||
/* static */
|
||||
nsresult nsContentSecurityManager::CheckFTPSubresourceLoad(
|
||||
nsIChannel* aChannel) {
|
||||
|
|
|
@ -23,8 +23,6 @@ class nsIStreamListener;
|
|||
} \
|
||||
}
|
||||
|
||||
typedef mozilla::Pair<nsCString, mozilla::Maybe<nsString>> FilenameType;
|
||||
|
||||
class nsContentSecurityManager : public nsIContentSecurityManager,
|
||||
public nsIChannelEventSink {
|
||||
public:
|
||||
|
@ -40,11 +38,6 @@ class nsContentSecurityManager : public nsIContentSecurityManager,
|
|||
static bool AllowTopLevelNavigationToDataURI(nsIChannel* aChannel);
|
||||
static bool AllowInsecureRedirectToDataURI(nsIChannel* aNewChannel);
|
||||
|
||||
static FilenameType FilenameToEvalType(const nsString& fileName);
|
||||
static void AssertEvalNotRestricted(JSContext* cx,
|
||||
nsIPrincipal* aSubjectPrincipal,
|
||||
const nsAString& aScript);
|
||||
|
||||
private:
|
||||
static nsresult CheckChannel(nsIChannel* aChannel);
|
||||
static nsresult CheckFTPSubresourceLoad(nsIChannel* aChannel);
|
||||
|
|
|
@ -13,6 +13,358 @@
|
|||
|
||||
#include "mozilla/dom/Document.h"
|
||||
|
||||
/*
|
||||
* Performs a Regular Expression match, optionally returning the results.
|
||||
*
|
||||
* @param aPattern The regex pattern
|
||||
* @param aString The string to compare against
|
||||
* @param aOnlyMatch Whether we want match results or only a true/false for
|
||||
* the match
|
||||
* @param aMatchResult Out param for whether or not the pattern matched
|
||||
* @param aRegexResults Out param for the matches of the regex, if requested
|
||||
* @returns nsresult indicating correct function operation or error
|
||||
*/
|
||||
nsresult RegexEval(const nsAString& aPattern, const nsAString& aString,
|
||||
bool aOnlyMatch, bool& aMatchResult,
|
||||
nsTArray<nsString>* aRegexResults = nullptr) {
|
||||
aMatchResult = false;
|
||||
|
||||
AutoJSAPI jsapi;
|
||||
jsapi.Init();
|
||||
|
||||
JSContext* cx = jsapi.cx();
|
||||
AutoDisableJSInterruptCallback disabler(cx);
|
||||
|
||||
JSAutoRealm ar(cx, xpc::UnprivilegedJunkScope());
|
||||
|
||||
JS::RootedObject regexp(
|
||||
cx, JS::NewUCRegExpObject(cx, aPattern.BeginReading(), aPattern.Length(),
|
||||
JS::RegExpFlag::Unicode));
|
||||
if (!regexp) {
|
||||
return NS_ERROR_ILLEGAL_VALUE;
|
||||
}
|
||||
|
||||
JS::RootedValue regexResult(cx, JS::NullValue());
|
||||
|
||||
size_t index = 0;
|
||||
if (!JS::ExecuteRegExpNoStatics(cx, regexp, aString.BeginReading(),
|
||||
aString.Length(), &index, aOnlyMatch,
|
||||
®exResult)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
if (regexResult.isNull()) {
|
||||
// On no match, ExecuteRegExpNoStatics returns Null
|
||||
return NS_OK;
|
||||
}
|
||||
if (aOnlyMatch) {
|
||||
// On match, with aOnlyMatch = true, ExecuteRegExpNoStatics returns boolean
|
||||
// true.
|
||||
MOZ_ASSERT(regexResult.isBoolean() && regexResult.toBoolean());
|
||||
aMatchResult = true;
|
||||
return NS_OK;
|
||||
}
|
||||
if (aRegexResults == nullptr) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
|
||||
// Now we know we have a result, and we need to extract it so we can read it.
|
||||
uint32_t length;
|
||||
JS::RootedObject regexResultObj(cx, ®exResult.toObject());
|
||||
if (!JS_GetArrayLength(cx, regexResultObj, &length)) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
MOZ_LOG(sCSMLog, LogLevel::Verbose, ("Regex Matched %i strings", length));
|
||||
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
JS::RootedValue element(cx);
|
||||
if (!JS_GetElement(cx, regexResultObj, i, &element)) {
|
||||
return NS_ERROR_NO_CONTENT;
|
||||
}
|
||||
|
||||
nsAutoJSString value;
|
||||
if (!value.init(cx, element)) {
|
||||
return NS_ERROR_NO_CONTENT;
|
||||
}
|
||||
|
||||
MOZ_LOG(sCSMLog, LogLevel::Verbose,
|
||||
("Regex Matching: %i: %s", i, NS_ConvertUTF16toUTF8(value).get()));
|
||||
aRegexResults->AppendElement(value);
|
||||
}
|
||||
|
||||
aMatchResult = true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Telemetry Events extra data only supports 80 characters, so we optimize the
|
||||
* filename to be smaller and collect more data.
|
||||
*/
|
||||
nsString OptimizeFileName(const nsAString& aFileName) {
|
||||
nsString optimizedName(aFileName);
|
||||
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Verbose,
|
||||
("Optimizing FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get()));
|
||||
|
||||
optimizedName.ReplaceSubstring(NS_LITERAL_STRING(".xpi!"),
|
||||
NS_LITERAL_STRING("!"));
|
||||
optimizedName.ReplaceSubstring(NS_LITERAL_STRING("shield.mozilla.org!"),
|
||||
NS_LITERAL_STRING("s!"));
|
||||
optimizedName.ReplaceSubstring(NS_LITERAL_STRING("mozilla.org!"),
|
||||
NS_LITERAL_STRING("m!"));
|
||||
if (optimizedName.Length() > 80) {
|
||||
optimizedName.Truncate(80);
|
||||
}
|
||||
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Verbose,
|
||||
("Optimized FileName: %s", NS_ConvertUTF16toUTF8(optimizedName).get()));
|
||||
return optimizedName;
|
||||
}
|
||||
|
||||
/*
|
||||
* FilenameToEvalType takes a fileName and returns a Pair of strings.
|
||||
* The First entry is a string indicating the type of fileName
|
||||
* The Second entry is a Maybe<string> that can contain additional details to
|
||||
* report.
|
||||
*
|
||||
* The reason we use strings (instead of an int/enum) is because the Telemetry
|
||||
* Events API only accepts strings.
|
||||
*
|
||||
* Function is a static member of the class to enable gtests.
|
||||
*/
|
||||
|
||||
/* static */
|
||||
FilenameType nsContentSecurityUtils::FilenameToEvalType(
|
||||
const nsString& fileName) {
|
||||
// These are strings because the Telemetry Events API only accepts strings
|
||||
static NS_NAMED_LITERAL_CSTRING(kChromeURI, "chromeuri");
|
||||
static NS_NAMED_LITERAL_CSTRING(kResourceURI, "resourceuri");
|
||||
static NS_NAMED_LITERAL_CSTRING(kSingleString, "singlestring");
|
||||
static NS_NAMED_LITERAL_CSTRING(kMozillaExtension, "mozillaextension");
|
||||
static NS_NAMED_LITERAL_CSTRING(kOtherExtension, "otherextension");
|
||||
static NS_NAMED_LITERAL_CSTRING(kSuspectedUserChromeJS,
|
||||
"suspectedUserChromeJS");
|
||||
static NS_NAMED_LITERAL_CSTRING(kOther, "other");
|
||||
static NS_NAMED_LITERAL_CSTRING(kRegexFailure, "regexfailure");
|
||||
|
||||
static NS_NAMED_LITERAL_STRING(kUCJSRegex, "(.+).uc.js\\?*[0-9]*$");
|
||||
static NS_NAMED_LITERAL_STRING(kExtensionRegex, "extensions/(.+)@(.+)!(.+)$");
|
||||
static NS_NAMED_LITERAL_STRING(kSingleFileRegex, "^[a-zA-Z0-9.?]+$");
|
||||
|
||||
// resource:// and chrome://
|
||||
if (StringBeginsWith(fileName, NS_LITERAL_STRING("chrome://"))) {
|
||||
return FilenameType(kChromeURI, Some(fileName));
|
||||
}
|
||||
if (StringBeginsWith(fileName, NS_LITERAL_STRING("resource://"))) {
|
||||
return FilenameType(kResourceURI, Some(fileName));
|
||||
}
|
||||
|
||||
// Extension
|
||||
bool regexMatch;
|
||||
nsTArray<nsString> regexResults;
|
||||
nsresult rv = RegexEval(kExtensionRegex, fileName, /* aOnlyMatch = */ false,
|
||||
regexMatch, ®exResults);
|
||||
if (NS_FAILED(rv)) {
|
||||
return FilenameType(kRegexFailure, Nothing());
|
||||
}
|
||||
if (regexMatch) {
|
||||
nsCString type =
|
||||
StringEndsWith(regexResults[2], NS_LITERAL_STRING("mozilla.org.xpi"))
|
||||
? kMozillaExtension
|
||||
: kOtherExtension;
|
||||
auto& extensionNameAndPath =
|
||||
Substring(regexResults[0], ArrayLength("extensions/") - 1);
|
||||
return FilenameType(type, Some(OptimizeFileName(extensionNameAndPath)));
|
||||
}
|
||||
|
||||
// Single File
|
||||
rv = RegexEval(kSingleFileRegex, fileName, /* aOnlyMatch = */ true,
|
||||
regexMatch);
|
||||
if (NS_FAILED(rv)) {
|
||||
return FilenameType(kRegexFailure, Nothing());
|
||||
}
|
||||
if (regexMatch) {
|
||||
return FilenameType(kSingleString, Some(fileName));
|
||||
}
|
||||
|
||||
// Suspected userChromeJS script
|
||||
rv = RegexEval(kUCJSRegex, fileName, /* aOnlyMatch = */ true, regexMatch);
|
||||
if (NS_FAILED(rv)) {
|
||||
return FilenameType(kRegexFailure, Nothing());
|
||||
}
|
||||
if (regexMatch) {
|
||||
return FilenameType(kSuspectedUserChromeJS, Nothing());
|
||||
}
|
||||
|
||||
return FilenameType(kOther, Nothing());
|
||||
}
|
||||
|
||||
/* static */
|
||||
void nsContentSecurityUtils::AssertEvalNotRestricted(
|
||||
JSContext* cx, nsIPrincipal* aSubjectPrincipal, const nsAString& aScript) {
|
||||
// This allowlist contains files that are permanently allowed to use
|
||||
// eval()-like functions. It is supposed to be restricted to files that are
|
||||
// exclusively used in testing contexts.
|
||||
static nsLiteralCString evalAllowlist[] = {
|
||||
// Test-only third-party library
|
||||
NS_LITERAL_CSTRING("resource://testing-common/sinon-7.2.7.js"),
|
||||
// Test-only third-party library
|
||||
NS_LITERAL_CSTRING("resource://testing-common/ajv-4.1.1.js"),
|
||||
// Test-only utility
|
||||
NS_LITERAL_CSTRING("resource://testing-common/content-task.js"),
|
||||
|
||||
// The Browser Toolbox/Console
|
||||
NS_LITERAL_CSTRING("debugger"),
|
||||
};
|
||||
|
||||
// We also permit two specific idioms in eval()-like contexts. We'd like to
|
||||
// elminate these too; but there are in-the-wild Mozilla privileged extensions
|
||||
// that use them.
|
||||
static NS_NAMED_LITERAL_STRING(sAllowedEval1, "this");
|
||||
static NS_NAMED_LITERAL_STRING(sAllowedEval2,
|
||||
"function anonymous(\n) {\nreturn this\n}");
|
||||
|
||||
bool systemPrincipal = aSubjectPrincipal->IsSystemPrincipal();
|
||||
if (systemPrincipal &&
|
||||
StaticPrefs::security_allow_eval_with_system_principal()) {
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() %s because allowing pref is "
|
||||
"enabled",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process")));
|
||||
return;
|
||||
}
|
||||
|
||||
if (XRE_IsE10sParentProcess() &&
|
||||
StaticPrefs::security_allow_eval_in_parent_process()) {
|
||||
MOZ_LOG(sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() in parent process because allowing pref is "
|
||||
"enabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!systemPrincipal && !XRE_IsE10sParentProcess()) {
|
||||
// Usage of eval we are unconcerned with.
|
||||
return;
|
||||
}
|
||||
|
||||
// This preference is a file used for autoconfiguration of Firefox
|
||||
// by administrators. It has also been (ab)used by the userChromeJS
|
||||
// project to run legacy-style 'extensions', some of which use eval,
|
||||
// all of which run in the System Principal context.
|
||||
nsAutoString jsConfigPref;
|
||||
Preferences::GetString("general.config.filename", jsConfigPref);
|
||||
if (!jsConfigPref.IsEmpty()) {
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() %s because of "
|
||||
"general.config.filename",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process")));
|
||||
return;
|
||||
}
|
||||
|
||||
// This preference is better known as userchrome.css which allows
|
||||
// customization of the Firefox UI. Believe it or not, you can also
|
||||
// use XBL bindings to get it to run Javascript in the same manner
|
||||
// as userChromeJS above, so even though 99.9% of people using
|
||||
// userchrome.css aren't doing that, we're still going to need to
|
||||
// disable the eval() assertion for them.
|
||||
if (Preferences::GetBool(
|
||||
"toolkit.legacyUserProfileCustomizations.stylesheets")) {
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() %s because of "
|
||||
"toolkit.legacyUserProfileCustomizations.stylesheets",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process")));
|
||||
return;
|
||||
}
|
||||
|
||||
// We permit these two common idioms to get access to the global JS object
|
||||
if (!aScript.IsEmpty() &&
|
||||
(aScript == sAllowedEval1 || aScript == sAllowedEval2)) {
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() %s because a key string is "
|
||||
"provided",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process")));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the allowlist for the provided filename. getFilename is a helper
|
||||
// function
|
||||
auto getFilename = [](JSContext* cx) -> nsCString {
|
||||
JS::AutoFilename scriptFilename;
|
||||
if (JS::DescribeScriptedCaller(cx, &scriptFilename)) {
|
||||
nsDependentCSubstring fileName_(scriptFilename.get(),
|
||||
strlen(scriptFilename.get()));
|
||||
ToLowerCase(fileName_);
|
||||
// Extract file name alone if scriptFilename contains line number
|
||||
// separated by multiple space delimiters in few cases.
|
||||
int32_t fileNameIndex = fileName_.FindChar(' ');
|
||||
if (fileNameIndex != -1) {
|
||||
fileName_.SetLength(fileNameIndex);
|
||||
}
|
||||
|
||||
nsAutoCString fileName(fileName_);
|
||||
return std::move(fileName);
|
||||
}
|
||||
return NS_LITERAL_CSTRING("unknown-file");
|
||||
};
|
||||
|
||||
nsCString fileName = getFilename(cx);
|
||||
for (const nsLiteralCString& allowlistEntry : evalAllowlist) {
|
||||
if (fileName.Equals(allowlistEntry)) {
|
||||
MOZ_LOG(
|
||||
sCSMLog, LogLevel::Debug,
|
||||
("Allowing eval() %s because the containing "
|
||||
"file is in the allowlist",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send Telemetry
|
||||
Telemetry::EventID eventType =
|
||||
systemPrincipal ? Telemetry::EventID::Security_Evalusage_Systemcontext
|
||||
: Telemetry::EventID::Security_Evalusage_Parentprocess;
|
||||
|
||||
FilenameType fileNameType =
|
||||
FilenameToEvalType(NS_ConvertUTF8toUTF16(fileName));
|
||||
mozilla::Maybe<nsTArray<EventExtraEntry>> extra;
|
||||
if (fileNameType.second().isSome()) {
|
||||
extra = Some<nsTArray<EventExtraEntry>>({EventExtraEntry{
|
||||
NS_LITERAL_CSTRING("fileinfo"),
|
||||
NS_ConvertUTF16toUTF8(fileNameType.second().value())}});
|
||||
} else {
|
||||
extra = Nothing();
|
||||
}
|
||||
if (!sTelemetryEventEnabled.exchange(true)) {
|
||||
sTelemetryEventEnabled = true;
|
||||
Telemetry::SetEventRecordingEnabled(NS_LITERAL_CSTRING("security"), true);
|
||||
}
|
||||
Telemetry::RecordEvent(eventType, mozilla::Some(fileNameType.first()), extra);
|
||||
|
||||
// Crash or Log
|
||||
#ifdef DEBUG
|
||||
MOZ_CRASH_UNSAFE_PRINTF(
|
||||
"Blocking eval() %s from file %s and script provided "
|
||||
"%s",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process"),
|
||||
fileName.get(), NS_ConvertUTF16toUTF8(aScript).get());
|
||||
#else
|
||||
MOZ_LOG(sCSMLog, LogLevel::Warning,
|
||||
("Blocking eval() %s from file %s and script "
|
||||
"provided %s",
|
||||
(systemPrincipal ? "with System Principal" : "in parent process"),
|
||||
fileName.get(), NS_ConvertUTF16toUTF8(aScript).get()));
|
||||
#endif
|
||||
|
||||
// In the future, we will change this function to return false and abort JS
|
||||
// execution without crashing the process. For now, just collect data.
|
||||
}
|
||||
|
||||
#if defined(DEBUG)
|
||||
/* static */
|
||||
void nsContentSecurityUtils::AssertAboutPageHasCSP(Document* aDocument) {
|
||||
|
|
|
@ -15,10 +15,17 @@ class Document;
|
|||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
typedef mozilla::Pair<nsCString, mozilla::Maybe<nsString>> FilenameType;
|
||||
|
||||
class nsContentSecurityUtils {
|
||||
public:
|
||||
static FilenameType FilenameToEvalType(const nsString& fileName);
|
||||
static void AssertEvalNotRestricted(JSContext* cx,
|
||||
nsIPrincipal* aSubjectPrincipal,
|
||||
const nsAString& aScript);
|
||||
|
||||
#if defined(DEBUG)
|
||||
static void AssertAboutPageHasCSP(Document* aDocument);
|
||||
static void AssertAboutPageHasCSP(mozilla::dom::Document* aDocument);
|
||||
#endif
|
||||
};
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "nsContentSecurityManager.h"
|
||||
#include "nsContentSecurityUtils.h"
|
||||
#include "nsStringFwd.h"
|
||||
|
||||
static NS_NAMED_LITERAL_CSTRING(kChromeURI, "chromeuri");
|
||||
|
@ -25,13 +25,13 @@ TEST(FilenameEvalParser, ResourceChrome)
|
|||
{
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "chrome://firegestures/content/browser.js");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kChromeURI && ret.second().isSome() &&
|
||||
ret.second().value() == str);
|
||||
}
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "resource://firegestures/content/browser.js");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kResourceURI && ret.second().isSome() &&
|
||||
ret.second().value() == str);
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ TEST(FilenameEvalParser, MozExtension)
|
|||
"jar:file:///c:/users/bob/appdata/roaming/mozilla/firefox/profiles/foo/"
|
||||
"extensions/federated-learning@shield.mozilla.org.xpi!/experiments/"
|
||||
"study/api.js");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kMozillaExtension &&
|
||||
ret.second().value() ==
|
||||
NS_LITERAL_STRING(
|
||||
|
@ -57,7 +57,7 @@ TEST(FilenameEvalParser, MozExtension)
|
|||
"jar:file:///c:/users/bob/appdata/roaming/mozilla/firefox/profiles/foo/"
|
||||
"extensions/federated-learning@shigeld.mozilla.org.xpi!/experiments/"
|
||||
"study/api.js");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(
|
||||
ret.first() == kMozillaExtension &&
|
||||
ret.second().value() ==
|
||||
|
@ -70,7 +70,7 @@ TEST(FilenameEvalParser, MozExtension)
|
|||
"jar:file:///c:/users/bob/appdata/roaming/mozilla/firefox/profiles/foo/"
|
||||
"extensions/federated-learning@shigeld.mozilla.org.xpi!/experiments/"
|
||||
"study/apiiiiiiiiiiiiiiiiiiiiiiiiiiiiii.js");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(
|
||||
ret.first() == kMozillaExtension &&
|
||||
ret.second().value() ==
|
||||
|
@ -83,20 +83,20 @@ TEST(FilenameEvalParser, UserChromeJS)
|
|||
{
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "firegestures/content/browser.uc.js");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kSuspectedUserChromeJS &&
|
||||
!ret.second().isSome());
|
||||
}
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "firegestures/content/browser.uc.js?");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kSuspectedUserChromeJS &&
|
||||
!ret.second().isSome());
|
||||
}
|
||||
{
|
||||
nsLiteralString str =
|
||||
NS_LITERAL_STRING("firegestures/content/browser.uc.js?243244224");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kSuspectedUserChromeJS &&
|
||||
!ret.second().isSome());
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ TEST(FilenameEvalParser, UserChromeJS)
|
|||
str,
|
||||
"file:///b:/fxprofiles/mark/chrome/"
|
||||
"addbookmarkherewithmiddleclick.uc.js?1558444389291");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kSuspectedUserChromeJS &&
|
||||
!ret.second().isSome());
|
||||
}
|
||||
|
@ -115,23 +115,23 @@ TEST(FilenameEvalParser, SingleFile)
|
|||
{
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "firegestures/content");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() != kSingleString && !ret.second().isSome());
|
||||
}
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "firegestures\\content");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() != kSingleString && !ret.second().isSome());
|
||||
}
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "browser.uc.js?2456");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kSingleString && ret.second().isSome() &&
|
||||
ret.second().value() == str);
|
||||
}
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "debugger");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kSingleString && ret.second().isSome() &&
|
||||
ret.second().value() == str);
|
||||
}
|
||||
|
@ -141,22 +141,22 @@ TEST(FilenameEvalParser, Other)
|
|||
{
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "firegestures/content");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kOther && !ret.second().isSome());
|
||||
}
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "firegestures\\content");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kOther && !ret.second().isSome());
|
||||
}
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "/home/tom/files/thing");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kOther && !ret.second().isSome());
|
||||
}
|
||||
{
|
||||
NS_NAMED_LITERAL_STRING(str, "file://c/uers/tom/file.txt");
|
||||
FilenameType ret = nsContentSecurityManager::FilenameToEvalType(str);
|
||||
FilenameType ret = nsContentSecurityUtils::FilenameToEvalType(str);
|
||||
ASSERT_TRUE(ret.first() == kOther && !ret.second().isSome());
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче