isolated-function
v0.1.49
Published
Runs untrusted code in a Node.js v8 sandbox.
Maintainers
Readme
Install
npm install isolated-function --saveQuickstart
isolated-function is a modern solution for running untrusted code in Node.js.
const isolatedFunction = require('isolated-function')
/* create an isolated-function, with resources limitation */
const [sum, teardown] = isolatedFunction((y, z) => y + z, {
memory: 128, // in MB
timeout: 10000 // in milliseconds
})
/* interact with the isolated-function */
const { value, profiling } = await sum(3, 2)
/* close resources associated with the isolated-function initialization */
await teardown()Minimal privilege execution
The hosted code runs in a separate process, with minimal privilege, using Node.js permission model API.
const [fn, teardown] = isolatedFunction(() => {
const fs = require('fs')
fs.writeFileSync('/etc/passwd', 'foo')
})
await fn()
// => PermissionError: Access to 'FileSystemWrite' has been restricted.If you exceed your limit, an error will occur. Any of the following interaction will throw an error:
- Native modules
- Child process
- Worker Threads
- Inspector protocol
- File system access
- WASI
Granting specific permissions
You can grant specific permissions to the isolated function using the allow.permissions option:
const [fn, teardown] = isolatedFunction(
() => {
const { execSync } = require('child_process')
return execSync('echo hello').toString().trim()
},
{
allow: { permissions: ['child-process'] }
}
)
const { value } = await fn()
console.log(value) // 'hello'
await teardown()See #allow.permissions to know more.
Auto install dependencies
The hosted code is parsed for detecting require/import calls and install these dependencies:
const [isEmoji, teardown] = isolatedFunction(input => {
/* this dependency only exists inside the isolated function */
const isEmoji = require('[email protected]') // default is latest
return isEmoji(input)
})
await isEmoji('🙌') // => true
await isEmoji('foo') // => false
await teardown()The dependencies, along with the hosted code, are bundled by esbuild into a single file that will be evaluated at runtime.
Restricting allowed dependencies
When running untrusted code, you should restrict which npm packages can be installed to prevent supply chain attacks:
const [fn, teardown] = isolatedFunction(
input => {
const isEmoji = require('is-standard-emoji')
return isEmoji(input)
},
{
allow: { dependencies: ['is-standard-emoji', 'lodash'] }
}
)
await fn('🙌') // => true
await teardown()If the code tries to require a package not in the allowed list, a DependencyUnallowedError is thrown before any npm install happens:
const [fn, teardown] = isolatedFunction(
() => {
const malicious = require('malicious-package')
return malicious()
},
{
allow: { dependencies: ['lodash'] }
}
)
await fn()
// => DependencyUnallowedError: Dependency 'malicious-package' is not in the allowed listSecurity Note: Even with the sandbox, arbitrary package installation is dangerous because npm packages can execute code during installation via
preinstall/postinstallscripts. The--ignore-scriptsflag is used to mitigate this, but providing anallow.dependencieswhitelist is the recommended approach for running untrusted code.
Execution profiling
Any hosted code execution will be run in their own separate process:
/** make a function to consume ~128MB */
const [fn, teardown] = isolatedFunction(() => {
const storage = []
const oneMegabyte = 1024 * 1024
while (storage.length < 78) {
const array = new Uint8Array(oneMegabyte)
for (let ii = 0; ii < oneMegabyte; ii += 4096) {
array[ii] = 1
}
storage.push(array)
}
})
t.teardown(cleanup)
const { value, profiling } = await fn()
console.log(profiling)
// {
// memory: 128204800,
// duration: 54.98325
// }Each execution has a profiling, which helps understand what happened.
Resource limits
You can limit a isolated-function by memory:
const [fn, teardown] = isolatedFunction(() => {
const storage = []
const oneMegabyte = 1024 * 1024
while (storage.length < 78) {
const array = new Uint8Array(oneMegabyte)
for (let ii = 0; ii < oneMegabyte; ii += 4096) {
array[ii] = 1
}
storage.push(array)
}
}, { memory: 64 })
await fn()
// => MemoryError: Out of memoryor by execution duration:
const [fn, teardown] = isolatedFunction(() => {
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
await delay(duration)
return 'done'
}, { timeout: 50 })
await fn(100)
// => TimeoutError: Execution timed outLogging
The logs are collected into a logging object returned after the execution:
const [fn, teardown] = isolatedFunction(() => {
console.log('console.log')
console.info('console.info')
console.debug('console.debug')
console.warn('console.warn')
console.error('console.error')
return 'done'
})
const { logging } await fn()
console.log(logging)
// {
// log: ['console.log'],
// info: ['console.info'],
// debug: ['console.debug'],
// warn: ['console.warn'],
// error: ['console.error']
// }Error handling
Any error during isolated-function execution will be propagated:
const [fn, cleanup] = isolatedFunction(() => {
throw new TypeError('oh no!')
})
const result = await fn()
// TypeError: oh no!You can also return the error instead of throwing it with { throwError: false }:
const [fn, cleanup] = isolatedFunction(() => {
throw new TypeError('oh no!')
})
const { isFullfiled, value } = await fn()
if (!isFufilled) {
console.error(value)
// TypeError: oh no!
}API
isolatedFunction(code, [options])
code
Required
Type: function
The hosted function to run.
options
memory
Type: number
Default: Infinity
Set the function memory limit, in megabytes.
throwError
Type: boolean
Default: false
When is true, it returns the error rather than throw it.
The error will be accessible against { value: error, isFufilled: false } object.
Set the function memory limit, in megabytes.
timeout
Type: number
Default: Infinity
Timeout after a specified amount of time, in milliseconds.
tmpdir
Type: function
It setup the temporal folder to be used for installing code dependencies.
The default implementation is:
const tmpdir = async () => {
const cwd = await fs.mkdtemp(path.join(require('os').tmpdir(), 'compile-'))
await fs.mkdir(cwd, { recursive: true })
const cleanup = () => fs.rm(cwd, { recursive: true, force: true })
return { cwd, cleanup }
}allow
Type: object
Default: {}
Configuration object for allowed permissions and dependencies.
const [fn, cleanup] = isolatedFunction(
() => {
const { execSync } = require('child_process')
const lodash = require('lodash')
return lodash.uniq([1, 2, 2, 3])
},
{
allow: {
permissions: ['child-process'],
dependencies: ['lodash']
}
}
)permissions
Type: string[]
Default: []
An array of permissions to grant to the isolated function based on Node.js Options
When empty, the function runs with minimal privileges and will throw an error if it attempts to access restricted resources. Available permissions are:
addonschild-processfs-readfs-writeinspectornetwasiworker
Example:
const [fn, cleanup] = isolatedFunction(
async () => {
const http = require('node:http')
// Network request code here
},
{
allow: { permissions: ['net'] }
}
)dependencies
Type: string[]
Default: undefined
A whitelist of npm package names that are allowed to be installed. When provided, only packages in this list can be required/imported by the isolated function.
This is a critical security feature when running untrusted code, as it prevents arbitrary package installation which could lead to remote code execution via malicious packages.
const [fn, cleanup] = isolatedFunction(
() => {
const lodash = require('lodash')
const axios = require('axios')
return lodash.get({ a: 1 }, 'a')
},
{
allow: { dependencies: ['lodash', 'axios'] }
}
)When allow.dependencies is not provided, any package can be installed (default behavior for backwards compatibility).
=> (fn([...args]), teardown())
fn
Type: function
The isolated function to execute. You can pass arguments over it.
teardown
Type: function
A function to be called to release resources associated with the isolated-function.
Environment Variables
ISOLATED_FUNCTIONS_MINIFY
Default: true
When is false, it disabled minify the compiled code.
DEBUG
Pass DEBUG=isolated-function for enabling debug timing output.
License
isolated-function © Kiko Beats, released under the MIT License. Authored and maintained by Kiko Beats with help from contributors.
kikobeats.com · GitHub @Kiko Beats · X @Kikobeats
