Skip to content

ForbesLindesay/secure-javascript-sandbox

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

secure-javascript-sandbox

Secure sandbox for JavaScript plugins using Rust and Web Assembly.

Development Setup

Wasi

Many rust crates don't work out of the box with wasm32-unknown-unknown because it does not provide functionality like system time and a source of randomness. Many common libraries do conditionally support a target of wasm32-wasi though. To use the wasm32-wasi target you may first have to install it by running:

rustup target add wasm32-wasi

Wasmtime

Wasmtime is a runtime for code that has been compiled to target wasm32-wasi. We use wasmtime as a library, but you can also directly run the .wasm files if you install wasmtime by running:

curl https://wasmtime.dev/install.sh -sSf | bash

Wasm-NM

The .wasm file is a binary format. If you want to read the instructions that were generated, you can extract a text based representation by running:

cargo install wasm-nm

and then running:

wasm-nm -z target/wasm32-wasi/release/secure_js_sandbox_interpreter_boa.wasm > sandbox.txt

Architecture

This sandbox consists of the following components:

  • Interpreter - This is an interpreter for JavaScript that is compiled to the wasm32-wasi target. My current implementation uses boa to actually run the JavaScript, as it is entirely written in Rust, and can be easily compiled to run in wasm32-wasi. The interpreter should prevent the JavaScript accessing the system's disk, network etc. but does not limit the CPU and RAM consumed by the JavaScript.
  • Host - The Host library runs the Interpreter using wasmtime. This enables us to impose limits on CPU usage (fuel) and RAM usage (memory). It also further sandboxes the Interpreter so that any bugs in the Interpreter cannot accidentally permit the JavaScript code to access system resources.
  • Server - The Server is designed to be deployed as a docker image to a service like Google's Cloud Run. It allows a JSON API to be used to call the Host. Ideally, this docker image should be deployed with minimal privileges, in order to limit the damage if bugs in the Interpreter and Host were to enable a sandbox escape. It's also a good idea to deploy it in an auto-scaling configuration so that bursts in requests can be easily handled. Having said that, the Server does support multiple concurrent threads, so should be able to handle fairly high request volume on even modest hardware (depending on how much "fuel" and "memory" you allocate to each call).
  • CLI - The CLI is an alternative to the server that lets you directly call the Host

Interpreter

To run the interpreter natively (i.e. without the Host sandbox), you can run:

cargo run --bin secure_js_sandbox_interpreter_boa

To compile the interpreter to wasm32-wasi, you can run:

cargo build --bin secure_js_sandbox_interpreter_boa --release --target wasm32-wasi

This generates the output file: secure_js_sandbox_interpreter_boa.wasm. You can try running this using:

wasmtime target/wasm32-wasi/release/secure_js_sandbox_interpreter_boa.wasm

The interpreter should have a standardized interface, allowing easy experimentation with other JavaScript interpreters in the future:

  • ToyJS - Relatively new project and probably much more limited than Boa
  • Starlight - Much less actively maintained than boa
  • V8 etc. - probably much harder to compile to web assembly
  • JavaScript Core - JSC.js shows it is possible to compile this to web assembly

CLI

To run the CLI, you must first compile the Interpreter to wasm32-wasi, you can then run:

cargo run --bin secure_js_sandbox_cli --script "console.log('hello world')"

You can also run a benchmark comparing the secure_js_sandbox_cli against an insecure attempt at using node.js to create a sandbox by running zsh tests/benchmark.zsh

Server

To run the Server, you must first compile the Interpreter to wasm32-wasi, you can then run:

cargo run --bin secure_js_sandbox_server

Options:

secure_js_sandbox_server 

USAGE:
    secure_js_sandbox_server [OPTIONS]

