Skip to content

Commit 85c356c

Browse files
committed
src: implement query callbacks for vm
This allows using a Proxy object as the sandbox for a VM context. PR-URL: #22390 Fixes: #17480 Fixes: #17481 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 349612b commit 85c356c

File tree

4 files changed

+171
-2
lines changed

4 files changed

+171
-2
lines changed

doc/api/vm.md

+42
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,48 @@ within which it can operate. The process of creating the V8 Context and
916916
associating it with the `sandbox` object is what this document refers to as
917917
"contextifying" the `sandbox`.
918918

919+
## vm module and Proxy object
920+
921+
Leveraging a `Proxy` object as the sandbox of a VM context could result in a
922+
very powerful runtime environment that intercepts all accesses to the global
923+
object. However, there are some restrictions in the JavaScript engine that one
924+
needs to be aware of to prevent unexpected results. In particular, providing a
925+
`Proxy` object with a `get` handler could disallow any access to the original
926+
global properties of the new VM context, as the `get` hook does not distinguish
927+
between the `undefined` value and "requested property is not present" &ndash;
928+
the latter of which would ordinarily trigger a lookup on the context global
929+
object.
930+
931+
Included below is a sample for how to work around this restriction. It
932+
initializes the sandbox as a `Proxy` object without any hooks, only to add them
933+
after the relevant properties have been saved.
934+
935+
```js
936+
'use strict';
937+
const { createContext, runInContext } = require('vm');
938+
939+
function createProxySandbox(handlers) {
940+
// Create a VM context with a Proxy object with no hooks specified.
941+
const sandbox = {};
942+
const proxyHandlers = {};
943+
const contextifiedProxy = createContext(new Proxy(sandbox, proxyHandlers));
944+
945+
// Save the initial globals onto our sandbox object.
946+
const contextThis = runInContext('this', contextifiedProxy);
947+
for (const prop of Reflect.ownKeys(contextThis)) {
948+
const descriptor = Object.getOwnPropertyDescriptor(contextThis, prop);
949+
Object.defineProperty(sandbox, prop, descriptor);
950+
}
951+
952+
// Now that `sandbox` contains all the initial global properties, assign the
953+
// provided handlers to the handlers we used to create the Proxy.
954+
Object.assign(proxyHandlers, handlers);
955+
956+
// Return the created contextified Proxy object.
957+
return contextifiedProxy;
958+
}
959+
```
960+
919961
[`Error`]: errors.html#errors_class_error
920962
[`URL`]: url.html#url_class_url
921963
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval

src/node_contextify.cc

