isolated-vm

repository·main·Indexed 25 days ago

https://github.com/laverdet/isolated-vm

A Node.js library providing access to V8's Isolate interface to create completely isolated JavaScript environments. It allows developers to run untrusted or resource-intensive code securely by managing separate isolates, contexts, and scripts. The library includes features for memory limiting, CPU profiling, and efficient data transfer between isolates using Reference, Callback, and ExternalCopy classes.

Tokens
5.5K
Snippets
2
Records
32
Agent score
84%

What's inside isolated-vm

  1. Security best practices for running untrusted code

    main

    Using isolated-vm does not automatically make your application safe. Follow these guidelines to mitigate risks:

    • Do not leak handles: Never pass isolated-vm objects (like Reference or ExternalCopy) to untrusted code. An attacker can use these to escape the isolate and gain control of the Node.js process.
    • Process Isolation: Keep isolated-vm instances in a separate Node.js process from your critical infrastructure.
    • Update Node.js: Keep Node.js updated to ensure you have the latest V8 security patches.
    • Resource Limits: Be aware that memoryLimit is a guideline. A determined attacker might use 2-3 times the specified limit before the script is terminated.
    • Snapshot Safety: Never accept snapshot payloads from users, as they contain compiled machine code and can allow arbitrary code execution.
  2. Install isolated-vm and meet requirements

    main

    To use isolated-vm, you must have Node.js version 16.x or later installed.

    Critical Requirement for Node.js 20.x+: You must pass the --no-node-snapshot flag to the node command when running your application.

    Compiler Requirements: Since isolated-vm is a native module, you must have a compiler installed on your system:

    • Ubuntu: sudo apt-get install python g++ build-essential
    • Alpine: sudo apk add python3 make g++
    • Amazon Linux AMI: sudo yum install gcc72 gcc72-c++
    • Arch Linux: sudo pacpac -S make gcc python
    • Red Hat: sudo dnf install python3 make gcc gcc-c++ zlib-devel brotli-devel openssl-devel
    • Windows/macOS: Follow node-gyp instructions.
  3. Guidelines for creating native modules for isolated-vm

    main

    When developing native compiled modules to be used with isolated-vm, your code must adhere to the following requirements:

    1. Isolate-aware: The module must respect the isolate boundaries.
    2. Context-aware: The module must respect the context boundaries.
    3. Thread-safe: The module must be safe for use in a multi-threaded environment.

    Recommendations:

    • Use nan (Native Abstractions for Node) as demonstrated in the native-example directory. nan functions are generally isolate and context-aware, which simplifies development.
    • Existing native modules can often be modified to support isolated-vm with minimal changes.

    Restrictions:

    • Do not use libuv in isolated-vm, except within the default isolate.
    • Asynchronous callbacks are not supported, except within the default isolate.
  4. Passing data and functions into an isolate

    main

    Isolates are strictly isolated environments with their own heap. You cannot directly pass Node.js modules, functions, or objects into an isolate.

    To achieve functionality within an isolate, use one of these patterns:

    1. Code Injection: Pass the source code of a function directly into the isolate and execute it there (similar to a <script /> tag).
    2. Shim Delegates: For operations requiring file access, network requests, or native modules, set up a shim delegate. This delegate performs the operation in the main Node.js environment and passes the result back to the isolate (similar to a REST call).
  5. Set ScriptOrigin for debugging

    main

    When compiling code, you can specify a ScriptOrigin to provide metadata for V8 debugging (stack traces and the inspector).

    Fields:

    • filename: The filename of the source code (recommended to use a valid URI scheme, e.g., file:///test.js).
    • columnOffset: Column offset of the source code.
    • lineOffset: Line offset of the source code.
  6. Configure TransferOptions for inter-isolate data movement

    main

    When moving data between isolates, you can use TransferOptions to control how values are handled. By default, only transferable values pass between isolates. Use these options to change that behavior:

    • copy: Automatically deep copy the value.
    • externalCopy: Automatically wrap the value in an ExternalCopy instance.
    • reference: Automatically wrap the value in a Reference instance.
    • promise: Automatically proxy any returned promises between isolates. This can be used in combination with other options.
  7. Configure Isolate options

    main

    When creating an Isolate, you can provide an options object:

    • memoryLimit (number): Memory limit in MB. Default is 128MB, minimum is 8MB. This is a guideline; attackers may exceed this by 2-3x.
    • inspector (boolean): Enable V8 inspector support.
    • snapshot (ExternalCopy[ArrayBuffer]): An optional snapshot created via createSnapshot to initialize the heap.
  8. Optimize script execution with CachedDataOptions

    main

    You can speed up parsing of the same script by producing and consuming V8 cache data. This data can be saved to disk and used in different processes.

    Options:

    • cachedData: An ExternalCopy[ArrayBuffer] containing previously compiled data. If the data is rejected by V8, cachedDataRejected will be set to true.
    • produceCachedData: If true, the returned object will have a cachedData property containing an ExternalCopy handle.

    Security Warning: cachedData contains compiled machine code. Do not accept cachedData payloads from untrusted users, as they could execute arbitrary code.

  9. Basic usage of isolated-vm

    main

    This example demonstrates how to create a new Isolate with a memory limit, create a Context within it, and bridge functions from the main Node.js environment into the isolate using derefInto() and setSync(). It also shows how to execute code via evalSync() and run() and how the memory limit is enforced.

    // Create a new isolate limited to 128MB
    const ivm = require('isolated-vm');
    const isolate = new ivm.Isolate({ memoryLimit: 128 });
    
    // Create a new context within this isolate. Each context has its own copy of all the builtin
    // Objects. So for instance if one context does Object.prototype.foo = 1 this would not affect any
    // other contexts.
    const context = isolate.createContextSync();
    
    // Get a Reference{} to the global object within the context.
    const jail = context.global;
    
    // This makes the global object available in the context as `global`. We use `derefInto()` here
    // because otherwise `global` would actually be a Reference{} object in the new isolate.
    jail.setSync('global', jail.derefInto());
    
    // We will create a basic `log` function for the new isolate to use.
    jail.setSync('log', function(...args) {
    	console.log(...args);
    });
    
    // And let's test it out:
    context.evalSync('log("hello world")');
    // > hello world
    
    // Let's see what happens when we try to blow the isolate's memory
    const hostile = isolate.compileScriptSync(`
    	const storage = [];
    	const twoMegabytes = 1024 * 1024 * 2;
    	while (true) {
    		const array = new Uint8Array(twoMegabytes);
    		for (let ii = 0; ii < twoMegabytes; ii += 4096) {
    			array[ii] = 1; // we have to put something in the array to flush to real memory
    		}
    		storage.push(array);
    		log('I\'ve wasted '+ (storage.length * 2)+ 'MB');
    	}
    `);
    
    // Using the async version of `run` so that calls to `log` will get to the main node isolate
    hostile.run(context).catch(err => console.error(err));
    // I've wasted 2MB
    // I've wasted 4MB
    // ...
    // I've wasted 130MB
    // I've wasted 132MB
    // RangeError: Array buffer allocation failed
  10. Work with JavaScript Modules

    main

    The Module class represents a JavaScript module. Modules can only run in the isolate that created them.

    Key Methods

    • module.instantiate(context, resolveCallback) / module.instantiateSync(context, resolveCallback): Instantiates the module and its dependencies. The resolveCallback must return a Module instance for each dependency.
    • module.evaluate(options) / module.evaluateSync(options): Evaluates the module and returns the last expression. Subsequent calls return the result of the first invocation.
      • options.timeout: Maximum execution time in milliseconds.
    • module.release(): Releases the module reference.

    Properties

    • module.dependencySpecifiers: A read-only array of all dependency specifiers.
    • module.namespace: A Reference containing all exported values.
  11. Manage isolates with the Isolate class

    main

    The Isolate class represents a sandboxed environment. You can create contexts, compile modules, and monitor resource usage within an isolate.

    Key Methods

    • isolate.compileModule(code) / isolate.compileModuleSync(code): Compiles JavaScript code into a Module.
      • options.meta: A callback invoked when the module first accesses import.meta. Only usable if compiling from within the same isolate.
    • isolate.createContext() / isolate.createContextSync(): Creates a new Context within the isolate.
      • options.inspector: Boolean to enable the V8 inspector for this context (must also be enabled for the isolate).
    • isolate.dispose(): Destroys the isolate and invalidates all associated references.
    • isolate.getHeapStatistics() / isolate.getHeapStatisticsSync(): Returns V8 heap statistics. Includes externally_allocated_size (memory not in the V8 heap but counting against memoryLimit).
    • isolate.startCpuProfiler(title) / isolate.stopCpuProfiler(title): Used for performance profiling.

    Monitoring Properties

    • isolate.cpuTime: Total CPU time spent in nanoseconds (as bigint).
    • isolate.wallTime: Total wall time spent in nanoseconds (as bigint).
    • isolate.isDisposed: Boolean indicating if the isolate has been destroyed.
    • isolate.referenceCount: Total count of active Reference instances belonging to this isolate.
  12. Run scripts with the Script class

    main

    A Script is a compiled chunk of JavaScript that can be executed in any context within its parent isolate.

    Key Methods

    • script.run(context, options) / script.runSync(context, options): Runs the script in the provided Context. Returns the last evaluated value if transferable.
      • options.release: If true, script.release() is automatically called after execution.
      • options.timeout: Maximum execution time in milliseconds.
    • script.release(): Releases the script reference, allowing the script data to be garbage collected. Note that previously created functions/data in the isolate remain alive.