Skip to content

Commit cb5f358

Browse files
committed
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). PR-URL: #19016 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
1 parent a03c90b commit cb5f358

File tree

7 files changed

+204
-2
lines changed

7 files changed

+204
-2
lines changed

doc/api/vm.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,14 @@ added: v0.3.1
495495
value of the [`url.origin`][] property of a [`URL`][] object. Most notably,
496496
this string should omit the trailing slash, as that denotes a path.
497497
**Default:** `''`.
498+
* `contextCodeGeneration` {Object}
499+
* `strings` {boolean} If set to false any calls to `eval` or function
500+
constructors (`Function`, `GeneratorFunction`, etc) will throw an
501+
`EvalError`.
502+
**Default**: `true`.
503+
* `wasm` {boolean} If set to false any attempt to compile a WebAssembly
504+
module will throw a `WebAssembly.CompileError`.
505+
**Default**: `true`.
498506

499507
First contextifies the given `sandbox`, runs the compiled code contained by
500508
the `vm.Script` object within the created sandbox, and returns the result.
@@ -578,6 +586,14 @@ added: v0.3.1
578586
the [`url.origin`][] property of a [`URL`][] object. Most notably, this
579587
string should omit the trailing slash, as that denotes a path.
580588
**Default:** `''`.
589+
* `codeGeneration` {Object}
590+
* `strings` {boolean} If set to false any calls to `eval` or function
591+
constructors (`Function`, `GeneratorFunction`, etc) will throw an
592+
`EvalError`.
593+
**Default**: `true`.
594+
* `wasm` {boolean} If set to false any attempt to compile a WebAssembly
595+
module will throw a `WebAssembly.CompileError`.
596+
**Default**: `true`.
581597

582598
If given a `sandbox` object, the `vm.createContext()` method will [prepare
583599
that sandbox][contextified] so that it can be used in calls to

lib/vm.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,36 @@ function validateString(prop, propName) {
8484
throw new ERR_INVALID_ARG_TYPE(propName, 'string', prop);
8585
}
8686

