Skip to content

Commit cdb7d2f

Browse files
TimothyGuaddaleax
authored andcommitted
src: implement query callbacks for vm
This is a re-land of a commit landed as part of nodejs#22390. --- This allows using a Proxy object as the sandbox for a VM context. Refs: nodejs#22390 Fixes: nodejs#17480 Fixes: nodejs#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 8989c76 commit cdb7d2f

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
@@ -944,6 +944,48 @@ within which it can operate. The process of creating the V8 Context and
944944
associating it with the `sandbox` object is what this document refers to as
945945
"contextifying" the `sandbox`.
946946

947+
## vm module and Proxy object
948+
949+
Leveraging a `Proxy` object as the sandbox of a VM context could result in a
950+
very powerful runtime environment that intercepts all accesses to the global
951+
object. However, there are some restrictions in the JavaScript engine that one
952+
needs to be aware of to prevent unexpected results. In particular, providing a
953+
`Proxy` object with a `get` handler could disallow any access to the original
954+
global properties of the new VM context, as the `get` hook does not distinguish
955+
between the `undefined` value and "requested property is not present" &ndash;
956+
the latter of which would ordinarily trigger a lookup on the context global
957+
object.
958+
959+
Included below is a sample for how to work around this restriction. It
960+
initializes the sandbox as a `Proxy` object without any hooks, only to add them
961+
after the relevant properties have been saved.
962+
963+
```js
964+
'use strict';
965+
const { createContext, runInContext } = require('vm');
966+
967+
function createProxySandbox(handlers) {
968+
// Create a VM context with a Proxy object with no hooks specified.
969+
const sandbox = {};
970+
const proxyHandlers = {};
971+
const contextifiedProxy = createContext(new Proxy(sandbox, proxyHandlers));
972+
973+
// Save the initial globals onto our sandbox object.
974+
const contextThis = runInContext('this', contextifiedProxy);
975+
for (const prop of Reflect.ownKeys(contextThis)) {
976+
const descriptor = Object.getOwnPropertyDescriptor(contextThis, prop);
977+
Object.defineProperty(sandbox, prop, descriptor);
978+
}
979+
980+
// Now that `sandbox` contains all the initial global properties, assign the
981+
// provided handlers to the handlers we used to create the Proxy.
982+
Object.assign(proxyHandlers, handlers);
983+
984+
// Return the created contextified Proxy object.
985+
return contextifiedProxy;
986+
}
987+
```
988+
947989
[`Error`]: errors.html#errors_class_error
948990
[`URL`]: url.html#url_class_url
949991
[`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);
@@ -391,6 +393,28 @@ void ContextifyContext::PropertySetterCallback(
391393
ctx->sandbox()->Set(property, value);
392394
}
393395

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

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

src/node_contextify.h

+6
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ class ContextifyContext {
6969
v8::Local<v8::Name> property,
7070
v8::Local<v8::Value> value,
7171
const v8::PropertyCallbackInfo<v8::Value>& args);
72+
static void PropertyQueryCallback(
73+
v8::Local<v8::Name> property,
74+
const v8::PropertyCallbackInfo<v8::Integer>& args);
7275
static void PropertyDescriptorCallback(
7376
v8::Local<v8::Name> property,
7477
const v8::PropertyCallbackInfo<v8::Value>& args);
@@ -88,6 +91,9 @@ class ContextifyContext {
8891
uint32_t index,
8992
v8::Local<v8::Value> value,
9093
const v8::PropertyCallbackInfo<v8::Value>& args);
94+
static void IndexedPropertyQueryCallback(
95+
uint32_t index,
96+
const v8::PropertyCallbackInfo<v8::Integer>& args);
9197
static void IndexedPropertyDescriptorCallback(
9298
uint32_t index,
9399
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)