+40-2
Original file line numberDiff line numberDiff line change
@@ -143,19 +143,21 @@ Local<Context> ContextifyContext::CreateV8Context(
143143

144144
NamedPropertyHandlerConfiguration config(PropertyGetterCallback,
145145
PropertySetterCallback,
146-
PropertyDescriptorCallback,
146+
PropertyQueryCallback,
147147
PropertyDeleterCallback,
148148
PropertyEnumeratorCallback,
149149
PropertyDefinerCallback,
150+
PropertyDescriptorCallback,
150151
CreateDataWrapper(env));
151152

152153
IndexedPropertyHandlerConfiguration indexed_config(
153154
IndexedPropertyGetterCallback,
154155
IndexedPropertySetterCallback,
155-
IndexedPropertyDescriptorCallback,
156+
IndexedPropertyQueryCallback,
156157
IndexedPropertyDeleterCallback,
157158
PropertyEnumeratorCallback,
158159
IndexedPropertyDefinerCallback,
160+
IndexedPropertyDescriptorCallback,
159161
CreateDataWrapper(env));
160162

161163
object_template->SetHandler(config);
@@ -390,6 +392,28 @@ void ContextifyContext::PropertySetterCallback(
390392
ctx->sandbox()->Set(property, value);
391393
}
392394

395+
// static
396+
void ContextifyContext::PropertyQueryCallback(
397+
Local<Name> property,
398+
const PropertyCallbackInfo<Integer>& args) {
399+
ContextifyContext* ctx = ContextifyContext::Get(args);
400+
401+
// Still initializing
402+
if (ctx->context_.IsEmpty())
403+
return;
404+
405+
Local<Context> context = ctx->context();
406+
407+
Local<Object> sandbox = ctx->sandbox();
408+
409+
PropertyAttribute attributes;
410+
if (sandbox->HasOwnProperty(context, property).FromMaybe(false) &&
411+
sandbox->GetPropertyAttributes(context, property).To(&attributes)) {
412+
args.GetReturnValue().Set(attributes);
413+
}
414+
}
415+
416+
393417
// static
394418
void ContextifyContext::PropertyDescriptorCallback(
395419
Local<Name> property,
@@ -535,6 +559,20 @@ void ContextifyContext::IndexedPropertySetterCallback(
535559
Uint32ToName(ctx->context(), index), value, args);
536560
}
537561

562+
// static
563+
void ContextifyContext::IndexedPropertyQueryCallback(
564+
uint32_t index,
565+
const PropertyCallbackInfo<Integer>& args) {
566+
ContextifyContext* ctx = ContextifyContext::Get(args);
567+
568+
// Still initializing
569+
if (ctx->context_.IsEmpty())
570+
return;
571+
572+
ContextifyContext::PropertyQueryCallback(
573+
Uint32ToName(ctx->context(), index), args);
574+
}
575+
538576
// static
539577
void ContextifyContext::IndexedPropertyDescriptorCallback(
540578
uint32_t index,

src/node_contextify.h

+6
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ class ContextifyContext {
6565
v8::Local<v8::Name> property,
6666
v8::Local<v8::Value> value,
6767
const v8::PropertyCallbackInfo<v8::Value>& args);
68+
static void PropertyQueryCallback(
69+
v8::Local<v8::Name> property,
70+
const v8::PropertyCallbackInfo<v8::Integer>& args);
6871
static void PropertyDescriptorCallback(
6972
v8::Local<v8::Name> property,
7073
const v8::PropertyCallbackInfo<v8::Value>& args);
@@ -84,6 +87,9 @@ class ContextifyContext {
8487
uint32_t index,
8588
v8::Local<v8::Value> value,
8689
const v8::PropertyCallbackInfo<v8::Value>& args);
90+
static void IndexedPropertyQueryCallback(
91+
uint32_t index,
92+
const v8::PropertyCallbackInfo<v8::Integer>& args);
8793
static void IndexedPropertyDescriptorCallback(
8894
uint32_t index,
8995
const v8::PropertyCallbackInfo<v8::Value>& args);

test/parallel/test-vm-proxy.js

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use strict';
2+
require('../common');
3+
const assert = require('assert');
4+
const vm = require('vm');
5+
6+
const sandbox = {};
7+
const proxyHandlers = {};
8+
const contextifiedProxy = vm.createContext(new Proxy(sandbox, proxyHandlers));
9+
10+
// One must get the globals and manually assign it to our own global object, to
11+
// mitigate against https://github.com/nodejs/node/issues/17465.
12+
const contextThis = vm.runInContext('this', contextifiedProxy);
13+
for (const prop of Reflect.ownKeys(contextThis)) {
14+
const descriptor = Object.getOwnPropertyDescriptor(contextThis, prop);
15+
Object.defineProperty(sandbox, prop, descriptor);
16+
}
17+
18+
// Finally, activate the proxy.
19+
const numCalled = {};
20+
for (const hook of Reflect.ownKeys(Reflect)) {
21+
numCalled[hook] = 0;
22+
proxyHandlers[hook] = (...args) => {
23+
numCalled[hook]++;
24+
return Reflect[hook](...args);
25+
};
26+
}
27+
28+
{
29+
// Make sure the `in` operator only calls `getOwnPropertyDescriptor` and not
30+
// `get`.
31+
// Refs: https://github.com/nodejs/node/issues/17480
32+
assert.strictEqual(vm.runInContext('"a" in this', contextifiedProxy), false);
33+
assert.deepStrictEqual(numCalled, {
34+
defineProperty: 0,
35+
deleteProperty: 0,
36+
apply: 0,
37+
construct: 0,
38+
get: 0,
39+
getOwnPropertyDescriptor: 1,
40+
getPrototypeOf: 0,
41+
has: 0,
42+
isExtensible: 0,
43+
ownKeys: 0,
44+
preventExtensions: 0,
45+
set: 0,
46+
setPrototypeOf: 0
47+
});
48+
}
49+
50+
{
51+
// Make sure `Object.getOwnPropertyDescriptor` only calls
52+
// `getOwnPropertyDescriptor` and not `get`.
53+
// Refs: https://github.com/nodejs/node/issues/17481
54+
55+
// Get and store the function in a lexically scoped variable to avoid
56+
// interfering with the actual test.
57+
vm.runInContext(
58+
'const { getOwnPropertyDescriptor } = Object;',
59+
contextifiedProxy);
60+
61+
for (const p of Reflect.ownKeys(numCalled)) {
62+
numCalled[p] = 0;
63+
}
64+
65+
assert.strictEqual(
66+
vm.runInContext('getOwnPropertyDescriptor(this, "a")', contextifiedProxy),
67+
undefined);
68+
assert.deepStrictEqual(numCalled, {
69+
defineProperty: 0,
70+
deleteProperty: 0,
71+
apply: 0,
72+
construct: 0,
73+
get: 0,
74+
getOwnPropertyDescriptor: 1,
75+
getPrototypeOf: 0,
76+
has: 0,
77+
isExtensible: 0,
78+
ownKeys: 0,
79+
preventExtensions: 0,
80+
set: 0,
81+
setPrototypeOf: 0
82+
});
83+
}

0 commit comments

Comments
 (0)