From ecc0e20cbe009a6761a1f444c39b42aa7127cb13 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 26 Feb 2018 14:29:41 -0600 Subject: [PATCH] vm: add code generation options Adds options to a VM Context to disable code generation from strings (such as eval or new Function) and WASM code generation (WebAssembly.compile). --- doc/api/vm.md | 16 +++++ lib/vm.js | 37 ++++++++++- src/node.cc | 11 ++++ src/node_context_data.h | 5 ++ src/node_contextify.cc | 24 +++++++ src/node_contextify.h | 3 + test/parallel/test-vm-codegen.js | 110 +++++++++++++++++++++++++++++++ 7 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-vm-codegen.js diff --git a/doc/api/vm.md b/doc/api/vm.md index 12e35e9f727d3e..b9ccf93b7beb67 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -495,6 +495,14 @@ added: v0.3.1 value of the [`url.origin`][] property of a [`URL`][] object. Most notably, this string should omit the trailing slash, as that denotes a path. **Default:** `''`. + * `contextCodeGeneration` {Object} + * `strings` {boolean} If set to false any calls to `eval` or function + constructors (`Function`, `GeneratorFunction`, etc) will throw an + `EvalError`. + **Default**: `true`. + * `wasm` {boolean} If set to false any attempt to compile a WebAssembly + module will throw a `WebAssembly.CompileError`. + **Default**: `true`. First contextifies the given `sandbox`, runs the compiled code contained by the `vm.Script` object within the created sandbox, and returns the result. @@ -578,6 +586,14 @@ added: v0.3.1 the [`url.origin`][] property of a [`URL`][] object. Most notably, this string should omit the trailing slash, as that denotes a path. **Default:** `''`. + * `codeGeneration` {Object} + * `strings` {boolean} If set to false any calls to `eval` or function + constructors (`Function`, `GeneratorFunction`, etc) will throw an + `EvalError`. + **Default**: `true`. + * `wasm` {boolean} If set to false any attempt to compile a WebAssembly + module will throw a `WebAssembly.CompileError`. + **Default**: `true`. If given a `sandbox` object, the `vm.createContext()` method will [prepare that sandbox][contextified] so that it can be used in calls to diff --git a/lib/vm.js b/lib/vm.js index 554aff8bfcc1b0..5266dbd29fbf8e 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -84,14 +84,36 @@ function validateString(prop, propName) { throw new ERR_INVALID_ARG_TYPE(propName, 'string', prop); } +function validateBool(prop, propName) { + if (prop !== undefined && typeof prop !== 'boolean') + throw new ERR_INVALID_ARG_TYPE(propName, 'boolean', prop); +} + +function validateObject(prop, propName) { + if (prop !== undefined && (typeof prop !== 'object' || prop === null)) + throw new ERR_INVALID_ARG_TYPE(propName, 'Object', prop); +} + function getContextOptions(options) { if (options) { + validateObject(options.contextCodeGeneration, + 'options.contextCodeGeneration'); const contextOptions = { name: options.contextName, - origin: options.contextOrigin + origin: options.contextOrigin, + codeGeneration: typeof options.contextCodeGeneration === 'object' ? { + strings: options.contextCodeGeneration.strings, + wasm: options.contextCodeGeneration.wasm, + } : undefined, }; validateString(contextOptions.name, 'options.contextName'); validateString(contextOptions.origin, 'options.contextOrigin'); + if (contextOptions.codeGeneration) { + validateBool(contextOptions.codeGeneration.strings, + 'options.contextCodeGeneration.strings'); + validateBool(contextOptions.codeGeneration.wasm, + 'options.contextCodeGeneration.wasm'); + } return contextOptions; } return {}; @@ -109,10 +131,21 @@ function createContext(sandbox, options) { if (typeof options !== 'object' || options === null) { throw new ERR_INVALID_ARG_TYPE('options', 'object', options); } + validateObject(options.codeGeneration, 'options.codeGeneration'); options = { name: options.name, - origin: options.origin + origin: options.origin, + codeGeneration: typeof options.codeGeneration === 'object' ? { + strings: options.codeGeneration.strings, + wasm: options.codeGeneration.wasm, + } : undefined, }; + if (options.codeGeneration !== undefined) { + validateBool(options.codeGeneration.strings, + 'options.codeGeneration.strings'); + validateBool(options.codeGeneration.wasm, + 'options.codeGeneration.wasm'); + } if (options.name === undefined) { options.name = `VM Context ${defaultContextNameIndex++}`; } else if (typeof options.name !== 'string') { diff --git a/src/node.cc b/src/node.cc index 682052aadf1e44..8ac86eb8ed164f 100644 --- a/src/node.cc +++ b/src/node.cc @@ -28,6 +28,7 @@ #include "node_revert.h" #include "node_debug_options.h" #include "node_perf.h" +#include "node_context_data.h" #if defined HAVE_PERFCTR #include "node_counters.h" @@ -4421,6 +4422,8 @@ Local NewContext(Isolate* isolate, HandleScope handle_scope(isolate); auto intl_key = FIXED_ONE_BYTE_STRING(isolate, "Intl"); auto break_iter_key = FIXED_ONE_BYTE_STRING(isolate, "v8BreakIterator"); + context->SetEmbedderData( + ContextEmbedderIndex::kAllowWasmCodeGeneration, True(isolate)); Local intl_v; if (context->Global()->Get(context, intl_key).ToLocal(&intl_v) && intl_v->IsObject()) { @@ -4498,6 +4501,13 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data, return exit_code; } +bool AllowWasmCodeGenerationCallback( + Local context, Local) { + Local wasm_code_gen = + context->GetEmbedderData(ContextEmbedderIndex::kAllowWasmCodeGeneration); + return wasm_code_gen->IsUndefined() || wasm_code_gen->IsTrue(); +} + inline int Start(uv_loop_t* event_loop, int argc, const char* const* argv, int exec_argc, const char* const* exec_argv) { @@ -4516,6 +4526,7 @@ inline int Start(uv_loop_t* event_loop, isolate->SetAbortOnUncaughtExceptionCallback(ShouldAbortOnUncaughtException); isolate->SetMicrotasksPolicy(v8::MicrotasksPolicy::kExplicit); isolate->SetFatalErrorHandler(OnFatalError); + isolate->SetAllowWasmCodeGenerationCallback(AllowWasmCodeGenerationCallback); { Mutex::ScopedLock scoped_lock(node_isolate_mutex); diff --git a/src/node_context_data.h b/src/node_context_data.h index 9d3145bb800b59..522ce292d21684 100644 --- a/src/node_context_data.h +++ b/src/node_context_data.h @@ -15,9 +15,14 @@ namespace node { #define NODE_CONTEXT_SANDBOX_OBJECT_INDEX 33 #endif +#ifndef NODE_CONTEXT_ALLOW_WASM_CODE_GENERATION_INDEX +#define NODE_CONTEXT_ALLOW_WASM_CODE_GENERATION_INDEX 34 +#endif + enum ContextEmbedderIndex { kEnvironment = NODE_CONTEXT_EMBEDDER_DATA_INDEX, kSandboxObject = NODE_CONTEXT_SANDBOX_OBJECT_INDEX, + kAllowWasmCodeGeneration = NODE_CONTEXT_ALLOW_WASM_CODE_GENERATION_INDEX, }; } // namespace node diff --git a/src/node_contextify.cc b/src/node_contextify.cc index d267a5180ff655..a7f46f61269e2b 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -185,6 +185,30 @@ Local ContextifyContext::CreateV8Context( CHECK(name->IsString()); Utf8Value name_val(env->isolate(), name); + Local codegen = options_obj->Get(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "codeGeneration")) + .ToLocalChecked(); + + if (!codegen->IsUndefined()) { + CHECK(codegen->IsObject()); + Local codegen_obj = codegen.As(); + + Local allow_code_gen_from_strings = + codegen_obj->Get(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "strings")) + .ToLocalChecked(); + ctx->AllowCodeGenerationFromStrings( + allow_code_gen_from_strings->IsUndefined() || + allow_code_gen_from_strings->IsTrue()); + + Local allow_wasm_code_gen = codegen_obj->Get(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "wasm")) + .ToLocalChecked(); + ctx->SetEmbedderData(ContextEmbedderIndex::kAllowWasmCodeGeneration, + Boolean::New(env->isolate(), allow_wasm_code_gen->IsUndefined() || + allow_wasm_code_gen->IsTrue())); + } + ContextInfo info(*name_val); Local origin = diff --git a/src/node_contextify.h b/src/node_contextify.h index b25e1a75d4f26c..cf3e6452fd0f7c 100644 --- a/src/node_contextify.h +++ b/src/node_contextify.h @@ -24,6 +24,9 @@ class ContextifyContext { v8::Local sandbox_obj, v8::Local options_obj); static void Init(Environment* env, v8::Local target); + static bool AllowWasmCodeGeneration( + v8::Local context, v8::Local); + static ContextifyContext* ContextFromContextifiedSandbox( Environment* env, const v8::Local& sandbox); diff --git a/test/parallel/test-vm-codegen.js b/test/parallel/test-vm-codegen.js new file mode 100644 index 00000000000000..ebafe30c076e01 --- /dev/null +++ b/test/parallel/test-vm-codegen.js @@ -0,0 +1,110 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +const { createContext, runInContext, runInNewContext } = require('vm'); + +const WASM_BYTES = Buffer.from( + [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]); + + +function expectsError(fn, type) { + try { + fn(); + assert.fail('expected fn to error'); + } catch (err) { + if (typeof type === 'string') + assert.strictEqual(err.name, type); + else + assert(err instanceof type); + } +} + +{ + const ctx = createContext({ WASM_BYTES }); + const test = 'eval(""); new WebAssembly.Module(WASM_BYTES);'; + runInContext(test, ctx); + + runInNewContext(test, { WASM_BYTES }, { + contextCodeGeneration: undefined, + }); +} + +{ + const ctx = createContext({}, { + codeGeneration: { + strings: false, + }, + }); + + const EvalError = runInContext('EvalError', ctx); + expectsError(() => { + runInContext('eval("x")', ctx); + }, EvalError); +} + +{ + const ctx = createContext({ WASM_BYTES }, { + codeGeneration: { + wasm: false, + }, + }); + + const CompileError = runInContext('WebAssembly.CompileError', ctx); + expectsError(() => { + runInContext('new WebAssembly.Module(WASM_BYTES)', ctx); + }, CompileError); +} + +expectsError(() => { + runInNewContext('eval("x")', {}, { + contextCodeGeneration: { + strings: false, + }, + }); +}, 'EvalError'); + +expectsError(() => { + runInNewContext('new WebAssembly.Module(WASM_BYTES)', { WASM_BYTES }, { + contextCodeGeneration: { + wasm: false, + }, + }); +}, 'CompileError'); + +common.expectsError(() => { + createContext({}, { + codeGeneration: { + strings: 0, + }, + }); +}, { + code: 'ERR_INVALID_ARG_TYPE', +}); + +common.expectsError(() => { + runInNewContext('eval("x")', {}, { + contextCodeGeneration: { + wasm: 1, + }, + }); +}, { + code: 'ERR_INVALID_ARG_TYPE' +}); + +common.expectsError(() => { + createContext({}, { + codeGeneration: 1, + }); +}, { + code: 'ERR_INVALID_ARG_TYPE', +}); + +common.expectsError(() => { + createContext({}, { + codeGeneration: null, + }); +}, { + code: 'ERR_INVALID_ARG_TYPE', +});