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:
Frédéric Wang
2024-12-11 20:00:31 +00:00
parent df628bf5ac
commit 410b2420a9
9 changed files with 144 additions and 5 deletions

View File

@@ -1554,6 +1554,7 @@ void nsScriptSecurityManager::InitJSCallbacks(JSContext* aCx) {
static const JSSecurityCallbacks securityCallbacks = { static const JSSecurityCallbacks securityCallbacks = {
ContentSecurityPolicyPermitsJSAction, ContentSecurityPolicyPermitsJSAction,
nullptr, // codeForEvalGets
JSPrincipalsSubsume, JSPrincipalsSubsume,
}; };

View File

@@ -92,8 +92,24 @@ enum class RuntimeCode { JS, WASM };
typedef bool (*JSCSPEvalChecker)(JSContext* cx, JS::RuntimeCode kind, typedef bool (*JSCSPEvalChecker)(JSContext* cx, JS::RuntimeCode kind,
JS::HandleString code); 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 { struct JSSecurityCallbacks {
JSCSPEvalChecker contentSecurityPolicyAllows; JSCSPEvalChecker contentSecurityPolicyAllows;
JSCodeForEvalOp codeForEvalGets;
JSSubsumesOp subsumes; JSSubsumesOp subsumes;
}; };

View File

@@ -239,21 +239,31 @@ static bool EvalKernel(JSContext* cx, HandleValue v, EvalType evalType,
env->is<GlobalLexicalEnvironmentObject>()); env->is<GlobalLexicalEnvironmentObject>());
AssertInnerizedEnvironmentChain(cx, *env); AssertInnerizedEnvironmentChain(cx, *env);
// Step 2. // "Dynamic Code Brand Checks" adds support for Object values.
if (!v.isString()) { // 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); vp.set(v);
return true; return true;
} }
// Steps 3-4. // Steps 6-8.
RootedString str(cx, v.toString());
if (!cx->isRuntimeCodeGenEnabled(JS::RuntimeCode::JS, str)) { if (!cx->isRuntimeCodeGenEnabled(JS::RuntimeCode::JS, str)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_CSP_BLOCKED_EVAL); JSMSG_CSP_BLOCKED_EVAL);
return false; return false;
} }
// Step 5 ff. // Step 9 ff.
// Per ES5, indirect eval runs in the global scope. (eval is specified this // 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 // way so that the compiler can make assumptions about what bindings may or

View File

@@ -40,6 +40,7 @@ UNIFIED_SOURCES += [
"testDeflateStringToUTF8Buffer.cpp", "testDeflateStringToUTF8Buffer.cpp",
"testDeleteProperty.cpp", "testDeleteProperty.cpp",
"testDifferentNewTargetInvokeConstructor.cpp", "testDifferentNewTargetInvokeConstructor.cpp",
"testDynamicCodeBrandChecks.cpp",
"testEmptyWindowIsOmitted.cpp", "testEmptyWindowIsOmitted.cpp",
"testErrorCopying.cpp", "testErrorCopying.cpp",
"testErrorLineOfContext.cpp", "testErrorLineOfContext.cpp",

View File

@@ -0,0 +1,94 @@
/* -*- 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__));
cx->clearPendingException();
return true;
}
END_TEST(testDynamicCodeBrandChecks_CustomHostGetCodeForEval)

View File

@@ -258,6 +258,7 @@ struct StructuredCloneTestPrincipals final : public JSPrincipals {
JSSecurityCallbacks StructuredCloneTestPrincipals::securityCallbacks = { JSSecurityCallbacks StructuredCloneTestPrincipals::securityCallbacks = {
nullptr, // contentSecurityPolicyAllows nullptr, // contentSecurityPolicyAllows
nullptr, // codeForEvalGets
subsumes}; subsumes};
BEGIN_TEST(testStructuredClone_SavedFrame) { BEGIN_TEST(testStructuredClone_SavedFrame) {

View File

@@ -955,6 +955,7 @@ class ShellPrincipals final : public JSPrincipals {
JSSecurityCallbacks ShellPrincipals::securityCallbacks = { JSSecurityCallbacks ShellPrincipals::securityCallbacks = {
nullptr, // contentSecurityPolicyAllows nullptr, // contentSecurityPolicyAllows
nullptr, // codeForEvalGets
subsumes}; subsumes};
// The fully-trusted principal subsumes all other principals. // The fully-trusted principal subsumes all other principals.

View File

@@ -1245,6 +1245,17 @@ bool JSContext::isRuntimeCodeGenEnabled(JS::RuntimeCode kind,
return true; 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( size_t JSContext::sizeOfExcludingThis(
mozilla::MallocSizeOf mallocSizeOf) const { mozilla::MallocSizeOf mallocSizeOf) const {
/* /*

View File

@@ -805,6 +805,10 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext,
// runtime code generation "unsafe-eval", or "wasm-unsafe-eval" for Wasm. // runtime code generation "unsafe-eval", or "wasm-unsafe-eval" for Wasm.
bool isRuntimeCodeGenEnabled(JS::RuntimeCode kind, js::HandleString code); 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 sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const;
size_t sizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; size_t sizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const;