Skip to content

Commit

Permalink
Merge pull request #489 from XmiliaH/add-filesystem-api
Browse files Browse the repository at this point in the history
Added custom file system API
  • Loading branch information
XmiliaH authored Nov 29, 2022
2 parents ffa9398 + f2e935a commit 81f625d
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 29 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ Unlike `VM`, `NodeVM` allows you to require modules in the same way that you wou
* `require.resolve` - An additional lookup function in case a module wasn't found in one of the traditional node lookup paths.
* `require.customRequire` - Use instead of the `require` function to load modules from the host.
* `require.strict` - `false` to not force strict mode on modules loaded by require (default: `true`).
* `require.fs` - Custom file system implementation.
* `nesting` - **WARNING**: Allowing this is a security risk as scripts can create a NodeVM which can require any host module. `true` to enable VMs nesting (default: `false`).
* `wrapper` - `commonjs` (default) to wrap script into CommonJS wrapper, `none` to retrieve value returned by the script.
* `argv` - Array to be passed to `process.argv`.
Expand Down
49 changes: 49 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,51 @@
import {EventEmitter} from 'events';
import fs from 'fs';
import pa from 'path';

/**
* Interface for nodes fs module
*/
export interface VMFS {
/** Implements fs.statSync */
statSync: typeof fs.statSync;
/** Implements fs.readFileSync */
readFileSync: typeof fs.readFileSync;
}

/**
* Interface for nodes path module
*/
export interface VMPath {
/** Implements path.resolve */
resolve: typeof pa.resolve;
/** Implements path.isAbsolute */
isAbsolute: typeof pa.isAbsolute;
/** Implements path.join */
join: typeof pa.join;
/** Implements path.basename */
basename: typeof pa.basename;
/** Implements path.dirname */
dirname: typeof pa.dirname;
/** Implements fs.statSync */
statSync: typeof fs.statSync;
/** Implements fs.readFileSync */
readFileSync: typeof fs.readFileSync;
}

/**
* Custom file system which abstracts functions from node's fs and path modules.
*/
export interface VMFileSystemInterface implements VMFS, VMPath {
/** Implements (sep) => sep === path.sep */
isSeparator(char: string): boolean;
}

/**
* Implementation of a default file system.
*/
export class VMFileSystem implements VMFileSystemInterface {
constructor(options?: {fs?: VMFS, path?: VMPath});
}

/**
* Require options for a VM
Expand Down Expand Up @@ -26,6 +73,8 @@ export interface VMRequire {
customRequire?: (id: string) => any;
/** Load modules in strict mode. (default: true) */
strict?: boolean;
/** FileSystem to load files from */
fs?: VMFileSystemInterface;
}

