Skip to content

Commit

Permalink
Implement an UnsafeEval binding
Browse files Browse the repository at this point in the history
This adds a new `UnsafeEval` binding APi that is intended only for
use with workerd and only when the `--experimental` flag is specified.
See the included test for an example on how to use it.
  • Loading branch information
jasnell committed Oct 24, 2023
1 parent 8871567 commit e034369
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 1 deletion.
34 changes: 34 additions & 0 deletions src/workerd/api/tests/unsafe-test.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
18 changes: 18 additions & 0 deletions src/workerd/api/tests/unsafe-test.wd-test
Original file line number Diff line number Diff line change
@@ -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 = true)
]
)
),
],
);
46 changes: 46 additions & 0 deletions src/workerd/api/unsafe.c++
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#include "unsafe.h"

namespace workerd::api {

namespace {
static constexpr auto EVAL_STR = "eval"_kjc;
inline kj::StringPtr getName(jsg::Optional<kj::String>& 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<kj::String> 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<kj::String> name,
jsg::Arguments<jsg::JsRef<jsg::JsString>> args,
const jsg::TypeHandler<UnsafeEvalFunction>& 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<v8::String>(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
48 changes: 48 additions & 0 deletions src/workerd/api/unsafe.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#pragma once

#include <workerd/jsg/jsg.h>

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<kj::String> name);

using UnsafeEvalFunction = jsg::Function<jsg::Value(jsg::Arguments<jsg::Value>)>;

// 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<kj::String> name,
jsg::Arguments<jsg::JsRef<jsg::JsString>> args,
const jsg::TypeHandler<UnsafeEvalFunction>& handler);

JSG_RESOURCE_TYPE(UnsafeEval) {
JSG_METHOD(eval);
JSG_METHOD(newFunction);
}
};

#define EW_UNSAFE_ISOLATE_TYPES \
api::UnsafeEval

} // namespace workerd::api
6 changes: 6 additions & 0 deletions src/workerd/jsg/modules.c++
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ v8::Local<v8::Value> CommonJsModuleContext::require(jsg::Lock& js, kj::String sp
}
}

v8::Local<v8::Value> NonModuleScript::runAndReturn(v8::Local<v8::Context> context) const {
auto isolate = context->GetIsolate();
auto boundScript = unboundScript.Get(isolate)->BindToCurrentContext();
return check(boundScript->Run(context));
}

void NonModuleScript::run(v8::Local<v8::Context> context) const {
auto isolate = context->GetIsolate();
auto boundScript = unboundScript.Get(isolate)->BindToCurrentContext();
Expand Down
2 changes: 2 additions & 0 deletions src/workerd/jsg/modules.h
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ class NonModuleScript {
// context then will run it to completion.
void run(v8::Local<v8::Context> context) const;

v8::Local<v8::Value> runAndReturn(v8::Local<v8::Context> context) const;

static jsg::NonModuleScript compile(kj::StringPtr code, jsg::Lock& js, kj::StringPtr name = "worker.js");

private:
Expand Down
8 changes: 8 additions & 0 deletions src/workerd/server/server.c++
Original file line number Diff line number Diff line change
Expand Up @@ -2415,6 +2415,14 @@ static kj::Maybe<WorkerdApiIsolate::Global> 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 "
Expand Down
8 changes: 8 additions & 0 deletions src/workerd/server/workerd-api.c++
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <workerd/api/r2.h>
#include <workerd/api/r2-admin.h>
#include <workerd/api/trace.h>
#include <workerd/api/unsafe.h>
#include <workerd/api/urlpattern.h>
#include <workerd/api/node/node.h>
#include <workerd/io/promise-wrapper.h>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -623,6 +625,9 @@ static v8::Local<v8::Value> 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<api::UnsafeEval>());
}
}

return value;
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/workerd/server/workerd-api.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,11 @@ class WorkerdApiIsolate final: public Worker::ApiIsolate {
};
}
};
struct UnsafeEval {};
kj::String name;
kj::OneOf<Json, Fetcher, KvNamespace, R2Bucket, R2Admin, CryptoKey, EphemeralActorNamespace,
DurableActorNamespace, QueueBinding, kj::String, kj::Array<byte>, Wrapped,
AnalyticsEngine, Hyperdrive> value;
AnalyticsEngine, Hyperdrive, UnsafeEval> value;

Global clone() const;
};
Expand Down
3 changes: 3 additions & 0 deletions src/workerd/server/workerd.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,9 @@ struct Worker {
# A binding for Hyperdrive. Allows workers to use Hyperdrive caching & pooling for Postgres
# databases.

unsafeEval @23 :Bool;
# A simple binding that enables access to the UnsafeEval API.

# TODO(someday): dispatch, other new features
}

Expand Down

0 comments on commit e034369

Please sign in to comment.