A tiny, experimental library to allow you to execute arbitrary JavaScript functions safely in a sandbox
Sandybox is a tiny (<1.5kb), experimental library to allow you to execute arbitrary JavaScript functions safely in a sandbox on the web.
Sandybox has three primary goals, to be:
In short, functions should be totally separated from the main application and thus safe to run.
In the future, Sandybox should be:
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.
The following are known limitations and/or explicit trade-offs of Sandybox:
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));
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.
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,
});