SandboxJS

repository·main·Indexed 42 days ago

https://github.com/nyariv/sandboxjs

A JavaScript sandboxing library designed to safely execute untrusted or third-party code. It protects against jailbreak techniques by parsing code into a Lisp-like structure and checking every prototype function call against a configurable whitelist. Version 0.9.7 includes features for auditing code permissions, managing multiple execution scopes, and extending the executor via custom operation callbacks.

Tokens
5.3K
Snippets
19
Records
25
Agent score
53%

What's inside SandboxJS

  1. Configure custom whitelisted globals and prototypes

    main

    You can customize the sandbox security model by providing your own prototypeWhitelist and globals.

    • Use Sandbox.SAFE_PROTOTYPES to get the default prototype whitelist (a Set).
    • Use Sandbox.SAFE_GLOBALS to get the default safe globals object.

    Example of adding alert to globals and Node to the prototype whitelist:

    const prototypeWhitelist = Sandbox.SAFE_PROTOTYPES;
    prototypeWhitelist.set(Node, new Set());
    
    const globals = {...Sandbox.SAFE_GLOBALS, alert};
    
    const sandbox = new Sandbox({globals, prototypeWhitelist});
  2. Understand the Lisp-like data structure

    main

    SandboxJS uses a Lisp-like structure to represent code internally. A Lisp object is a tuple consisting of a LispType (the operation), and two LispItems (the operands/arguments).

    Key types include:

    • LispItemSingle: Can be LispType.None, LispType.True, a string, or a Lisp object.
    • LispItem: Either a LispItemSingle or an array of LispItemSingles.
    • Lisp: A tuple [LispType, LispItem, LispItem].

    This structure allows the parser to represent complex expressions like function calls, assignments, and control flow in a predictable, tree-like format.

  3. Basic usage of SandboxJS

    main

    To execute code in a sandbox, create a new Sandbox instance, compile your code string into an executable, and then call .run() with a scope object containing the variables available to the code.

    const code = `return myTest;`;
    const scope = { myTest: "hello world" };
    const sandbox = new Sandbox();
    const exec = sandbox.compile(code);
    const result = exec(scope).run(); // result: "hello world"
  4. Execute code with multiple scopes

    main

    SandboxJS allows you to pass multiple scope objects to an executable. This is useful for managing layered permissions or reusing scopes across different execution layers.

    const sandbox = new Sandbox();
    
    const scopeA = {a: 1};
    const scopeB = {b: 2};
    const scopeC = {c: 3};
    
    const code = `a = 4; let d = 5; let b = 6`;
    const exec = sandbox.compile(code);
    exec(scopeA, scopeB, scopeC).run();
    
    console.log(scopeA); // {a: 4}
    console.log(scopeB); // {b: 2}
    console.log(scopeC); // {c: 3, d: 5, b: 6}
  5. Audit code to identify required permissions

    main

    If you are unsure which globals or prototypes a piece of code requires, use Sandbox.audit(code). This method permits all access but returns a JSON object detailing every global and prototype accessed during the execution. This is useful for determining the exact whitelist needed for a specific library.

    const code = `console.log("test")`;
    console.log(Sandbox.audit(code));
  6. Configure Sandbox options with IOptionParams

    main

    When initializing a sandbox, you can provide an options object conforming to IOptionParams to control security, execution limits, and environment.

    Key configuration options include:

    • audit: Enable to generate an IAuditReport tracking access to globals and prototypes.
    • forbidFunctionCalls / forbidFunctionCreation: Restrict the ability to execute or define functions.
    • prototypeWhitelist: A Map<Function, Set<string>> defining which properties are allowed on specific prototypes.
    • globals: An IGlobals object (mapping strings to values) to define the sandboxed global environment.
    • symbolWhitelist: An ISymbolWhitelist to control access to specific symbols.
    • executionQuota: A bigint representing the maximum allowed execution ticks.
    • nonBlocking: If true, the sandbox will use non-blocking execution patterns.
    • haltOnSandboxError: Determines if the sandbox should stop immediately upon encountering a sandbox-specific error.
    • maxParserRecursionDepth: Limits the depth of the parser to prevent stack overflow attacks.
    • functionReplacements: A Map<Function, (ctx: IContext) => Function> used to inject custom logic when sandboxed code accesses specific native functions. The factory is called once per context and the result is cached.
    const options: IOptionParams = {
      audit: true,
      forbidFunctionCalls: false,
      forbidFunctionCreation: true,
      prototypeWhitelist: new Map(),
      globals: { console: { log: () => {} } },
      symbolWhitelist: {},
      executionQuota: 1000000n,
      nonBlocking: true,
      haltOnSandboxError: true,
      maxParserRecursionDepth: 100,
      functionReplacements: new Map()
    };
  7. Reference: Safe Prototypes in SandboxJS

    main

    These are the default prototypes allowed within the sandbox.

    - SandboxGlobal
    - Function
    - Boolean
    - Object
    - Number
    - BigInt
    - String
    - Date
    - RegExp
    - Error
    - Array
    - Int8Array
    - Uint8Array
    - Uint8ClampedArray
    - Int16Array
    - Uint16Array
    - Int32Array
    - Uint32Array
    - Float32Array
    - Float64Array
    - Map
    - Set
    - WeakMap
    - WeakSet
    - Promise
  8. Reference: Safe Globals in SandboxJS

    main

    These are the default global properties available in the sandbox. Note that setTimeout, setInterval, clearTimeout, and clearInterval are excluded by default.

    - globalThis
    - Function
    - eval
    - setTimeout (excluded)
    - setInterval (excluded)
    - clearTimeout (excluded)
    - clearInterval (excluded)
    - console
    - isFinite
    - isNaN
    - parseFloat
    - parseInt
    - decodeURI
    - decodeURIComponent
    - encodeURI
    - encodeURIComponent
    - escape
    - unescape
    - Boolean
    - Number
    - BigInt
    - String
    - Object
    - Array
    - Symbol
    - Error
    - EvalError
    - RangeError
    - ReferenceError
    - SyntaxError
    - TypeError
    - URIError
    - Int8Array
    - Uint8Array
    - Uint8ClampedArray
    - Int16Array
    - Uint16Array
    - Int32Array
    - Uint32Array
    - Float32Array
    - Float64Array
    - Map
    - Set
    - WeakMap
    - WeakSet
    - Promise
    - Intl
    - JSON
    - Math
  9. Implement a LispCallback

    main

    The LispCallback<T> type is used to define callback functions that react to specific parts of the Lisp tree during parsing or execution. The callback receives a context object containing depth information, constants, and the current state of the parse.

    type LispCallback<T> = (ctx: LispCallbackCtx & { type: T }) => any;

    LispCallbackCtx includes:

    • generatorDepth: Current generator depth.
    • asyncDepth: Current async depth.
    • lispDepth: Current Lisp tree depth.
    • constants: The IConstants object.
    • type: The specific type being processed.
    • part: The CodeString being processed.
    • res: An array of strings (results).
    • expect: The expected value/token.
    • lispTree: The current Lisp node.
  10. Compile code for execution with `compile` and `compileAsync`

    main

    The Sandbox class provides several compilation methods to transform code strings into executable functions. These methods return a function that accepts one or more IScope objects and returns an object containing the execution context and a run method.

    • compile<T>(code, optimize?): Returns a synchronous runner. Note: This will throw a SandboxCapabilityError if the sandbox is configured with nonBlocking: true.
    • compileAsync<T>(code, optimize?): Returns an asynchronous runner that returns a Promise<T>.

    Use optimize: true to enable optimizations during the parsing phase.

    // Synchronous compilation
    const runner = sandbox.compile('const x = 1; x + 2;');
    const { context, run } = runner([myScope]);
    const result = run(); // returns 3
    
    // Asynchronous compilation
    const asyncRunner = sandbox.compileAsync('await Promise.resolve(true)');
    const { run: runAsync } = asyncRunner([myScope]);
    const resultAsync = await runAsync(); // returns true
  11. Register custom operations with addOps

    main

    You can extend the executor's capabilities by registering custom operation callbacks using the addOps function. This allows you to define how specific LispType values are handled during execution.

    To register an operation, provide the LispType and a callback function. The callback receives an OpsCallbackParams object containing the execution context, the current Lisp item (tree), the scope, and control flow information.

    import { addOps } from '@nyariv/sandboxjs';
    
    // Example: Registering a custom operation for a specific LispType
    addOps(myCustomType, ({ op, exec, a, b, obj, bobj, ticks, tree, scope, context, done, statementLabels, internal, generatorYield }) => {
      // Implement custom execution logic here
      // Use 'exec' to continue execution or 'done' to signal completion
    });