/**
Expand Down
84 changes: 84 additions & 0 deletions lib/filesystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use strict';

const pa = require('path');
const fs = require('fs');

class DefaultFileSystem {

resolve(path) {
return pa.resolve(path);
}

isSeparator(char) {
return char === '/' || char === pa.sep;
}

isAbsolute(path) {
return pa.isAbsolute(path);
}

join(...paths) {
return pa.join(...paths);
}

basename(path) {
return pa.basename(path);
}

dirname(path) {
return pa.dirname(path);
}

statSync(path, options) {
return fs.statSync(path, options);
}

readFileSync(path, options) {
return fs.readFileSync(path, options);
}

}

class VMFileSystem {

constructor({fs: fsModule = fs, path: pathModule = pa} = {}) {
this.fs = fsModule;
this.path = pathModule;
}

resolve(path) {
return this.path.resolve(path);
}

isSeparator(char) {
return char === '/' || char === this.path.sep;
}

isAbsolute(path) {
return this.path.isAbsolute(path);
}

join(...paths) {
return this.path.join(...paths);
}

basename(path) {
return this.path.basename(path);
}

dirname(path) {
return this.path.dirname(path);
}

statSync(path, options) {
return this.fs.statSync(path, options);
}

readFileSync(path, options) {
return this.fs.readFileSync(path, options);
}

}

exports.DefaultFileSystem = DefaultFileSystem;
exports.VMFileSystem = VMFileSystem;
4 changes: 4 additions & 0 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ const {
const {
NodeVM
} = require('./nodevm');
const {
VMFileSystem
} = require('./filesystem');

exports.VMError = VMError;
exports.VMScript = VMScript;
exports.NodeVM = NodeVM;
exports.VM = VM;
exports.VMFileSystem = VMFileSystem;
26 changes: 14 additions & 12 deletions lib/resolver-compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// Translate the old options to the new Resolver functionality.

const fs = require('fs');
const pa = require('path');
const nmod = require('module');
const {EventEmitter} = require('events');
const util = require('util');
Expand All @@ -15,6 +14,7 @@ const {
const {VMScript} = require('./script');
const {VM} = require('./vm');
const {VMError} = require('./bridge');
const {DefaultFileSystem} = require('./filesystem');

/**
* Require wrapper to be able to annotate require with webpackIgnore.
Expand Down Expand Up @@ -46,8 +46,8 @@ function makeExternalMatcher(obj) {

class LegacyResolver extends DefaultResolver {

constructor(builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict, externals, allowTransitive) {
super(builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict);
constructor(fileSystem, builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict, externals, allowTransitive) {
super(fileSystem, builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict);
this.externals = externals;
this.currMod = undefined;
this.trustedMods = new WeakMap();
Expand Down Expand Up @@ -264,15 +264,17 @@ function defaultCustomResolver() {
return undefined;
}

const DENY_RESOLVER = new Resolver({__proto__: null}, [], id => {
const DEFAULT_FS = new DefaultFileSystem();

const DENY_RESOLVER = new Resolver(DEFAULT_FS, {__proto__: null}, [], id => {
throw new VMError(`Access denied to require '${id}'`, 'EDENIED');
});

function resolverFromOptions(vm, options, override, compiler) {
if (!options) {
if (!override) return DENY_RESOLVER;
const builtins = genBuiltinsFromOptions(vm, undefined, undefined, override);
return new Resolver(builtins, [], defaultRequire);
return new Resolver(DEFAULT_FS, builtins, [], defaultRequire);
}

const {
Expand All @@ -284,22 +286,22 @@ function resolverFromOptions(vm, options, override, compiler) {
customRequire: hostRequire = defaultRequire,
context = 'host',
strict = true,
fs: fsOpt = DEFAULT_FS,
} = options;

const builtins = genBuiltinsFromOptions(vm, builtinOpt, mockOpt, override);

if (!externalOpt) return new Resolver(builtins, [], hostRequire);
if (!externalOpt) return new Resolver(fsOpt, builtins, [], hostRequire);

let checkPath;
if (rootPaths) {
const checkedRootPaths = (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => pa.resolve(f));
const checkedRootPaths = (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f));
checkPath = (filename) => {
return checkedRootPaths.some(path => {
if (!filename.startsWith(path)) return false;
const len = path.length;
if (filename.length === len || (len > 0 && path[len-1] === pa.sep)) return true;
const sep = filename[len];
return sep === '/' || sep === pa.sep;
if (filename.length === len || (len > 0 && fsOpt.isSeparator(path[len-1]))) return true;
return fsOpt.isSeparator(filename[len]);
});
};
} else {
Expand All @@ -326,7 +328,7 @@ function resolverFromOptions(vm, options, override, compiler) {
}

if (typeof externalOpt !== 'object') {
return new DefaultResolver(builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict);
return new DefaultResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict);
}

let transitive = false;
Expand All @@ -337,7 +339,7 @@ function resolverFromOptions(vm, options, override, compiler) {
transitive = context === 'sandbox' && externalOpt.transitive;
}
externals = external.map(makeExternalMatcher);
return new LegacyResolver(builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict, externals, transitive);
return new LegacyResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict, externals, transitive);
}

exports.resolverFromOptions = resolverFromOptions;
28 changes: 13 additions & 15 deletions lib/resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

// The Resolver is currently experimental and might be exposed to users in the future.

const pa = require('path');
const fs = require('fs');

const {
VMError
} = require('./bridge');
Expand All @@ -24,7 +21,8 @@ function isArrayIndex(key) {

class Resolver {

constructor(builtinModules, globalPaths, hostRequire) {
constructor(fs, builtinModules, globalPaths, hostRequire) {
this.fs = fs;
this.builtinModules = builtinModules;
this.globalPaths = globalPaths;
this.hostRequire = hostRequire;
Expand All @@ -35,31 +33,31 @@ class Resolver {
}

pathResolve(path) {
return pa.resolve(path);
return this.fs.resolve(path);
}

pathIsRelative(path) {
if (path === '' || path[0] !== '.') return false;
if (path.length === 1) return true;
const idx = path[1] === '.' ? 2 : 1;
if (path.length <= idx) return false;
return path[idx] === '/' || path[idx] === pa.sep;
return this.fs.isSeparator(path[idx]);
}

pathIsAbsolute(path) {
return pa.isAbsolute(path);
return path !== '' && (this.fs.isSeparator(path[0]) || this.fs.isAbsolute(path));
}

pathConcat(...paths) {
return pa.join(...paths);
return this.fs.join(...paths);
}

pathBasename(path) {
return pa.basename(path);
return this.fs.basename(path);
}

pathDirname(path) {
return pa.dirname(path);
return this.fs.dirname(path);
}

lookupPaths(mod, id) {
Expand Down Expand Up @@ -140,8 +138,8 @@ class Resolver {

class DefaultResolver extends Resolver {

constructor(builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict) {
super(builtinModules, globalPaths, hostRequire);
constructor(fs, builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict) {
super(fs, builtinModules, globalPaths, hostRequire);
this.checkPath = checkPath;
this.pathContext = pathContext;
this.customResolver = customResolver;
Expand All @@ -157,7 +155,7 @@ class DefaultResolver extends Resolver {

pathTestIsDirectory(path) {
try {
const stat = fs.statSync(path, {__proto__: null, throwIfNoEntry: false});
const stat = this.fs.statSync(path, {__proto__: null, throwIfNoEntry: false});
return stat && stat.isDirectory();
} catch (e) {
return false;
Expand All @@ -166,15 +164,15 @@ class DefaultResolver extends Resolver {

pathTestIsFile(path) {
try {
const stat = fs.statSync(path, {__proto__: null, throwIfNoEntry: false});
const stat = this.fs.statSync(path, {__proto__: null, throwIfNoEntry: false});
return stat && stat.isFile();
} catch (e) {
return false;
}
}

readFile(path) {
return fs.readFileSync(path, {encoding: 'utf8'});
return this.fs.readFileSync(path, {encoding: 'utf8'});
}

readFileWhenExists(path) {
Expand Down
4 changes: 2 additions & 2 deletions lib/setup-node-sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ function requireImpl(mod, id, direct) {
return nmod;
}

const path = resolver.pathDirname(filename);
const path = resolver.fs.dirname(filename);
const module = new Module(filename, path, mod);
resolver.registerModule(module, filename, path, mod, direct);
mod._updateChildren(module, true);
Expand Down Expand Up @@ -146,7 +146,7 @@ Module._cache = {__proto__: null};
}

function findBestExtensionHandler(filename) {
const name = resolver.pathBasename(filename);
const name = resolver.fs.basename(filename);
for (let i = 0; (i = localStringPrototypeIndexOf(name, '.', i + 1)) !== -1;) {
const ext = localStringPrototypeSlice(name, i);
const handler = Module._extensions[ext];
Expand Down

0 comments on commit 81f625d

Please sign in to comment.