Running untrusted code safely in browsers

Burak Yiğit Kaya
Senior Software Engineer
June 06, 2022

Why Formsort uses WebWorkers for safe code execution

At Formsort, we allow our form builders to write short snippets of code for one of our core distinguishing features:

The only difference between these two types of variables are their data source: calculated variables only use other variables as their input, whereas API variables make an API request (via HTTP) and use its response as the input. Both support no-code or low-code options such as selecting a field from a JSON response but eventually, one needs the real powers (and curses) of a Turing-complete language. We provide a tightly controlled and validated TypeScript environment through the famous Monaco editor that also powers the extremely popular VS Code editor for input. We run this custom code in a sandbox when we need to run it, let it be in Studio (the form designer), or Flow (the actual form itself).

Interestingly, I have worked at many companies which relied on safe code execution on third-party environments. This kind of code will break the host environment it is running on unless there is some sandboxing as the host environment rapidly changes due to product changes. Back in the day, the canonical solution for sandboxing untrusted or unsafe JavaScript code was using iframes. Iframes essentially allow you to have an isolated page inside another web page with a communication port to exchange messages in between. However, they were initially invented for a different purpose: to embed external content on your page safely, such as those YouTube or Twitter embeds. Our method for using them for safe code execution was essentially a hack:

  1. Use an invisible iframe
  2. Load some broker code and wait until it loads
  3. Listen to messages from the parent page
  4. Process the messages and respond with the results
Iframe sandbox sequence diagram

This approach was not meant for safe code execution and had many caveats. Iframes may fail to load, and for security reasons it is very hard to detect when they do. Same for messages getting lost or ignored. All those lines, especially dashed ones in the diagram above are potential silent failures. Moreover, they can still block the page as the custom, unsafe code is still executed in the same “thread” (work pipeline) that the main page runs on. They also inherit the security rules from the parent page when used in code execution mode (sourceless iframes), limiting our options for enforcing strict security requirements on our services while still allowing unsafe code to be executed in them.

In the past years, web standards have evolved and now have a dedicated construct for this called Workers. The main benefits are:

  • You can have any number of them
  • They have no visual/DOM component as they are meant for code execution
  • They execute the code in an isolated environment with no access to the DOM
  • They have a good API to control and observe for code execution
  • They are supported across many browsers, including IE 10 and old versions of Safari

Despite these benefits, we initially perceived a major downside to Workers: you have to have a dedicated script to be loaded in them. Turns out, that’s not the case thanks to another new-comer called “blob URLs”. Blob URLs allow one to construct the execution script on the fly. This essentially gives us a safe eval: we can generate a blob URL from a code string and set it as the Worker’s URL that runs it in a completely isolated environment.

WebWorker sandbox sequence diagram

Given these, we decided to overhaul our dynamic variable calculation code to use web workers when they are available. The results were quite encouraging:

  • A distinct drop of error events from dynamic variables
  • Detailed error logs when there is an error so we can fix it or ping our customers about them

Here’s some sample code to demonstrate how we use them:

Want to learn more? Sign up to chat with our team or try it for yourself here.