Bug 1577280 - Add a script filename validation callback. r=tcampbell

This is a process-wide callback that can be used by embedders to block parsing
or decoding of scripts that are considered unsafe.

Differential Revision: https://phabricator.services.mozilla.com/D44620

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Jan de Mooij 2019-09-20 07:41:30 +00:00
parent 79f14fa6ca
commit 7081cf6275
12 changed files with 172 additions and 3 deletions

View File

@ -206,10 +206,11 @@ class JS_PUBLIC_API ReadOnlyCompileOptions : public TransitiveCompileOptions {
bool nonSyntacticScope = false;
bool noScriptRval = false;
private:
friend class CompileOptions;
protected:
// Flag used to bypass the filename validation callback.
// See also SetFilenameValidationCallback.
bool skipFilenameValidation_ = false;
ReadOnlyCompileOptions() = default;
// Set all POD options (those not requiring reference counts, copies,
@ -220,6 +221,7 @@ class JS_PUBLIC_API ReadOnlyCompileOptions : public TransitiveCompileOptions {
public:
// Read-only accessors for non-POD options. The proper way to set these
// depends on the derived type.
bool skipFilenameValidation() const { return skipFilenameValidation_; }
const char* filename() const { return filename_; }
const char* introducerFilename() const { return introducerFilename_; }
const char16_t* sourceMapURL() const { return sourceMapURL_; }
@ -389,6 +391,11 @@ class MOZ_STACK_CLASS JS_PUBLIC_API CompileOptions final
return *this;
}
CompileOptions& setSkipFilenameValidation(bool b) {
skipFilenameValidation_ = b;
return *this;
}
CompileOptions& setSelfHostingMode(bool shm) {
selfHostingMode = shm;
return *this;

View File

@ -1803,6 +1803,27 @@ static bool DisableTrackAllocations(JSContext* cx, unsigned argc, Value* vp) {
return true;
}
static bool SetTestFilenameValidationCallback(JSContext* cx, unsigned argc,
Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// Accept all filenames that start with "safe". In system code also accept
// filenames starting with "system".
auto testCb = [](const char* filename, bool isSystemRealm) -> bool {
if (strstr(filename, "safe") == filename) {
return true;
}
if (isSystemRealm && strstr(filename, "system") == filename) {
return true;
}
return false;
};
JS::SetFilenameValidationCallback(testCb);
args.rval().setUndefined();
return true;
}
static void FinalizeExternalString(const JSStringFinalizer* fin,
char16_t* chars);
@ -6199,6 +6220,11 @@ static const JSFunctionSpecWithHelp TestingFunctions[] = {
"disableTrackAllocations()",
" Stop capturing the JS stack at every allocation."),
JS_FN_HELP("setTestFilenameValidationCallback", SetTestFilenameValidationCallback, 0, 0,
"setTestFilenameValidationCallback()",
" Set the filename validation callback to a callback that accepts only\n"
" filenames starting with 'safe' or (only in system realms) 'system'."),
JS_FN_HELP("newExternalString", NewExternalString, 1, 0,
"newExternalString(str)",
" Copies str's chars and returns a new external string."),

View File

@ -0,0 +1,50 @@
load(libdir + "asserts.js");
setTestFilenameValidationCallback();
// Filenames starting with "safe" are fine.
assertEq(evaluate("2", {fileName: "safe.js"}), 2);
assertEq(evaluate("eval(3)", {fileName: "safe.js"}), 3);
assertEq(evaluate("Function('return 4')()", {fileName: "safe.js"}), 4);
// Delazification is fine.
function foo(x) {
function bar(x) { return x + 1; }
return bar(x);
}
assertEq(foo(1), 2);
// These are all blocked.
assertThrowsInstanceOf(() => evaluate("throw 2", {fileName: "unsafe.js"}), InternalError);
assertThrowsInstanceOf(() => evaluate("throw 2", {fileName: "system.js"}), InternalError);
assertThrowsInstanceOf(() => evaluate("throw 2", {fileName: ""}), InternalError);
assertThrowsInstanceOf(() => evaluate("throw 2"), InternalError);
assertThrowsInstanceOf(() => eval("throw 2"), InternalError);
assertThrowsInstanceOf(() => Function("return 1"), InternalError);
assertThrowsInstanceOf(() => parseModule("{ function x() {} }"), InternalError);
// The error message must contain the filename.
var ex = null;
try {
evaluate("throw 2", {fileName: "file://foo.js"});
} catch (e) {
ex = e;
}
assertEq(ex.toString(), "InternalError: unsafe filename: file://foo.js");
// Off-thread parse throws too, when finishing.
if (helperThreadCount() > 0) {
offThreadCompileScript('throw 1');
assertThrowsInstanceOf(() => runOffThreadScript(), InternalError);
}
// Unsafe filename is accepted if we opt-out.
assertEq(evaluate("2", {fileName: "unsafe.js", skipFileNameValidation: true}), 2);
assertEq(evaluate("3", {skipFileNameValidation: true}), 3);
// In system realms we also accept filenames starting with "system".
var systemRealm = newGlobal({newCompartment: true, systemPrincipal: true});
assertEq(systemRealm.evaluate("1 + 2", {fileName: "system.js"}), 3);
assertEq(systemRealm.evaluate("2 + 2", {fileName: "safe.js"}), 4);
assertThrowsInstanceOf(() => systemRealm.evaluate("1 + 2", {fileName: "unsafe.js"}),
systemRealm.InternalError);

View File

@ -0,0 +1,22 @@
load(libdir + "asserts.js");
load(libdir + 'bytecode-cache.js');
// Install the callback after evaluating the script and saving the bytecode
// (generation 0). XDR decoding after this should throw.
var g = newGlobal({cloneSingletons: true});
test = `
assertEq(generation, 0);
`;
assertThrowsInstanceOf(() => {
evalWithCache(test, {
global: g,
checkAfter: function (ctx) {
assertEq(g.generation, 0);
setTestFilenameValidationCallback();
}
});
}, g.InternalError);
// Generation should be 1 (XDR decoding threw an exception).
assertEq(g.generation, 1);

View File

@ -184,6 +184,7 @@ MSG_DEF(JSMSG_TOO_BIG_TO_ENCODE, 0, JSEXN_INTERNALERR, "data are to big to
MSG_DEF(JSMSG_TOO_DEEP, 1, JSEXN_INTERNALERR, "{0} nested too deeply")
MSG_DEF(JSMSG_UNCAUGHT_EXCEPTION, 1, JSEXN_INTERNALERR, "uncaught exception: {0}")
MSG_DEF(JSMSG_UNKNOWN_FORMAT, 1, JSEXN_INTERNALERR, "unknown bytecode format {0}")
MSG_DEF(JSMSG_UNSAFE_FILENAME, 1, JSEXN_INTERNALERR, "unsafe filename: {0}")
// Frontend
MSG_DEF(JSMSG_ACCESSOR_WRONG_ARGS, 3, JSEXN_SYNTAXERR, "{0} functions must have {1} argument{2}")

View File

@ -1842,6 +1842,11 @@ JS_PUBLIC_API void SetHelperThreadTaskCallback(
HelperThreadTaskCallback = callback;
}
JS_PUBLIC_API void JS::SetFilenameValidationCallback(
JS::FilenameValidationCallback cb) {
js::gFilenameValidationCallback = cb;
}
/*** Standard internal methods **********************************************/
JS_PUBLIC_API bool JS_GetPrototype(JSContext* cx, HandleObject obj,
@ -3551,6 +3556,7 @@ void JS::ReadOnlyCompileOptions::copyPODNonTransitiveOptions(
isRunOnce = rhs.isRunOnce;
noScriptRval = rhs.noScriptRval;
nonSyntacticScope = rhs.nonSyntacticScope;
skipFilenameValidation_ = rhs.skipFilenameValidation_;
}
JS::OwningCompileOptions::OwningCompileOptions(JSContext* cx)

View File

@ -396,6 +396,20 @@ JS_PUBLIC_API bool InitSelfHostedCode(JSContext* cx);
*/
JS_PUBLIC_API void AssertObjectBelongsToCurrentThread(JSObject* obj);
/**
* Install a process-wide callback to validate script filenames. The JS engine
* will invoke this callback for each JS script it parses or XDR decodes.
*
* If the callback returns |false|, an exception is thrown and parsing/decoding
* will be aborted.
*
* See also CompileOptions::setSkipFilenameValidation to opt-out of the callback
* for specific parse jobs.
*/
using FilenameValidationCallback = bool (*)(const char* filename,
bool isSystemRealm);
JS_PUBLIC_API void SetFilenameValidationCallback(FilenameValidationCallback cb);
} /* namespace JS */
/**

View File

@ -1810,6 +1810,13 @@ static bool ParseCompileOptions(JSContext* cx, CompileOptions& options,
options.setFile(fileNameBytes.get());
}
if (!JS_GetProperty(cx, opts, "skipFileNameValidation", &v)) {
return false;
}
if (!v.isUndefined()) {
options.setSkipFilenameValidation(ToBoolean(v));
}
if (!JS_GetProperty(cx, opts, "element", &v)) {
return false;
}
@ -8473,6 +8480,7 @@ static const JSFunctionSpecWithHelp shell_functions[] = {
" isRunOnce: use the isRunOnce compiler option (default: false)\n"
" noScriptRval: use the no-script-rval compiler option (default: false)\n"
" fileName: filename for error messages and debug info\n"
" skipFileNameValidation: skip the filename-validation callback\n"
" lineNumber: starting line number for error messages and debug info\n"
" columnNumber: starting column number for error messages and debug info\n"
" global: global in which to execute the code\n"

View File

@ -1721,6 +1721,32 @@ ScriptSourceObject* ScriptSourceObject::unwrappedCanonical() const {
return &UncheckedUnwrap(obj)->as<ScriptSourceObject>();
}
static MOZ_MUST_USE bool MaybeValidateFilename(
JSContext* cx, HandleScriptSourceObject sso,
const ReadOnlyCompileOptions& options) {
// When parsing off-thread we want to do filename validation on the main
// thread. This makes off-thread parsing more pure and is simpler because we
// can't easily throw exceptions off-thread.
MOZ_ASSERT(!cx->isHelperThreadContext());
if (!gFilenameValidationCallback) {
return true;
}
const char* filename = sso->source()->filename();
if (!filename || options.skipFilenameValidation()) {
return true;
}
if (gFilenameValidationCallback(filename, cx->realm()->isSystem())) {
return true;
}
JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_UNSAFE_FILENAME,
filename);
return false;
}
/* static */
bool ScriptSourceObject::initFromOptions(
JSContext* cx, HandleScriptSourceObject source,
@ -1733,6 +1759,10 @@ bool ScriptSourceObject::initFromOptions(
MOZ_ASSERT(source->getReservedSlot(INTRODUCTION_SCRIPT_SLOT)
.isMagic(JS_GENERIC_MAGIC));
if (!MaybeValidateFilename(cx, source, options)) {
return false;
}
RootedObject element(cx, options.element());
RootedString elementAttributeName(cx, options.elementAttributeName());
if (!initElementProperties(cx, source, element, elementAttributeName)) {

View File

@ -71,6 +71,8 @@ using mozilla::PositiveInfinity;
Atomic<size_t> JSRuntime::liveRuntimesCount;
Atomic<JS::LargeAllocationFailureCallback> js::OnLargeAllocationFailure;
JS::FilenameValidationCallback js::gFilenameValidationCallback = nullptr;
namespace js {
void (*HelperThreadTaskCallback)(js::RunnableTask*);

View File

@ -1104,6 +1104,8 @@ extern mozilla::Atomic<JS::LargeAllocationFailureCallback>
// jsapi.h.
extern mozilla::Atomic<JS::BuildIdOp> GetBuildId;
extern JS::FilenameValidationCallback gFilenameValidationCallback;
// This callback is set by js::SetHelperThreadTaskCallback and may be null.
// See comment in jsapi.h.
extern void (*HelperThreadTaskCallback)(js::RunnableTask*);

View File

@ -2625,6 +2625,7 @@ void js::FillSelfHostingCompileOptions(CompileOptions& options) {
*/
options.setIntroductionType("self-hosted");
options.setFileAndLine("self-hosted", 1);
options.setSkipFilenameValidation(true);
options.setSelfHostingMode(true);
options.setForceFullParse();
options.werrorOption = true;