Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolver API #519

Merged
merged 4 commits into from May 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Expand Up @@ -134,7 +134,7 @@ Unlike `VM`, `NodeVM` allows you to require modules in the same way that you wou
* `eval` - If set to `false` any calls to `eval` or function constructors (`Function`, `GeneratorFunction`, etc.) will throw an `EvalError` (default: `true`).
* `wasm` - If set to `false` any attempt to compile a WebAssembly module will throw a `WebAssembly.CompileError` (default: `true`).
* `sourceExtensions` - Array of file extensions to treat as source code (default: `['js']`).
* `require` - `true` or object to enable `require` method (default: `false`).
* `require` - `true`, an object or a Resolver to enable `require` method (default: `false`).
* `require.external` - Values can be `true`, an array of allowed external modules, or an object (default: `false`). All paths matching `/node_modules/${any_allowed_external_module}/(?!/node_modules/)` are allowed to be required.
* `require.external.modules` - Array of allowed external modules. Also supports wildcards, so specifying `['@scope/*-ver-??]`, for instance, will allow using all modules having a name of the form `@scope/something-ver-aa`, `@scope/other-ver-11`, etc. The `*` wildcard does not match path separators.
* `require.external.transitive` - Boolean which indicates if transitive dependencies of external modules are allowed (default: `false`). **WARNING**: When a module is required transitively, any module is then able to require it normally, even if this was not possible before it was loaded.
Expand Down Expand Up @@ -211,6 +211,28 @@ const script = new VMScript('require("foobar")', {filename: '/data/myvmscript.js
vm.run(script);
```

### Resolver

A resolver can be created via `makeResolverFromLegacyOptions` and be used for multiple `NodeVM` instances allowing to share compiled module code potentially speeding up load times. The first example of `NodeVM` can be rewritten using `makeResolverFromLegacyOptions` as follows.

```js
const resolver = makeResolverFromLegacyOptions({
external: true,
builtin: ['fs', 'path'],
root: './',
mock: {
fs: {
readFileSync: () => 'Nice try!'
}
}
});
const vm = new NodeVM({
console: 'inherit',
sandbox: {},
require: resolver
});
```

## VMScript

You can increase performance by using precompiled scripts. The precompiled VMScript can be run multiple times. It is important to note that the code is not bound to any VM (context); rather, it is bound before each run, just for that run.
Expand Down
54 changes: 44 additions & 10 deletions index.d.ts
Expand Up @@ -59,6 +59,24 @@ export class VMFileSystem implements VMFileSystemInterface {
isSeparator(char: string): boolean;
}

/**
* Function that will be called to load a built-in into a vm.
*/
export type BuiltinLoad = (vm: NodeVM) => any;
/**
* Either a function that will be called to load a built-in into a vm or an object with a init method and a load method to load the built-in.
*/
export type Builtin = BuiltinLoad | {init: (vm: NodeVM)=>void, load: BuiltinLoad};
/**
* Require method
*/
export type HostRequire = (id: string) => any;

/**
* This callback will be called to specify the context to use "per" module. Defaults to 'sandbox' if no return value provided.
*/
export type PathContextCallback = (modulePath: string, extensionType: string) => 'host' | 'sandbox';

/**
* Require options for a VM
*/
Expand All @@ -67,24 +85,25 @@ export interface VMRequire {
* Array of allowed built-in modules, accepts ["*"] for all. Using "*" increases the attack surface and potential
* new modules allow to escape the sandbox. (default: none)
*/
builtin?: string[];
builtin?: readonly string[];
/*
* `host` (default) to require modules in host and proxy them to sandbox. `sandbox` to load, compile and
* require modules in sandbox. Built-in modules except `events` always required in host and proxied to sandbox
* require modules in sandbox or a callback which chooses the context based on the filename.
* Built-in modules except `events` always required in host and proxied to sandbox
*/
context?: "host" | "sandbox";
context?: "host" | "sandbox" | PathContextCallback;
/** `true`, an array of allowed external modules or an object with external options (default: `false`) */
external?: boolean | string[] | { modules: string[], transitive: boolean };
external?: boolean | readonly string[] | { modules: readonly string[], transitive: boolean };
/** Array of modules to be loaded into NodeVM on start. */
import?: string[];
import?: readonly string[];
/** Restricted path(s) where local modules can be required (default: every path). */
root?: string | string[];
root?: string | readonly string[];
/** Collection of mock modules (both external or built-in). */
mock?: any;
/* An additional lookup function in case a module wasn't found in one of the traditional node lookup paths. */
resolve?: (moduleName: string, parentDirname: string) => string | { path: string, module?: string } | undefined;
/** Custom require to require host and built-in modules. */
customRequire?: (id: string) => any;
customRequire?: HostRequire;
/** Load modules in strict mode. (default: true) */
strict?: boolean;
/** FileSystem to load files from */
Expand All @@ -97,6 +116,19 @@ export interface VMRequire {
*/
export type CompilerFunction = (code: string, filename: string) => string;

export abstract class Resolver {
private constructor(fs: VMFileSystemInterface, globalPaths: readonly string[], builtins: Map<string, Builtin>);
}

/**
* Create a resolver as normal `NodeVM` does given `VMRequire` options.
*
* @param options The options that would have been given to `NodeVM`.
* @param override Custom overrides for built-ins.
* @param compiler Compiler to be used for loaded modules.
*/
export function makeResolverFromLegacyOptions(options: VMRequire, override?: {[key: string]: Builtin}, compiler?: CompilerFunction): Resolver;

/**
* Options for creating a VM
*/
Expand All @@ -109,7 +141,7 @@ export interface VMOptions {
/** VM's global object. */
sandbox?: any;
/**
* Script timeout in milliseconds. Timeout is only effective on code you run through `run`.
* Script timeout in milliseconds. Timeout is only effective on code you run through `run`.
* Timeout is NOT effective on any method returned by VM.
*/
timeout?: number;
Expand Down Expand Up @@ -141,7 +173,7 @@ export interface NodeVMOptions extends VMOptions {
/** `inherit` to enable console, `redirect` to redirect to events, `off` to disable console (default: `inherit`). */
console?: "inherit" | "redirect" | "off";
/** `true` or an object to enable `require` options (default: `false`). */
require?: boolean | VMRequire;
require?: boolean | VMRequire | Resolver;
/**
* **WARNING**: This should be disabled. It allows to create a NodeVM form within the sandbox which could return any host module.
* `true` to enable VMs nesting (default: `false`).
Expand All @@ -150,7 +182,7 @@ export interface NodeVMOptions extends VMOptions {
/** `commonjs` (default) to wrap script into CommonJS wrapper, `none` to retrieve value returned by the script. */
wrapper?: "commonjs" | "none";
/** File extensions that the internal module resolver should accept. */
sourceExtensions?: string[];
sourceExtensions?: readonly string[];
/**
* Array of arguments passed to `process.argv`.
* This object will not be copied and the script can change this object.
Expand Down Expand Up @@ -224,6 +256,8 @@ export class NodeVM extends EventEmitter implements VM {
readonly sandbox: any;
/** Only here because of implements VM. Does nothing. */
timeout?: number;
/** The resolver used to resolve modules */
readonly resolver: Resolver;
/** Runs the code */
run(js: string | VMScript, options?: string | { filename?: string, wrapper?: "commonjs" | "none", strict?: boolean }): any;
/** Runs the code in the specific file */
Expand Down
147 changes: 147 additions & 0 deletions lib/builtin.js
@@ -0,0 +1,147 @@

const fs = require('fs');
const nmod = require('module');
const {EventEmitter} = require('events');
const util = require('util');
const {VMScript} = require('./script');
const {VM} = require('./vm');

const eventsModules = new WeakMap();

function defaultBuiltinLoaderEvents(vm) {
return eventsModules.get(vm);
}

let cacheBufferScript;

function defaultBuiltinLoaderBuffer(vm) {
if (!cacheBufferScript) {
cacheBufferScript = new VMScript('return buffer=>({Buffer: buffer});', {__proto__: null, filename: 'buffer.js'});
}
const makeBuffer = vm.run(cacheBufferScript, {__proto__: null, strict: true, wrapper: 'none'});
return makeBuffer(Buffer);
}

let cacheUtilScript;

function defaultBuiltinLoaderUtil(vm) {
if (!cacheUtilScript) {
cacheUtilScript = new VMScript(`return function inherits(ctor, superCtor) {
ctor.super_ = superCtor;
Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
}`, {__proto__: null, filename: 'util.js'});
}
const inherits = vm.run(cacheUtilScript, {__proto__: null, strict: true, wrapper: 'none'});
const copy = Object.assign({}, util);
copy.inherits = inherits;
return vm.readonly(copy);
}

const BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives'))).filter(s=>!s.startsWith('internal/'));

let EventEmitterReferencingAsyncResourceClass = null;
if (EventEmitter.EventEmitterAsyncResource) {
// eslint-disable-next-line global-require
const {AsyncResource} = require('async_hooks');
const kEventEmitter = Symbol('kEventEmitter');
class EventEmitterReferencingAsyncResource extends AsyncResource {
constructor(ee, type, options) {
super(type, options);
this[kEventEmitter] = ee;
}
get eventEmitter() {
return this[kEventEmitter];
}
}
EventEmitterReferencingAsyncResourceClass = EventEmitterReferencingAsyncResource;
}

let cacheEventsScript;

const SPECIAL_MODULES = {
events: {
init(vm) {
if (!cacheEventsScript) {
const eventsSource = fs.readFileSync(`${__dirname}/events.js`, 'utf8');
cacheEventsScript = new VMScript(`(function (fromhost) { const module = {}; module.exports={};{ ${eventsSource}
} return module.exports;})`, {filename: 'events.js'});
}
const closure = VM.prototype.run.call(vm, cacheEventsScript);
const eventsInstance = closure(vm.readonly({
kErrorMonitor: EventEmitter.errorMonitor,
once: EventEmitter.once,
on: EventEmitter.on,
getEventListeners: EventEmitter.getEventListeners,
EventEmitterReferencingAsyncResource: EventEmitterReferencingAsyncResourceClass
}));
eventsModules.set(vm, eventsInstance);
vm._addProtoMapping(EventEmitter.prototype, eventsInstance.EventEmitter.prototype);
},
load: defaultBuiltinLoaderEvents
},
buffer: defaultBuiltinLoaderBuffer,
util: defaultBuiltinLoaderUtil
};

function addDefaultBuiltin(builtins, key, hostRequire) {
if (builtins.has(key)) return;
const special = SPECIAL_MODULES[key];
builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key)));
}


function makeBuiltinsFromLegacyOptions(builtins, hostRequire, mocks, overrides) {
const res = new Map();
if (mocks) {
const keys = Object.getOwnPropertyNames(mocks);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
res.set(key, (tvm) => tvm.readonly(mocks[key]));
}
}
if (overrides) {
const keys = Object.getOwnPropertyNames(overrides);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
res.set(key, overrides[key]);
}
}
if (Array.isArray(builtins)) {
const def = builtins.indexOf('*') >= 0;
if (def) {
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
const name = BUILTIN_MODULES[i];
if (builtins.indexOf(`-${name}`) === -1) {
addDefaultBuiltin(res, name, hostRequire);
}
}
} else {
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
const name = BUILTIN_MODULES[i];
if (builtins.indexOf(name) !== -1) {
addDefaultBuiltin(res, name, hostRequire);
}
}
}
} else if (builtins) {
for (let i = 0; i < BUILTIN_MODULES.length; i++) {
const name = BUILTIN_MODULES[i];
if (builtins[name]) {
addDefaultBuiltin(res, name, hostRequire);
}
}
}
return res;
}

function makeBuiltins(builtins, hostRequire) {
const res = new Map();
for (let i = 0; i < builtins.length; i++) {
const name = builtins[i];
addDefaultBuiltin(res, name, hostRequire);
}
return res;
}

exports.makeBuiltinsFromLegacyOptions = makeBuiltinsFromLegacyOptions;
exports.makeBuiltins = makeBuiltins;
8 changes: 8 additions & 0 deletions lib/main.js
Expand Up @@ -15,9 +15,17 @@ const {
const {
VMFileSystem
} = require('./filesystem');
const {
Resolver
} = require('./resolver');
const {
makeResolverFromLegacyOptions
} = require('./resolver-compat');

exports.VMError = VMError;
exports.VMScript = VMScript;
exports.NodeVM = NodeVM;
exports.VM = VM;
exports.VMFileSystem = VMFileSystem;
exports.Resolver = Resolver;
exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions;