Bug 1905239 - Introduce HostGetCodeForEval hook for PerformEval. r=tschuster
See https://tc39.es/proposal-dynamic-code-brand-checks Differential Revision: https://phabricator.services.mozilla.com/D229477
This commit is contained in:
@@ -1554,6 +1554,7 @@ void nsScriptSecurityManager::InitJSCallbacks(JSContext* aCx) {
|
||||
|
||||
static const JSSecurityCallbacks securityCallbacks = {
|
||||
ContentSecurityPolicyPermitsJSAction,
|
||||
nullptr, // codeForEvalGets
|
||||
JSPrincipalsSubsume,
|
||||
};
|
||||
|
||||
|
||||
@@ -92,8 +92,24 @@ enum class RuntimeCode { JS, WASM };
|
||||
typedef bool (*JSCSPEvalChecker)(JSContext* cx, JS::RuntimeCode kind,
|
||||
JS::HandleString code);
|
||||
|
||||
/*
|
||||
* Provide a string of code from an Object argument, to be used by eval.
|
||||
* See JSContext::getCodeForEval() in vm/JSContext.cpp as well as
|
||||
* https://tc39.es/proposal-dynamic-code-brand-checks/#sec-hostgetcodeforeval
|
||||
*
|
||||
* `code` is the JavaScript object passed by the user.
|
||||
* `outCode` is the JavaScript string to be actually executed, with nullptr
|
||||
* meaning NO-CODE.
|
||||
*
|
||||
* Return false on failure, true on success. The |outCode| parameter should not
|
||||
* be modified in case of failure.
|
||||
*/
|
||||
typedef bool (*JSCodeForEvalOp)(JSContext* cx, JS::HandleObject code,
|
||||
JS::MutableHandle<JSString*> outCode);
|
||||
|
||||
struct JSSecurityCallbacks {
|
||||
JSCSPEvalChecker contentSecurityPolicyAllows;
|
||||
JSCodeForEvalOp codeForEvalGets;
|
||||
JSSubsumesOp subsumes;
|
||||
};
|
||||
|
||||
|
||||
@@ -239,21 +239,31 @@ static bool EvalKernel(JSContext* cx, HandleValue v, EvalType evalType,
|
||||
env->is<GlobalLexicalEnvironmentObject>());
|
||||
AssertInnerizedEnvironmentChain(cx, *env);
|
||||
|
||||
// Step 2.
|
||||
if (!v.isString()) {
|
||||
// "Dynamic Code Brand Checks" adds support for Object values.
|
||||
// https://tc39.es/proposal-dynamic-code-brand-checks/#sec-performeval
|
||||
// Steps 2-4.
|
||||
RootedString str(cx);
|
||||
if (v.isString()) {
|
||||
str = v.toString();
|
||||
} else if (v.isObject()) {
|
||||
RootedObject obj(cx, &v.toObject());
|
||||
if (!cx->getCodeForEval(obj, &str)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!str) {
|
||||
vp.set(v);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Steps 3-4.
|
||||
RootedString str(cx, v.toString());
|
||||
// Steps 6-8.
|
||||
if (!cx->isRuntimeCodeGenEnabled(JS::RuntimeCode::JS, str)) {
|
||||
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
||||
JSMSG_CSP_BLOCKED_EVAL);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 5 ff.
|
||||
// Step 9 ff.
|
||||
|
||||
// Per ES5, indirect eval runs in the global scope. (eval is specified this
|
||||
// way so that the compiler can make assumptions about what bindings may or
|
||||
|
||||
@@ -40,6 +40,7 @@ UNIFIED_SOURCES += [
|
||||
"testDeflateStringToUTF8Buffer.cpp",
|
||||
"testDeleteProperty.cpp",
|
||||
"testDifferentNewTargetInvokeConstructor.cpp",
|
||||
"testDynamicCodeBrandChecks.cpp",
|
||||
"testEmptyWindowIsOmitted.cpp",
|
||||
"testErrorCopying.cpp",
|
||||
"testErrorLineOfContext.cpp",
|
||||
|
||||
93
js/src/jsapi-tests/testDynamicCodeBrandChecks.cpp
Normal file
93
js/src/jsapi-tests/testDynamicCodeBrandChecks.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
||||
* vim: set ts=8 sts=2 et sw=2 tw=80:
|
||||
*
|
||||
* Tests that the column number of error reports is properly copied over from
|
||||
* other reports when invoked from the C++ api.
|
||||
*/
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "jsapi-tests/tests.h"
|
||||
|
||||
BEGIN_TEST(testDynamicCodeBrandChecks_DefaultHostGetCodeForEval) {
|
||||
JS::RootedValue v(cx);
|
||||
|
||||
// String arguments are evaluated.
|
||||
EVAL("eval('5*8');", &v);
|
||||
CHECK(v.isNumber() && v.toNumber() == 40);
|
||||
|
||||
// Other arguments are returned as is by eval.
|
||||
EVAL("eval({myProp: 41});", &v);
|
||||
CHECK(v.isObject());
|
||||
JS::RootedObject obj(cx, &v.toObject());
|
||||
JS::RootedValue myProp(cx);
|
||||
CHECK(JS_GetProperty(cx, obj, "myProp", &myProp));
|
||||
CHECK(myProp.isNumber() && myProp.toNumber() == 41);
|
||||
|
||||
EVAL("eval({trustedCode: '6*7'}).trustedCode;", &v);
|
||||
CHECK(v.isString());
|
||||
JSString* str = v.toString();
|
||||
CHECK(JS_LinearStringEqualsLiteral(JS_ASSERT_STRING_IS_LINEAR(str), "6*7"));
|
||||
|
||||
EVAL("eval({trustedCode: 42}).trustedCode;", &v);
|
||||
CHECK(v.isNumber() && v.toNumber() == 42);
|
||||
|
||||
return true;
|
||||
}
|
||||
END_TEST(testDynamicCodeBrandChecks_DefaultHostGetCodeForEval)
|
||||
|
||||
static bool ExtractTrustedCodeStringProperty(
|
||||
JSContext* aCx, JS::Handle<JSObject*> aCode,
|
||||
JS::MutableHandle<JSString*> outCode) {
|
||||
JS::RootedValue value(aCx);
|
||||
if (!JS_GetProperty(aCx, aCode, "trustedCode", &value)) {
|
||||
return false;
|
||||
}
|
||||
if (value.isUndefined()) {
|
||||
// If the property is undefined, return NO-CODE.
|
||||
outCode.set(nullptr);
|
||||
return true;
|
||||
}
|
||||
if (value.isString()) {
|
||||
// If the property is a string, return it.
|
||||
outCode.set(value.toString());
|
||||
return true;
|
||||
}
|
||||
// Otherwise, emulate a failure.
|
||||
JS_ReportErrorASCII(aCx, "Unsupported value for trustedCode property");
|
||||
return false;
|
||||
}
|
||||
|
||||
BEGIN_TEST(testDynamicCodeBrandChecks_CustomHostGetCodeForEval) {
|
||||
JSSecurityCallbacks securityCallbacksWithEvalAcceptingObject = {
|
||||
nullptr, // contentSecurityPolicyAllows
|
||||
ExtractTrustedCodeStringProperty, // codeForEvalGets
|
||||
nullptr // subsumes
|
||||
};
|
||||
JS_SetSecurityCallbacks(cx, &securityCallbacksWithEvalAcceptingObject);
|
||||
JS::RootedValue v(cx);
|
||||
|
||||
// String arguments are evaluated.
|
||||
EVAL("eval('5*8');", &v);
|
||||
CHECK(v.isNumber() && v.toNumber() == 40);
|
||||
|
||||
// Other arguments are returned as is by eval...
|
||||
EVAL("eval({myProp: 41});", &v);
|
||||
CHECK(v.isObject());
|
||||
JS::RootedObject obj(cx, &v.toObject());
|
||||
JS::RootedValue myProp(cx);
|
||||
CHECK(JS_GetProperty(cx, obj, "myProp", &myProp));
|
||||
CHECK(myProp.isNumber() && myProp.toNumber() == 41);
|
||||
|
||||
// ... but Objects are first tentatively converted to String by the
|
||||
// codeForEvalGets callback.
|
||||
EVAL("eval({trustedCode: '6*7'});", &v);
|
||||
CHECK(v.isNumber() && v.toNumber() == 6 * 7);
|
||||
|
||||
// And if that codeForEvalGets callback fails, then so does the eval call.
|
||||
CHECK(!execDontReport("eval({trustedCode: 6*7});", __FILE__, __LINE__));
|
||||
|
||||
return true;
|
||||
}
|
||||
END_TEST(testDynamicCodeBrandChecks_CustomHostGetCodeForEval)
|
||||
@@ -258,6 +258,7 @@ struct StructuredCloneTestPrincipals final : public JSPrincipals {
|
||||
|
||||
JSSecurityCallbacks StructuredCloneTestPrincipals::securityCallbacks = {
|
||||
nullptr, // contentSecurityPolicyAllows
|
||||
nullptr, // codeForEvalGets
|
||||
subsumes};
|
||||
|
||||
BEGIN_TEST(testStructuredClone_SavedFrame) {
|
||||
|
||||
@@ -955,6 +955,7 @@ class ShellPrincipals final : public JSPrincipals {
|
||||
|
||||
JSSecurityCallbacks ShellPrincipals::securityCallbacks = {
|
||||
nullptr, // contentSecurityPolicyAllows
|
||||
nullptr, // codeForEvalGets
|
||||
subsumes};
|
||||
|
||||
// The fully-trusted principal subsumes all other principals.
|
||||
|
||||
@@ -1245,6 +1245,17 @@ bool JSContext::isRuntimeCodeGenEnabled(JS::RuntimeCode kind,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JSContext::getCodeForEval(HandleObject code,
|
||||
JS::MutableHandle<JSString*> outCode) {
|
||||
if (JSCodeForEvalOp gets = runtime()->securityCallbacks->codeForEvalGets) {
|
||||
return gets(this, code, outCode);
|
||||
}
|
||||
// Default implementation from the "Dynamic Code Brand Checks" spec.
|
||||
// https://tc39.es/proposal-dynamic-code-brand-checks/#sec-hostgetcodeforeval
|
||||
outCode.set(nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t JSContext::sizeOfExcludingThis(
|
||||
mozilla::MallocSizeOf mallocSizeOf) const {
|
||||
/*
|
||||
|
||||
@@ -805,6 +805,10 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext,
|
||||
// runtime code generation "unsafe-eval", or "wasm-unsafe-eval" for Wasm.
|
||||
bool isRuntimeCodeGenEnabled(JS::RuntimeCode kind, js::HandleString code);
|
||||
|
||||
// Get code to be used by eval for Object argument.
|
||||
bool getCodeForEval(JS::HandleObject code,
|
||||
JS::MutableHandle<JSString*> outCode);
|
||||
|
||||
size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const;
|
||||
size_t sizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user