From b79e24df196f1e3d90bc8eae5caea00819775a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Wang?= Date: Wed, 11 Dec 2024 09:40:19 +0000 Subject: [PATCH] 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 --- caps/nsScriptSecurityManager.cpp | 1 + js/public/Principals.h | 16 ++++ js/src/builtin/Eval.cpp | 20 +++- js/src/jsapi-tests/moz.build | 1 + .../testDynamicCodeBrandChecks.cpp | 93 +++++++++++++++++++ js/src/jsapi-tests/testStructuredClone.cpp | 1 + js/src/shell/js.cpp | 1 + js/src/vm/JSContext.cpp | 11 +++ js/src/vm/JSContext.h | 4 + 9 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 js/src/jsapi-tests/testDynamicCodeBrandChecks.cpp diff --git a/caps/nsScriptSecurityManager.cpp b/caps/nsScriptSecurityManager.cpp index a2c8339beef8..3f5f157c64a6 100644 --- a/caps/nsScriptSecurityManager.cpp +++ b/caps/nsScriptSecurityManager.cpp @@ -1554,6 +1554,7 @@ void nsScriptSecurityManager::InitJSCallbacks(JSContext* aCx) { static const JSSecurityCallbacks securityCallbacks = { ContentSecurityPolicyPermitsJSAction, + nullptr, // codeForEvalGets JSPrincipalsSubsume, }; diff --git a/js/public/Principals.h b/js/public/Principals.h index 89d08865d5a3..1c633b132bd0 100644 --- a/js/public/Principals.h +++ b/js/public/Principals.h @@ -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 outCode); + struct JSSecurityCallbacks { JSCSPEvalChecker contentSecurityPolicyAllows; + JSCodeForEvalOp codeForEvalGets; JSSubsumesOp subsumes; }; diff --git a/js/src/builtin/Eval.cpp b/js/src/builtin/Eval.cpp index 02826bc11490..c23accdc5399 100644 --- a/js/src/builtin/Eval.cpp +++ b/js/src/builtin/Eval.cpp @@ -239,21 +239,31 @@ static bool EvalKernel(JSContext* cx, HandleValue v, EvalType evalType, env->is()); 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 diff --git a/js/src/jsapi-tests/moz.build b/js/src/jsapi-tests/moz.build index 53bb0665737b..d3acec14d45b 100644 --- a/js/src/jsapi-tests/moz.build +++ b/js/src/jsapi-tests/moz.build @@ -40,6 +40,7 @@ UNIFIED_SOURCES += [ "testDeflateStringToUTF8Buffer.cpp", "testDeleteProperty.cpp", "testDifferentNewTargetInvokeConstructor.cpp", + "testDynamicCodeBrandChecks.cpp", "testEmptyWindowIsOmitted.cpp", "testErrorCopying.cpp", "testErrorLineOfContext.cpp", diff --git a/js/src/jsapi-tests/testDynamicCodeBrandChecks.cpp b/js/src/jsapi-tests/testDynamicCodeBrandChecks.cpp new file mode 100644 index 000000000000..bb166677ce5b --- /dev/null +++ b/js/src/jsapi-tests/testDynamicCodeBrandChecks.cpp @@ -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 aCode, + JS::MutableHandle 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) diff --git a/js/src/jsapi-tests/testStructuredClone.cpp b/js/src/jsapi-tests/testStructuredClone.cpp index 3a1617520711..15f2cc9cd3d6 100644 --- a/js/src/jsapi-tests/testStructuredClone.cpp +++ b/js/src/jsapi-tests/testStructuredClone.cpp @@ -258,6 +258,7 @@ struct StructuredCloneTestPrincipals final : public JSPrincipals { JSSecurityCallbacks StructuredCloneTestPrincipals::securityCallbacks = { nullptr, // contentSecurityPolicyAllows + nullptr, // codeForEvalGets subsumes}; BEGIN_TEST(testStructuredClone_SavedFrame) { diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 01d6ce1ec1fc..a768c294b1e3 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -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. diff --git a/js/src/vm/JSContext.cpp b/js/src/vm/JSContext.cpp index d23062e1b474..238987e4d20f 100644 --- a/js/src/vm/JSContext.cpp +++ b/js/src/vm/JSContext.cpp @@ -1245,6 +1245,17 @@ bool JSContext::isRuntimeCodeGenEnabled(JS::RuntimeCode kind, return true; } +bool JSContext::getCodeForEval(HandleObject code, + JS::MutableHandle 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 { /* diff --git a/js/src/vm/JSContext.h b/js/src/vm/JSContext.h index cd6f9824e7eb..a480d9d017b0 100644 --- a/js/src/vm/JSContext.h +++ b/js/src/vm/JSContext.h @@ -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 outCode); + size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; size_t sizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const;