OPTIONS:
        --fuel-per-call <FUEL_PER_CALL>
            The "fuel" for CPU operations. 440 million is approximately 100ms on my MacBook Pro
            [env: FUEL_PER_CALL=] [default: 440000000]

        --fuel-per-init <FUEL_PER_INIT>
            The "fuel" for CPU operations. 440 million is approximately 100ms on my MacBook Pro
            [env: FUEL_PER_INIT=] [default: 440000000]

    -h, --help
            Print help information

        --max-table-elements-per-sandbox <MAX_TABLE_ELEMENTS_PER_SANDBOX>
            I think this limits number of methods/exports in table, defaults to 10,000 [env:
            MAX_TABLE_ELEMENTS_PER_SANDBOX=] [default: 10000]

        --memory-limit-bytes-per-sandbox <MEMORY_LIMIT_BYTES_PER_SANDBOX>
            Limit to 50MB per sandbox by default [env: MEMORY_LIMIT_BYTES_PER_SANDBOX=] [default:
            52428800]

        --memory-limit-bytes-sandbox-cache <MEMORY_LIMIT_BYTES_SANDBOX_CACHE>
            Limit to 128MB of data in the sandbox cache [env: MEMORY_LIMIT_BYTES_SANDBOX_CACHE=]
            [default: 134217728]

        --port <PORT>
            [env: PORT=] [default: 3000]

GET /

Responds with current config and the memory used by the sandbox cache.

POST /execute

Example:

  time curl -X POST http://localhost:3000/execute \
    -H 'Content-Type: application/json' \
    -d '{"sandbox_id": "x", "init_script": "function fib(n) { return n <= 1 ? 1 : fib(n-1) + fib(n-2); }", "script": "fib(13)"}';

Request:

interface RequestBody {
  /**
   * The sandbox ID should be a unique ID per sandbox you want to use.
   * No two different users should have the same sandbox id. You can
   * pass `null` to disable sandbox reuse between requests.
   */
  sandbox_id: string | null;
  /**
   * Script to run before "script" in order to initialize new sandboxes.
   * This is especially useful when combined with a "sandbox_id" as it
   * lets you run some setup code once, and re-use the result across
   * many calls to the script. Note that sandbox re-use is only ever
   * on a best-effort basis, so your code should never rely on sandbox
   * reuse to function correctly.
   */
  init_script: string;
  /**
   * The JavaScript code to run on each request.
   */
  script: string;
}

Response:

/**
 * OK indicates that the JavaScript
 * code was successfully evaluated.
 * The "result" field contains the
 * value of the final expression that
 * was evaluated (providing it can be)
 * serialized to JSON.
 * 
 * Status Code = 200
 */
interface ResponseOk {
  status: "OK";
  result: unknown;
  stdout: string;
  stderr: string;
}

/**
 * RUNTIME_ERROR indicates that a runtime error
 * was thrown by JavaScript while attempting to
 * process the request.
 * 
 * Stack traces are currently not supported, but
 * may be added in a future release.
 * 
 * Status Code = 400
 */
interface ResponseRuntimeError {
  status: "RUNTIME_ERROR";
  stage: "INIT" | "SCRIPT";
  message: string;
  stdout: string;
  stderr: string;
}

/**
 * OUT_OF_FUEL indicates that too much CPU time was
 * consumed while attempting to process the request.
 * 
 * Status Code = 400
 */
interface ResponseOutOfFuel {
  status: "OUT_OF_FUEL";
  stage: "INIT" | "SCRIPT";
  message: string;
  stdout: string;
  stderr: string;
}

/**
 * OUT_OF_MEMORY indicates that too much memory was
 * consumed by this JavaScript sandbox.
 * 
 * Status Code = 400
 */
interface ResponseOutOfMemory {
  status: "OUT_OF_MEMORY";
  stage: "INIT" | "SCRIPT";
  message: string;
  stdout: string;
  stderr: string;
}

/**
 * INVALID_REQUEST indicates that the request body
 * did not match the expected schema.
 * 
 * Status Code = 400
 */
interface ResponseInvalidRequest {
  status: "INVALID_REQUEST";
  message: string;
}

/**
 * INTERNAL_SERVER_ERROR indicates that some unknown
 * error occurred while attempting to process the
 * request. This normally indicates a bug in
 * secure-js-sandbox.
 * 
 * Status Code = 500
 */
interface ResponseInternalServerError {
  status: "INTERNAL_SERVER_ERROR";
  stage?: "INIT" | "SCRIPT";
  message: string;
}

type ResponseBody =
  | ResponseOk
  | ResponseRuntimeError
  | ResponseOutOfFuel
  | ResponseOutOfMemory
  | ResponseInvalidRequest
  | ResponseInternalServerError

Docker

You can build the docker image by running:

docker build -t secure-js-sandbox .

You can run the docker image by running:

docker run --rm -it -p "3000:3000" secure-js-sandbox

About

Secure sandbox for JavaScript plugins using Rust and Web Assembly

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published