87+
function validateBool(prop, propName) {
88+
if (prop !== undefined && typeof prop !== 'boolean')
89+
throw new ERR_INVALID_ARG_TYPE(propName, 'boolean', prop);
90+
}
91+
92+
function validateObject(prop, propName) {
93+
if (prop !== undefined && (typeof prop !== 'object' || prop === null))
94+
throw new ERR_INVALID_ARG_TYPE(propName, 'Object', prop);
95+
}
96+
8797
function getContextOptions(options) {
8898
if (options) {
99+
validateObject(options.contextCodeGeneration,
100+
'options.contextCodeGeneration');
89101
const contextOptions = {
90102
name: options.contextName,
91-
origin: options.contextOrigin
103+
origin: options.contextOrigin,
104+
codeGeneration: typeof options.contextCodeGeneration === 'object' ? {
105+
strings: options.contextCodeGeneration.strings,
106+
wasm: options.contextCodeGeneration.wasm,
107+
} : undefined,
92108
};
93109
validateString(contextOptions.name, 'options.contextName');
94110
validateString(contextOptions.origin, 'options.contextOrigin');
111+
if (contextOptions.codeGeneration) {
112+
validateBool(contextOptions.codeGeneration.strings,
113+
'options.contextCodeGeneration.strings');
114+
validateBool(contextOptions.codeGeneration.wasm,
115+
'options.contextCodeGeneration.wasm');
116+
}
95117
return contextOptions;
96118
}
97119
return {};
@@ -109,10 +131,21 @@ function createContext(sandbox, options) {
109131
if (typeof options !== 'object' || options === null) {
110132
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
111133
}
134+
validateObject(options.codeGeneration, 'options.codeGeneration');
112135
options = {
113136
name: options.name,
114-
origin: options.origin
137+
origin: options.origin,
138+
codeGeneration: typeof options.codeGeneration === 'object' ? {
139+
strings: options.codeGeneration.strings,
140+
wasm: options.codeGeneration.wasm,
141+
} : undefined,
115142
};
143+
if (options.codeGeneration !== undefined) {
144+
validateBool(options.codeGeneration.strings,
145+
'options.codeGeneration.strings');
146+
validateBool(options.codeGeneration.wasm,
147+
'options.codeGeneration.wasm');
148+
}
116149
if (options.name === undefined) {
117150
options.name = `VM Context ${defaultContextNameIndex++}`;
118151
} else if (typeof options.name !== 'string') {

src/node.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "node_revert.h"
2929
#include "node_debug_options.h"
3030
#include "node_perf.h"
31+
#include "node_context_data.h"
3132

3233
#if defined HAVE_PERFCTR
3334
#include "node_counters.h"
@@ -4432,6 +4433,8 @@ Local<Context> NewContext(Isolate* isolate,
44324433
HandleScope handle_scope(isolate);
44334434
auto intl_key = FIXED_ONE_BYTE_STRING(isolate, "Intl");
44344435
auto break_iter_key = FIXED_ONE_BYTE_STRING(isolate, "v8BreakIterator");
4436+
context->SetEmbedderData(
4437+
ContextEmbedderIndex::kAllowWasmCodeGeneration, True(isolate));
44354438
Local<Value> intl_v;
44364439
if (context->Global()->Get(context, intl_key).ToLocal(&intl_v) &&
44374440
intl_v->IsObject()) {
@@ -4509,6 +4512,13 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data,
45094512
return exit_code;
45104513
}
45114514

4515+
bool AllowWasmCodeGenerationCallback(
4516+
Local<Context> context, Local<String>) {
4517+
Local<Value> wasm_code_gen =
4518+
context->GetEmbedderData(ContextEmbedderIndex::kAllowWasmCodeGeneration);
4519+
return wasm_code_gen->IsUndefined() || wasm_code_gen->IsTrue();
4520+
}
4521+
45124522
inline int Start(uv_loop_t* event_loop,
45134523
int argc, const char* const* argv,
45144524
int exec_argc, const char* const* exec_argv) {
@@ -4527,6 +4537,7 @@ inline int Start(uv_loop_t* event_loop,
45274537
isolate->SetAbortOnUncaughtExceptionCallback(ShouldAbortOnUncaughtException);
45284538
isolate->SetMicrotasksPolicy(v8::MicrotasksPolicy::kExplicit);
45294539
isolate->SetFatalErrorHandler(OnFatalError);
4540+
isolate->SetAllowWasmCodeGenerationCallback(AllowWasmCodeGenerationCallback);
45304541

45314542
{
45324543
Mutex::ScopedLock scoped_lock(node_isolate_mutex);

src/node_context_data.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ namespace node {
1515
#define NODE_CONTEXT_SANDBOX_OBJECT_INDEX 33
1616
#endif
1717

18+
#ifndef NODE_CONTEXT_ALLOW_WASM_CODE_GENERATION_INDEX
19+
#define NODE_CONTEXT_ALLOW_WASM_CODE_GENERATION_INDEX 34
20+
#endif
21+
1822
enum ContextEmbedderIndex {
1923
kEnvironment = NODE_CONTEXT_EMBEDDER_DATA_INDEX,
2024
kSandboxObject = NODE_CONTEXT_SANDBOX_OBJECT_INDEX,
25+
kAllowWasmCodeGeneration = NODE_CONTEXT_ALLOW_WASM_CODE_GENERATION_INDEX,
2126
};
2227

2328
} // namespace node

src/node_contextify.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,30 @@ Local<Context> ContextifyContext::CreateV8Context(
185185
CHECK(name->IsString());
186186
Utf8Value name_val(env->isolate(), name);
187187

188+
Local<Value> codegen = options_obj->Get(env->context(),
189+
FIXED_ONE_BYTE_STRING(env->isolate(), "codeGeneration"))
190+
.ToLocalChecked();
191+
192+
if (!codegen->IsUndefined()) {
193+
CHECK(codegen->IsObject());
194+
Local<Object> codegen_obj = codegen.As<Object>();
195+
196+
Local<Value> allow_code_gen_from_strings =
197+
codegen_obj->Get(env->context(),
198+
FIXED_ONE_BYTE_STRING(env->isolate(), "strings"))
199+
.ToLocalChecked();
200+
ctx->AllowCodeGenerationFromStrings(
201+
allow_code_gen_from_strings->IsUndefined() ||
202+
allow_code_gen_from_strings->IsTrue());
203+
204+
Local<Value> allow_wasm_code_gen = codegen_obj->Get(env->context(),
205+
FIXED_ONE_BYTE_STRING(env->isolate(), "wasm"))
206+
.ToLocalChecked();
207+
ctx->SetEmbedderData(ContextEmbedderIndex::kAllowWasmCodeGeneration,
208+
Boolean::New(env->isolate(), allow_wasm_code_gen->IsUndefined() ||
209+
allow_wasm_code_gen->IsTrue()));
210+
}
211+
188212
ContextInfo info(*name_val);
189213

190214
Local<Value> origin =

src/node_contextify.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class ContextifyContext {
2424
v8::Local<v8::Object> sandbox_obj, v8::Local<v8::Object> options_obj);
2525
static void Init(Environment* env, v8::Local<v8::Object> target);
2626

27+
static bool AllowWasmCodeGeneration(
28+
v8::Local<v8::Context> context, v8::Local<v8::String>);
29+
2730
static ContextifyContext* ContextFromContextifiedSandbox(
2831
Environment* env,
2932
const v8::Local<v8::Object>& sandbox);

test/parallel/test-vm-codegen.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
6+
const { createContext, runInContext, runInNewContext } = require('vm');
7+
8+
const WASM_BYTES = Buffer.from(
9+
[0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
10+
11+
12+
function expectsError(fn, type) {
13+
try {
14+
fn();
15+
assert.fail('expected fn to error');
16+
} catch (err) {
17+
if (typeof type === 'string')
18+
assert.strictEqual(err.name, type);
19+
else
20+
assert(err instanceof type);
21+
}
22+
}
23+
24+
{
25+
const ctx = createContext({ WASM_BYTES });
26+
const test = 'eval(""); new WebAssembly.Module(WASM_BYTES);';
27+
runInContext(test, ctx);
28+
29+
runInNewContext(test, { WASM_BYTES }, {
30+
contextCodeGeneration: undefined,
31+
});
32+
}
33+
34+
{
35+
const ctx = createContext({}, {
36+
codeGeneration: {
37+
strings: false,
38+
},
39+
});
40+
41+
const EvalError = runInContext('EvalError', ctx);
42+
expectsError(() => {
43+
runInContext('eval("x")', ctx);
44+
}, EvalError);
45+
}
46+
47+
{
48+
const ctx = createContext({ WASM_BYTES }, {
49+
codeGeneration: {
50+
wasm: false,
51+
},
52+
});
53+
54+
const CompileError = runInContext('WebAssembly.CompileError', ctx);
55+
expectsError(() => {
56+
runInContext('new WebAssembly.Module(WASM_BYTES)', ctx);
57+
}, CompileError);
58+
}
59+
60+
expectsError(() => {
61+
runInNewContext('eval("x")', {}, {
62+
contextCodeGeneration: {
63+
strings: false,
64+
},
65+
});
66+
}, 'EvalError');
67+
68+
expectsError(() => {
69+
runInNewContext('new WebAssembly.Module(WASM_BYTES)', { WASM_BYTES }, {
70+
contextCodeGeneration: {
71+
wasm: false,
72+
},
73+
});
74+
}, 'CompileError');
75+
76+
common.expectsError(() => {
77+
createContext({}, {
78+
codeGeneration: {
79+
strings: 0,
80+
},
81+
});
82+
}, {
83+
code: 'ERR_INVALID_ARG_TYPE',
84+
});
85+
86+
common.expectsError(() => {
87+
runInNewContext('eval("x")', {}, {
88+
contextCodeGeneration: {
89+
wasm: 1,
90+
},
91+
});
92+
}, {
93+
code: 'ERR_INVALID_ARG_TYPE'
94+
});
95+
96+
common.expectsError(() => {
97+
createContext({}, {
98+
codeGeneration: 1,
99+
});
100+
}, {
101+
code: 'ERR_INVALID_ARG_TYPE',
102+
});
103+
104+
common.expectsError(() => {
105+
createContext({}, {
106+
codeGeneration: null,
107+
});
108+
}, {
109+
code: 'ERR_INVALID_ARG_TYPE',
110+
});

0 commit comments

Comments
 (0)