Skip to content

Commit 983b578

Browse files
TimothyGutargos
authored andcommitted
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 <[email protected]> Reviewed-By: Gus Caplan <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 221ce6c commit 983b578

File tree

4 files changed

+171
-2
lines changed

4 files changed

+171
-2
lines changed

doc/api/vm.md

Lines changed: 42 additions & 0 deletions
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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,21 @@ Local<Context> ContextifyContext::CreateV8Context(
142142

143143
NamedPropertyHandlerConfiguration config(PropertyGetterCallback,
144144
PropertySetterCallback,
145-
PropertyDescriptorCallback,
145+
PropertyQueryCallback,
146146
PropertyDeleterCallback,
147147
PropertyEnumeratorCallback,
148148
PropertyDefinerCallback,
149+
PropertyDescriptorCallback,
149150
CreateDataWrapper(env));
150151

151152
IndexedPropertyHandlerConfiguration indexed_config(
152153
IndexedPropertyGetterCallback,
153154
IndexedPropertySetterCallback,
154-
IndexedPropertyDescriptorCallback,
155+
IndexedPropertyQueryCallback,
155156
IndexedPropertyDeleterCallback,
156157
PropertyEnumeratorCallback,
157158
IndexedPropertyDefinerCallback,
159+
IndexedPropertyDescriptorCallback,
158160
CreateDataWrapper(env));
159161

160162
object_template->SetHandler(config);
@@ -389,6 +391,28 @@ void ContextifyContext::PropertySetterCallback(
389391
ctx->sandbox()->Set(property, value);
390392
}
391393

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

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

src/node_contextify.h

Lines changed: 6 additions & 0 deletions
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

Lines changed: 83 additions & 0 deletions
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)