This post explores how to execute JavaScript in a sandboxed Web Worker.
Running user-generated scripts without sandboxing is risky because malicious scripts could access private data or cause disruptions. Using dynamic web workers allows you to contain potentially harmful operations.
We create a function dynamically using the AsyncFunction
constructor syntax. This method takes JavaScript code strings and converts them into functions. It also serves as a way to validate the code and catch errors.
const fn = (async () => {}).constructor('content', 'mime', source);
To create the script for the Web Worker, we define a __run
function to handle incoming messages. This function executes the received scripts based on the specified content and MIME type.
The Web Worker listens for messages containing the code to execute and returns the result back to the main thread.
We utilize the toString
method on the function we defined earlier to generate its source code. This guarantees no conflicts with the runtime string template and ensures the code stays isolated within its own function.
const runtime = `const __run=${fn.toString()};onmessage = async msg => {
const result = await __run(msg.data.content, msg.data.mime);
postMessage(result);
}`;
To execute the Web Worker, we initiate it asynchronously with the following steps:
return new Promise<FileValue>((resolve, reject) => {
const worker = new Worker(`data:text/javascript,${runtime}`);
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
worker.terminate();
reject('Timeout');
}, 10000);
worker.onmessage = ev => {
if (timeout) clearTimeout(timeout);
resolve(ev.data);
worker.terminate();
};
worker.onerror = e => {
if (timeout) clearTimeout(timeout);
reject(e.message);
worker.terminate();
};
worker.postMessage({ mime: file.mime, content });
});
We first create a Worker instance using the Worker
constructor, passing in a data URL that contains our runtime script as a string.
Using a data URL has the added benefit of enforcing cross-origin policies.
If the worker exceeds a specified duration (10 seconds here), it is terminated to prevent indefinite execution.
The onmessage
event handler takes care of the results returned by the worker. When a message comes in, it clears any active timeouts, resolves the promise with the received data, and terminates the worker to conserve resources.
If an error occurs within the worker, the onerror
event handler activates. It clears the timeout, rejects the promise with the error message, and terminates the worker.
We send the message to the worker using postMessage
, including both the MIME type and the content. This initiates the worker's execution.
This method ensures the scripts are executed in isolation, reducing the risk of security vulnerabilities while maintaining the functionality needed for user-generated content.