Skip to content

Commit

Permalink
Bug 1905239 - Introduce HostGetCodeForEval hook for PerformEval. r=ts…
Browse files Browse the repository at this point in the history
  • Loading branch information
fred-wang committed Dec 11, 2024
1 parent b042b13 commit 7b43b60
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 5 deletions.
1 change: 1 addition & 0 deletions caps/nsScriptSecurityManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,7 @@ void nsScriptSecurityManager::InitJSCallbacks(JSContext* aCx) {

static const JSSecurityCallbacks securityCallbacks = {
ContentSecurityPolicyPermitsJSAction,
nullptr, // codeForEvalGets
JSPrincipalsSubsume,
};

Expand Down
16 changes: 16 additions & 0 deletions js/public/Principals.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
20 changes: 15 additions & 5 deletions js/src/builtin/Eval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions js/src/jsapi-tests/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ UNIFIED_SOURCES += [
"testDeflateStringToUTF8Buffer.cpp",
"testDeleteProperty.cpp",
"testDifferentNewTargetInvokeConstructor.cpp",
"testDynamicCodeBrandChecks.cpp",
"testEmptyWindowIsOmitted.cpp",
"testErrorCopying.cpp",
"testErrorLineOfContext.cpp",
Expand Down
94 changes: 94 additions & 0 deletions js/src/jsapi-tests/testDynamicCodeBrandChecks.cpp
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions js/src/jsapi-tests/testStructuredClone.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ struct StructuredCloneTestPrincipals final : public JSPrincipals {

JSSecurityCallbacks StructuredCloneTestPrincipals::securityCallbacks = {
nullptr, // contentSecurityPolicyAllows
nullptr, // codeForEvalGets
subsumes};

BEGIN_TEST(testStructuredClone_SavedFrame) {
Expand Down
1 change: 1 addition & 0 deletions js/src/shell/js.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions js/src/vm/JSContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/*
Expand Down
4 changes: 4 additions & 0 deletions js/src/vm/JSContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit 7b43b60

Please sign in to comment.