sandybox

A tiny, experimental library to allow you to execute arbitrary JavaScript functions safely in a sandbox on the web.

Usage no npm install needed!

<script type="module">
  import sandybox from 'https://cdn.skypack.dev/sandybox';
</script>

README

Sandybox

npm version build status

Sandybox is a tiny (<1.5kb), experimental library to allow you to execute arbitrary JavaScript functions safely in a sandbox on the web.

Goals

Sandybox has three primary goals, to be:

  1. Sandboxed (secure) - Functions should not be able to access any same-origin information or make requests as if it were from the same-origin as the host.
  2. Non-blocking - Sandboxed functions should not be able to block the main thread.
  3. Performant - Sandboxed functions should be created and executed with minimal additional overhead compared to normal functions.

In short, functions should be totally separated from the main application and thus safe to run.

Future Goals

In the future, Sandybox should be:

  1. Compartmentalized - Functions in the sandbox should not be able to influence each other, such as by modifying global objects.

Architecture

Sandybox creates function sandboxes by instantiating a sandboxed iframe and then creating a Web Worker in the iframe. This meets the first two goals of the project, any code executed within the web worker will be secure and non-blocking.

However, in order to communicate with the web worker, you'll need to transfer any data to the iframe and then to the web worker which is definitely not performant.

To solve this problem, we can use a MessageChannel. We keep one port of the channel in the main thread and then send the other one to the web worker, giving us a direct line to the web worker. This will give us the minimal amount of overhead that we could possibly get for code running in a separate thread. In other words, it'll be performant.

Finally, we currently have no good solution to compartmentalization. The only standardized way to achieve it at this point in time would be to create separate web workers for each function, but this would be much harder to make performant. Given this was the lowest priority goal, this has not been implemented and instead moved to a future goal.

In the future, Realms or, more likely, Compartments should make compartmentalization easy, but it is likely a long way until either is standardized.

Limitations

The following are known limitations and/or explicit trade-offs of Sandybox:

  1. No DOM access - Given that sandboxed functions run in a web worker, there is no direct DOM access. Allowing DOM access would make it impossible to be non-blocking at this point in time.
  2. Data limited by the structured clone algorithm - Due to running in a separate execution context, only objects that can be cloned can be sent into or received from the functions.
  3. Functions can block each other - While the functions are guaranteed to not block the main thread, a function in the sandbox can block another function in the sandbox. It would be impossible to guarantee this doesn't happen as browsers can only execute a finite number of threads in parallel.

Usage

const sandbox = await Sandybox.create();
const sandboxedFunction = await sandbox.addFunction((word, repeat) => {
  let result = '';
  for (let i = 0; i < repeat; i++) result += word;
  return result;
});

console.log(await sandboxedFunction('hi!', 100000000));

Cleanup

If you're finished with a function and worried about memory usage, you can use sandbox.removeFunction(fn) to evict the function from the sandbox.

sandbox.removeFunction(sandboxedFunction);

Calling this will cause any unresolved executions of the function to reject. Additionally, once a function has been "removed" any calls to it will result in a rejected promise. Any rejections that are due to sandbox cleanup will be instances of SandboxError.

Similarly, if you are finished with a sandbox you can cleanup the entire thing by calling sandbox.cleanup(). This will remove all functions, the iframe, and the web worker.

sandbox.cleanup();

In the future, sandboxes and their functions will likely cleanup automatically by using a FinalizationRegistry. Until then, you'll need to manually cleanup if memory usage is a concern.

Allow Same Origin

While strongly recommended against, some use cases require the sandbox to run in the same origin as the host. This opens up potential attack vectors, but is still safer than evaluating code directly in your app.

To allow same origin, you can pass an options hash to the create method and set dangerouslyAllowSameOrigin:

const sameOriginSandbox = await Sandybox.create({
  dangerouslyAllowSameOrigin: true,
});