Skip to content

Latest commit

 

History

History

node-module-sandbox

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

@bestest/node-module-sandbox

This module is trying to separate node modules creation in selected context, without touching i.e. regular require cache.

Warning: Remember, you should not treat this as a safe sandbox solution! It is also changing some default behaviors, so you may treat is as a buggy as well.

How to use it

Execute code (with sandboxed modules cache)

// fn.js
exports.fn = () => 3

// main.js
const { NodeModuleSandbox } = require('@bestest/node-module-sandbox')

const sandbox1 = new NodeModuleSandbox()
const sandbox2 = new NodeModuleSandbox()

// Modify fn.js module in "sandbox1"
sandbox1.executeScript('require("./fn").fn = () => 5')

// Verify that './fn' has been modified in "sandbox1"
console.log(sandbox1.executeScript('require("./fn").fn()')) // 5

// Verify that './fn' has not been modified in "sandbox2"
console.log(sandbox2.executeScript('require("./fn").fn()')) // 3

// Verify that './fn' has not been modified locally
console.log(require('./fn').fn()) // 3

Passing arguments (or callback)

const { NodeModuleSandbox } = require('@bestest/node-module-sandbox')

const sandbox = new NodeModuleSandbox()

// Remember that this function will be serialized,
// so you should not use anything from outside of its context.
const fn = (callback) => {
  setTimeout(() => {
    callback(10)    
  }, 1000)
}

// Remember that this function will be serialized,
// so you should not use anything from outside of its context.
const fn2 = (x, y, z) => {
    console.log('Sum:', x + y + z)
}

sandbox.executeScriptWithArguments(fn, value => {
  console.log('The value is', value)
})

sandbox.executeScriptWithArguments(fn2, 1, 2, 3)

Passing custom file system

const { FileSystem } = require('@bestest/fs')
const { NodeModuleSandbox } = require('@bestest/node-module-sandbox')

const fileSystem = new FileSystem()

fileSystem.setLocalFile('./fixture.json', '"text"')

fileSystem.setLocalFile('./example.js', `
  module.exports = {
    root: "/root/"
  }
`)

fileSystem.setLocalFile('./a.js', `
  // File system can be mocked as well
  const fs = require('fs')
  const fixture = JSON.parse(fs.readFileSync('./fixture.json'))

  console.log(require('./example').root + fixture) // "/root/text"
`)

const sandbox = new NodeModuleSandbox({
  modules: { fs: fileSystem.fs }
})

sandbox.requireModule('./a.js', module) // logs "/root/text"
sandbox.requireModule('./a.js', module) // does nothing - this module is already resolved
sandbox.executeScript('require("./a.js")') // does nothing - this module is already resolved

Other options

  • If you would like to run code in better sandbox:
    • You may use native vm module, which will create new V8 context (but i.e. without access to Node.js modules).
  • If you would like to run code in separate thread:
    • You may think about worker threads, available since Node.js 10
    • You can run it as a separate process through child_process module
    • You may also want to take a look at cluster module

Problems

Extensions

Due to the way it is resolved, there are some differences in behavior:

  • .js and .json files will be loaded without any registered extension handlers (i.e. ts-node or @babel/register)
  • Native modules may be replaced by selected implementation
  • All other files will be loaded regularly, outside of sandbox

Global context is still available

Module context (module, require, __dirname, __filename) will be replaced, but other global objects will still be the same.

ESM imports are not modified

Unfortunately, there is no API yet available to access and modify ESMLoader.

Changelog

  • 1.1.1 (on 13.08.2019): improve list of script extensions for file resolution
  • 1.1.0 (on 13.08.2019): add option to pass arguments for executed scripts
  • 1.0.0 (on 13.08.2019): initial version