diff --git a/src/workerd/api/tests/unsafe-test.js b/src/workerd/api/tests/unsafe-test.js new file mode 100644 index 00000000000..b7851ffb0c4 --- /dev/null +++ b/src/workerd/api/tests/unsafe-test.js @@ -0,0 +1,34 @@ +import { + strictEqual, + ok, + throws +} from 'node:assert'; + +export const basics = { + test(ctx, env) { + strictEqual(env.unsafe.eval('1'), 1); + + // eval does not capture outer scope. + let m = 1; + throws(() => env.unsafe.eval('m')); + + throws(() => env.unsafe.eval(' throw new Error("boom"); ', 'foo'), { + message: 'boom', + stack: /at foo/ + }); + + // Regular dynamic eval is still not allowed + throws(() => eval('')); + } +}; + +export const newFunction = { + test(ctx, env) { + const fn = env.unsafe.newFunction('return m', 'bar', 'm'); + strictEqual(fn.length, 1); + strictEqual(fn.name, 'bar'); + strictEqual(fn(), undefined); + strictEqual(fn(1), 1); + strictEqual(fn(fn), fn); + } +}; diff --git a/src/workerd/api/tests/unsafe-test.wd-test b/src/workerd/api/tests/unsafe-test.wd-test new file mode 100644 index 00000000000..5a8d31b81b2 --- /dev/null +++ b/src/workerd/api/tests/unsafe-test.wd-test @@ -0,0 +1,18 @@ +using Workerd = import "/workerd/workerd.capnp"; + +const unitTests :Workerd.Config = ( + services = [ + ( name = "unsafe-test", + worker = ( + modules = [ + (name = "worker", esModule = embed "unsafe-test.js") + ], + compatibilityDate = "2023-01-15", + compatibilityFlags = ["nodejs_compat", "experimental"], + bindings = [ + (name = "unsafe", unsafeEval = void ) + ] + ) + ), + ], +); diff --git a/src/workerd/api/unsafe.c++ b/src/workerd/api/unsafe.c++ new file mode 100644 index 00000000000..e048acdeb4b --- /dev/null +++ b/src/workerd/api/unsafe.c++ @@ -0,0 +1,46 @@ +#include "unsafe.h" + +namespace workerd::api { + +namespace { +static constexpr auto EVAL_STR = "eval"_kjc; +inline kj::StringPtr getName(jsg::Optional& name, kj::StringPtr def) { + return name.map([](kj::String& str) { + return str.asPtr(); + }).orDefault(def); +} +} // namespace + +jsg::JsValue UnsafeEval::eval(jsg::Lock& js, kj::String script, + jsg::Optional name) { + js.setAllowEval(true); + KJ_DEFER(js.setAllowEval(false)); + auto compiled = jsg::NonModuleScript::compile(script, js, getName(name, EVAL_STR)); + return jsg::JsValue(compiled.runAndReturn(js.v8Context())); +} + +UnsafeEval::UnsafeEvalFunction UnsafeEval::newFunction( + jsg::Lock& js, + jsg::JsString script, + jsg::Optional name, + jsg::Arguments> args, + const jsg::TypeHandler& handler) { + js.setAllowEval(true); + KJ_DEFER(js.setAllowEval(false)); + + auto nameStr = js.str(getName(name, EVAL_STR)); + v8::ScriptOrigin origin(js.v8Isolate, nameStr); + v8::ScriptCompiler::Source source(script, origin); + + auto argNames = KJ_MAP(arg, args) { + return v8::Local(arg.getHandle(js)); + }; + + auto fn = jsg::check(v8::ScriptCompiler::CompileFunction( + js.v8Context(), &source, argNames.size(), argNames.begin(), 0, nullptr)); + fn->SetName(nameStr); + + return KJ_ASSERT_NONNULL(handler.tryUnwrap(js, fn)); +} + +} // namespace workerd::api diff --git a/src/workerd/api/unsafe.h b/src/workerd/api/unsafe.h new file mode 100644 index 00000000000..13f6a480c21 --- /dev/null +++ b/src/workerd/api/unsafe.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +namespace workerd::api { + +// A special binding object that allows for dynamic evaluation. +class UnsafeEval: public jsg::Object { +public: + UnsafeEval() = default; + + // A non-capturing eval. Compile and evaluates the given script, returning whatever + // value is returned by the script. This version of eval intentionally does not + // capture any part of the outer scope other than globalThis and globally scoped + // variables. The optional `name` will appear in stack traces for any errors thrown. + // + // console.log(env.unsafe.eval('1 + 1')); // prints 2 + // + jsg::JsValue eval(jsg::Lock& js, kj::String script, jsg::Optional name); + + using UnsafeEvalFunction = jsg::Function)>; + + // Compiles and returns a new Function using the given script. The function does not + // capture any part of the outer scope other than globalThis and globally scoped + // variables. The optional `name` will be set as the name of the function and will + // appear in stack traces for any errors thrown. An optional list of argument names + // can be passed in. + // + // const fn = env.unsafe.newFunction('return m', 'foo', 'm'); + // console.log(fn(1)); // prints 1 + // + UnsafeEvalFunction newFunction( + jsg::Lock& js, + jsg::JsString script, + jsg::Optional name, + jsg::Arguments> args, + const jsg::TypeHandler& handler); + + JSG_RESOURCE_TYPE(UnsafeEval) { + JSG_METHOD(eval); + JSG_METHOD(newFunction); + } +}; + +#define EW_UNSAFE_ISOLATE_TYPES \ + api::UnsafeEval + +} // namespace workerd::api diff --git a/src/workerd/jsg/modules.c++ b/src/workerd/jsg/modules.c++ index 6f785991255..6d9ec374f7e 100644 --- a/src/workerd/jsg/modules.c++ +++ b/src/workerd/jsg/modules.c++ @@ -284,6 +284,12 @@ v8::Local CommonJsModuleContext::require(jsg::Lock& js, kj::String sp } } +v8::Local NonModuleScript::runAndReturn(v8::Local context) const { + auto isolate = context->GetIsolate(); + auto boundScript = unboundScript.Get(isolate)->BindToCurrentContext(); + return check(boundScript->Run(context)); +} + void NonModuleScript::run(v8::Local context) const { auto isolate = context->GetIsolate(); auto boundScript = unboundScript.Get(isolate)->BindToCurrentContext(); diff --git a/src/workerd/jsg/modules.h b/src/workerd/jsg/modules.h index 8aea5adfbb6..d108cc17a52 100644 --- a/src/workerd/jsg/modules.h +++ b/src/workerd/jsg/modules.h @@ -148,6 +148,8 @@ class NonModuleScript { // context then will run it to completion. void run(v8::Local context) const; + v8::Local runAndReturn(v8::Local context) const; + static jsg::NonModuleScript compile(kj::StringPtr code, jsg::Lock& js, kj::StringPtr name = "worker.js"); private: diff --git a/src/workerd/server/server.c++ b/src/workerd/server/server.c++ index e71a42dcc4d..2017026d337 100644 --- a/src/workerd/server/server.c++ +++ b/src/workerd/server/server.c++ @@ -2415,6 +2415,14 @@ static kj::Maybe createBinding( .scheme = kj::str(binding.getHyperdrive().getScheme()), }); } + case config::Worker::Binding::UNSAFE_EVAL: { + if (!experimental) { + errorReporter.addError(kj::str("Unsafe eval is an experimental feature. ", + "You must run workerd with `--experimental` to use this feature.")); + return kj::none; + } + return makeGlobal(Global::UnsafeEval {}); + } } errorReporter.addError(kj::str( errorContext, "has unrecognized type. Was the config compiled with a newer version of " diff --git a/src/workerd/server/workerd-api.c++ b/src/workerd/server/workerd-api.c++ index e3d5406c377..f49592a1a43 100644 --- a/src/workerd/server/workerd-api.c++ +++ b/src/workerd/server/workerd-api.c++ @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -77,6 +78,7 @@ JSG_DECLARE_ISOLATE_TYPE(JsgWorkerdIsolate, EW_SCHEDULED_ISOLATE_TYPES, EW_STREAMS_ISOLATE_TYPES, EW_TRACE_ISOLATE_TYPES, + EW_UNSAFE_ISOLATE_TYPES, EW_URL_ISOLATE_TYPES, EW_URL_STANDARD_ISOLATE_TYPES, EW_URLPATTERN_ISOLATE_TYPES, @@ -623,6 +625,9 @@ static v8::Local createBindingValue( kj::str(hyperdrive.user), kj::str(hyperdrive.password), kj::str(hyperdrive.scheme))); } + KJ_CASE_ONEOF(unsafe, Global::UnsafeEval) { + value = lock.wrap(context, jsg::alloc()); + } } return value; @@ -696,6 +701,9 @@ WorkerdApiIsolate::Global WorkerdApiIsolate::Global::clone() const { KJ_CASE_ONEOF(hyperdrive, Global::Hyperdrive) { result.value = hyperdrive.clone(); } + KJ_CASE_ONEOF(unsafe, Global::UnsafeEval) { + result.value = Global::UnsafeEval {}; + } } return result; diff --git a/src/workerd/server/workerd-api.h b/src/workerd/server/workerd-api.h index 4de815a80ca..e2ebbd856bf 100644 --- a/src/workerd/server/workerd-api.h +++ b/src/workerd/server/workerd-api.h @@ -169,10 +169,11 @@ class WorkerdApiIsolate final: public Worker::ApiIsolate { }; } }; + struct UnsafeEval {}; kj::String name; kj::OneOf, Wrapped, - AnalyticsEngine, Hyperdrive> value; + AnalyticsEngine, Hyperdrive, UnsafeEval> value; Global clone() const; }; diff --git a/src/workerd/server/workerd.capnp b/src/workerd/server/workerd.capnp index e656f0c4b4b..03176a092af 100644 --- a/src/workerd/server/workerd.capnp +++ b/src/workerd/server/workerd.capnp @@ -366,6 +366,9 @@ struct Worker { # A binding for Hyperdrive. Allows workers to use Hyperdrive caching & pooling for Postgres # databases. + unsafeEval @23 :Void; + # A simple binding that enables access to the UnsafeEval API. + # TODO(someday): dispatch, other new features }