src: implement query callbacks for vm
This allows using a Proxy object as the sandbox for a VM context. PR-URL: https://github.com/nodejs/node/pull/22390 Fixes: https://github.com/nodejs/node/issues/17480 Fixes: https://github.com/nodejs/node/issues/17481 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
parent
349612b233
commit
85c356c10e
@ -916,6 +916,48 @@ within which it can operate. The process of creating the V8 Context and
|
|||||||
associating it with the `sandbox` object is what this document refers to as
|
associating it with the `sandbox` object is what this document refers to as
|
||||||
"contextifying" the `sandbox`.
|
"contextifying" the `sandbox`.
|
||||||
|
|
||||||
|
## vm module and Proxy object
|
||||||
|
|
||||||
|
Leveraging a `Proxy` object as the sandbox of a VM context could result in a
|
||||||
|
very powerful runtime environment that intercepts all accesses to the global
|
||||||
|
object. However, there are some restrictions in the JavaScript engine that one
|
||||||
|
needs to be aware of to prevent unexpected results. In particular, providing a
|
||||||
|
`Proxy` object with a `get` handler could disallow any access to the original
|
||||||
|
global properties of the new VM context, as the `get` hook does not distinguish
|
||||||
|
between the `undefined` value and "requested property is not present" –
|
||||||
|
the latter of which would ordinarily trigger a lookup on the context global
|
||||||
|
object.
|
||||||
|
|
||||||
|
Included below is a sample for how to work around this restriction. It
|
||||||
|
initializes the sandbox as a `Proxy` object without any hooks, only to add them
|
||||||
|
after the relevant properties have been saved.
|
||||||
|
|
||||||
|
```js
|
||||||
|
'use strict';
|
||||||
|
const { createContext, runInContext } = require('vm');
|
||||||
|
|
||||||
|
function createProxySandbox(handlers) {
|
||||||
|
// Create a VM context with a Proxy object with no hooks specified.
|
||||||
|
const sandbox = {};
|
||||||
|
const proxyHandlers = {};
|
||||||
|
const contextifiedProxy = createContext(new Proxy(sandbox, proxyHandlers));
|
||||||
|
|
||||||
|
// Save the initial globals onto our sandbox object.
|
||||||
|
const contextThis = runInContext('this', contextifiedProxy);
|
||||||
|
for (const prop of Reflect.ownKeys(contextThis)) {
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(contextThis, prop);
|
||||||
|
Object.defineProperty(sandbox, prop, descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that `sandbox` contains all the initial global properties, assign the
|
||||||
|
// provided handlers to the handlers we used to create the Proxy.
|
||||||
|
Object.assign(proxyHandlers, handlers);
|
||||||
|
|
||||||
|
// Return the created contextified Proxy object.
|
||||||
|
return contextifiedProxy;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
[`Error`]: errors.html#errors_class_error
|
[`Error`]: errors.html#errors_class_error
|
||||||
[`URL`]: url.html#url_class_url
|
[`URL`]: url.html#url_class_url
|
||||||
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
|
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
|
||||||
|
@ -143,19 +143,21 @@ Local<Context> ContextifyContext::CreateV8Context(
|
|||||||
|
|
||||||
NamedPropertyHandlerConfiguration config(PropertyGetterCallback,
|
NamedPropertyHandlerConfiguration config(PropertyGetterCallback,
|
||||||
PropertySetterCallback,
|
PropertySetterCallback,
|
||||||
PropertyDescriptorCallback,
|
PropertyQueryCallback,
|
||||||
PropertyDeleterCallback,
|
PropertyDeleterCallback,
|
||||||
PropertyEnumeratorCallback,
|
PropertyEnumeratorCallback,
|
||||||
PropertyDefinerCallback,
|
PropertyDefinerCallback,
|
||||||
|
PropertyDescriptorCallback,
|
||||||
CreateDataWrapper(env));
|
CreateDataWrapper(env));
|
||||||
|
|
||||||
IndexedPropertyHandlerConfiguration indexed_config(
|
IndexedPropertyHandlerConfiguration indexed_config(
|
||||||
IndexedPropertyGetterCallback,
|
IndexedPropertyGetterCallback,
|
||||||
IndexedPropertySetterCallback,
|
IndexedPropertySetterCallback,
|
||||||
IndexedPropertyDescriptorCallback,
|
IndexedPropertyQueryCallback,
|
||||||
IndexedPropertyDeleterCallback,
|
IndexedPropertyDeleterCallback,
|
||||||
PropertyEnumeratorCallback,
|
PropertyEnumeratorCallback,
|
||||||
IndexedPropertyDefinerCallback,
|
IndexedPropertyDefinerCallback,
|
||||||
|
IndexedPropertyDescriptorCallback,
|
||||||
CreateDataWrapper(env));
|
CreateDataWrapper(env));
|
||||||
|
|
||||||
object_template->SetHandler(config);
|
object_template->SetHandler(config);
|
||||||
@ -390,6 +392,28 @@ void ContextifyContext::PropertySetterCallback(
|
|||||||
ctx->sandbox()->Set(property, value);
|
ctx->sandbox()->Set(property, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
void ContextifyContext::PropertyQueryCallback(
|
||||||
|
Local<Name> property,
|
||||||
|
const PropertyCallbackInfo<Integer>& args) {
|
||||||
|
ContextifyContext* ctx = ContextifyContext::Get(args);
|
||||||
|
|
||||||
|
// Still initializing
|
||||||
|
if (ctx->context_.IsEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Local<Context> context = ctx->context();
|
||||||
|
|
||||||
|
Local<Object> sandbox = ctx->sandbox();
|
||||||
|
|
||||||
|
PropertyAttribute attributes;
|
||||||
|
if (sandbox->HasOwnProperty(context, property).FromMaybe(false) &&
|
||||||
|
sandbox->GetPropertyAttributes(context, property).To(&attributes)) {
|
||||||
|
args.GetReturnValue().Set(attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// static
|
// static
|
||||||
void ContextifyContext::PropertyDescriptorCallback(
|
void ContextifyContext::PropertyDescriptorCallback(
|
||||||
Local<Name> property,
|
Local<Name> property,
|
||||||
@ -535,6 +559,20 @@ void ContextifyContext::IndexedPropertySetterCallback(
|
|||||||
Uint32ToName(ctx->context(), index), value, args);
|
Uint32ToName(ctx->context(), index), value, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
void ContextifyContext::IndexedPropertyQueryCallback(
|
||||||
|
uint32_t index,
|
||||||
|
const PropertyCallbackInfo<Integer>& args) {
|
||||||
|
ContextifyContext* ctx = ContextifyContext::Get(args);
|
||||||
|
|
||||||
|
// Still initializing
|
||||||
|
if (ctx->context_.IsEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
ContextifyContext::PropertyQueryCallback(
|
||||||
|
Uint32ToName(ctx->context(), index), args);
|
||||||
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
void ContextifyContext::IndexedPropertyDescriptorCallback(
|
void ContextifyContext::IndexedPropertyDescriptorCallback(
|
||||||
uint32_t index,
|
uint32_t index,
|
||||||
|
@ -65,6 +65,9 @@ class ContextifyContext {
|
|||||||
v8::Local<v8::Name> property,
|
v8::Local<v8::Name> property,
|
||||||
v8::Local<v8::Value> value,
|
v8::Local<v8::Value> value,
|
||||||
const v8::PropertyCallbackInfo<v8::Value>& args);
|
const v8::PropertyCallbackInfo<v8::Value>& args);
|
||||||
|
static void PropertyQueryCallback(
|
||||||
|
v8::Local<v8::Name> property,
|
||||||
|
const v8::PropertyCallbackInfo<v8::Integer>& args);
|
||||||
static void PropertyDescriptorCallback(
|
static void PropertyDescriptorCallback(
|
||||||
v8::Local<v8::Name> property,
|
v8::Local<v8::Name> property,
|
||||||
const v8::PropertyCallbackInfo<v8::Value>& args);
|
const v8::PropertyCallbackInfo<v8::Value>& args);
|
||||||
@ -84,6 +87,9 @@ class ContextifyContext {
|
|||||||
uint32_t index,
|
uint32_t index,
|
||||||
v8::Local<v8::Value> value,
|
v8::Local<v8::Value> value,
|
||||||
const v8::PropertyCallbackInfo<v8::Value>& args);
|
const v8::PropertyCallbackInfo<v8::Value>& args);
|
||||||
|
static void IndexedPropertyQueryCallback(
|
||||||
|
uint32_t index,
|
||||||
|
const v8::PropertyCallbackInfo<v8::Integer>& args);
|
||||||
static void IndexedPropertyDescriptorCallback(
|
static void IndexedPropertyDescriptorCallback(
|
||||||
uint32_t index,
|
uint32_t index,
|
||||||
const v8::PropertyCallbackInfo<v8::Value>& args);
|
const v8::PropertyCallbackInfo<v8::Value>& args);
|
||||||
|
83
test/parallel/test-vm-proxy.js
Normal file
83
test/parallel/test-vm-proxy.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
'use strict';
|
||||||
|
require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const vm = require('vm');
|
||||||
|
|
||||||
|
const sandbox = {};
|
||||||
|
const proxyHandlers = {};
|
||||||
|
const contextifiedProxy = vm.createContext(new Proxy(sandbox, proxyHandlers));
|
||||||
|
|
||||||
|
// One must get the globals and manually assign it to our own global object, to
|
||||||
|
// mitigate against https://github.com/nodejs/node/issues/17465.
|
||||||
|
const contextThis = vm.runInContext('this', contextifiedProxy);
|
||||||
|
for (const prop of Reflect.ownKeys(contextThis)) {
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(contextThis, prop);
|
||||||
|
Object.defineProperty(sandbox, prop, descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, activate the proxy.
|
||||||
|
const numCalled = {};
|
||||||
|
for (const hook of Reflect.ownKeys(Reflect)) {
|
||||||
|
numCalled[hook] = 0;
|
||||||
|
proxyHandlers[hook] = (...args) => {
|
||||||
|
numCalled[hook]++;
|
||||||
|
return Reflect[hook](...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Make sure the `in` operator only calls `getOwnPropertyDescriptor` and not
|
||||||
|
// `get`.
|
||||||
|
// Refs: https://github.com/nodejs/node/issues/17480
|
||||||
|
assert.strictEqual(vm.runInContext('"a" in this', contextifiedProxy), false);
|
||||||
|
assert.deepStrictEqual(numCalled, {
|
||||||
|
defineProperty: 0,
|
||||||
|
deleteProperty: 0,
|
||||||
|
apply: 0,
|
||||||
|
construct: 0,
|
||||||
|
get: 0,
|
||||||
|
getOwnPropertyDescriptor: 1,
|
||||||
|
getPrototypeOf: 0,
|
||||||
|
has: 0,
|
||||||
|
isExtensible: 0,
|
||||||
|
ownKeys: 0,
|
||||||
|
preventExtensions: 0,
|
||||||
|
set: 0,
|
||||||
|
setPrototypeOf: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Make sure `Object.getOwnPropertyDescriptor` only calls
|
||||||
|
// `getOwnPropertyDescriptor` and not `get`.
|
||||||
|
// Refs: https://github.com/nodejs/node/issues/17481
|
||||||
|
|
||||||
|
// Get and store the function in a lexically scoped variable to avoid
|
||||||
|
// interfering with the actual test.
|
||||||
|
vm.runInContext(
|
||||||
|
'const { getOwnPropertyDescriptor } = Object;',
|
||||||
|
contextifiedProxy);
|
||||||
|
|
||||||
|
for (const p of Reflect.ownKeys(numCalled)) {
|
||||||
|
numCalled[p] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
vm.runInContext('getOwnPropertyDescriptor(this, "a")', contextifiedProxy),
|
||||||
|
undefined);
|
||||||
|
assert.deepStrictEqual(numCalled, {
|
||||||
|
defineProperty: 0,
|
||||||
|
deleteProperty: 0,
|
||||||
|
apply: 0,
|
||||||
|
construct: 0,
|
||||||
|
get: 0,
|
||||||
|
getOwnPropertyDescriptor: 1,
|
||||||
|
getPrototypeOf: 0,
|
||||||
|
has: 0,
|
||||||
|
isExtensible: 0,
|
||||||
|
ownKeys: 0,
|
||||||
|
preventExtensions: 0,
|
||||||
|
set: 0,
|
||||||
|
setPrototypeOf: 0
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user