diff --git a/README.md b/README.md index e11d9bd..3eac28c 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/index.d.ts b/index.d.ts index ba2cb05..80e68c3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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 @@ -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; } /** diff --git a/lib/filesystem.js b/lib/filesystem.js new file mode 100644 index 0000000..4337374 --- /dev/null +++ b/lib/filesystem.js @@ -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; diff --git a/lib/main.js b/lib/main.js index ae78444..de5ffc9 100644 --- a/lib/main.js +++ b/lib/main.js @@ -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; diff --git a/lib/resolver-compat.js b/lib/resolver-compat.js index 9c74ea0..c903118 100644 --- a/lib/resolver-compat.js +++ b/lib/resolver-compat.js @@ -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'); @@ -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. @@ -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(); @@ -264,7 +264,9 @@ 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'); }); @@ -272,7 +274,7 @@ 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 { @@ -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 { @@ -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; @@ -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; diff --git a/lib/resolver.js b/lib/resolver.js index 8ff5eb8..d492413 100644 --- a/lib/resolver.js +++ b/lib/resolver.js @@ -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'); @@ -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; @@ -35,7 +33,7 @@ class Resolver { } pathResolve(path) { - return pa.resolve(path); + return this.fs.resolve(path); } pathIsRelative(path) { @@ -43,23 +41,23 @@ class Resolver { 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) { @@ -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; @@ -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; @@ -166,7 +164,7 @@ 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; @@ -174,7 +172,7 @@ class DefaultResolver extends Resolver { } readFile(path) { - return fs.readFileSync(path, {encoding: 'utf8'}); + return this.fs.readFileSync(path, {encoding: 'utf8'}); } readFileWhenExists(path) { diff --git a/lib/setup-node-sandbox.js b/lib/setup-node-sandbox.js index 8d63c2c..05f38ef 100644 --- a/lib/setup-node-sandbox.js +++ b/lib/setup-node-sandbox.js @@ -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); @@ -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];