mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-19 08:15:31 +00:00
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:
parent
79f14fa6ca
commit
7081cf6275
@ -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;
|
||||
|
@ -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."),
|
||||
|
50
js/src/jit-test/tests/basic/script-filename-validation-1.js
Normal file
50
js/src/jit-test/tests/basic/script-filename-validation-1.js
Normal 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);
|
22
js/src/jit-test/tests/basic/script-filename-validation-2.js
Normal file
22
js/src/jit-test/tests/basic/script-filename-validation-2.js
Normal 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);
|
@ -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}")
|
||||
|
@ -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)
|
||||
|
@ -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 */
|
||||
|
||||
/**
|
||||
|
@ -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"
|
||||
|
@ -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)) {
|
||||
|
@ -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*);
|
||||
|
||||
|
@ -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*);
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user