Sandboxing JavaScript

tl;dr iframes are likely your best bet to properly sandbox JavaScript.

A sandbox, © dstrelau (CC BY 2.0)

So you want to allow your power users to extend your website, what could go wrong?

A quick search for ‘Sandboxing JavaScript’ on Stack Overflow will reveal it’s a common problem without an obvious foolproof solution. As with most of these things, the ultimate answer is “well, it depends”.

Here at Zendesk we’ve been on quite a journey with regards to sandboxing JavaScript. We started with a “free for all” approach, allowing our users to include arbitrary JavaScript, called Zendesk widgets, directly into the DOM of our (at the time) mostly static web app. Following on, we “tried to fix it” with the introduction of the App Framework, which provided a pseudo-sandbox as an attempt to isolate third-party code from our Ember.js based application. Finally, we decided to “do it the proper way” with the App Framework v2, using an iframe based architecture. Our hope is that, by talking through our journey and explaining what we think is the proper way, we’ll help other developers find the right solution for them based on their own use case.

Free for all

I don’t think many would argue for running untrusted code on your website, but the exact definition of untrusted may be argued. Can I trust my power users to include their own JavaScript? They can only affect their own stuff anyway…

Extension frameworks inherently need to balance how much flexibility they provide to apps, and the obvious amount of flexibility to provide is “as much as we‘ve got”.

So at the risk of stating the obvious let’s list out the potential dangers of running code you don’t control. Any JavaScript running on your page will be able to:

  • make ajax requests to your server as the logged-in user
  • make ajax requests to external services via CORS, potentially sharing data available on your page
  • manipulate your DOM
  • interact with global variables
  • modify the prototype of core objects
  • throw unhandled exceptions, which may stop your JavaScript from running

This is by no means an exhaustive list and while you may implement workarounds to protect against some of these, JavaScript, uh, finds a way.

While you and your users may be ok with this, it becomes troublesome to change your application when you have to constantly consider whether third-party code might be relying on internal code or behaviour which wasn’t explicitly exposed.

How we tried fixing it

When we started looking at developing a sandbox for third-party code our main goal was to encourage developers to write their code in an isolated fashion, relying only on public, documented APIs. We reviewed every app submitted to our marketplace and only allowed the ones that adhered to our guidelines. This caused some frustration for app developers, who felt the process was too restrictive and slow.

However, we didn’t want to get in the way of developers if they needed to do something we didn’t officially support, at their own risk, on their own account. That way we could offer an easy migration path for Zendesk widgets (embedded script tags with third-party code), which relied on the ability to manipulate the DOM.

So we used some unconventional JavaScript tricks, such as the with block, and essentially got away with it, except when we didn’t. At best a few apps occasionally broke because we made unexpected changes to our code and at worst some of our attempts at improving our sandbox caused outages. Our pseudo-sandbox worked as a way of educating developers to write structured code that followed the rules, but we still suffered a lot of pain with third-party code that didn’t. In fact, in some cases apps broke the rules despite the author’s best intentions.

Where we are now

The latest iteration of the Zendesk App Framework uses iframes to sandbox apps. Iframes work well to sandbox JavaScript for a number of reasons. Firstly, an iframe represents a nested browsing context, meaning they don’t share the global scope or the DOM with the parent page. Secondly, scripts running within an iframe are subject to the same-origin policy, described in more detail below. Finally, with HTML5 you can specify the sandbox attribute to enable further restrictions on iframes.

Here’s how we did it:

Asset CDN

In order to benefit from the security restrictions imposed by same-origin policy we host every third-party asset from a CDN on a different domain. This ensures that third-party scripts:

  • only have very limited access to Window and Location objects from a different origin
  • can only read from or write to their own cookie, localStorage, and IndexedDB
  • cannot make HTTP requests to a different origin, unless it supports CORS

For more information see Same-origin policy at MDN.

postMessage API

While scripts from different origins are not allowed to access each other, the window.postMessage API provides a controlled way for cross-origin communication by using message event handlers. Normally, this involves adding an event listener on one origin so you can post a message from the other. If you need two-way communication you need an event listener on both origins so they can post messages to each other. Additionally, for security purposes it is highly recommended to validate the origin of the event from your message handler.

window.postMessage is a low-level browser API and it may not be the most straight-forward way for developers to interact with your application. For this reason it often makes sense to write a JavaScript SDK that can be included on pages that are iframed on your website. The SDK can simplify the initial setup and also provide some higher-level APIs to interact with the parent page.

Zendesk apps can include the ZAF SDK library on their website to interact with the Zendesk App Framework from within an iframe. If you’re interested to see how it works the source code is available on GitHub and is documented here.

Authentication

Another benefit of using iframes is that it encourages third-party developers to host their own code. This may or may not be desirable depending on your use case, but the main advantage is that it gives developers the ability to use whichever technology they want on the client and server side.

When including an external page within an iframe it is important to provide a way for the external server to validate that the request comes from your application. This allows the third-party code to lookup data associated with the user and also prevents leakage of privileged information to unauthenticated requests.

Zendesk apps can opt in to the “signed urls” authentication feature. When this feature is on apps receive a POST request with a JSON Web Token (JWT), which can be validated against a public key that is specific to the app. This allows an app developer to validate that a request originates from a legitimate Zendesk instance. It also allows apps to lookup values associated with a Zendesk account or a particular Zendesk agent within their internal database, while preventing information leakage to unauthenticated third-parties. For more information on how we do this see our public documentation for Using Signed URLs.

Alternatives

Google Caja

Google Caja was initially evaluated as a potential solution for us, but its future seemed uncertain. Also, Caja would have required more fundamental changes than we felt necessary, as well as complicating backwards-compatibility. Caja however, does remain a source of design inspiration for high-level APIs.

Web Workers

Web Workers were also considered as an alternative to iframes. While a Web Worker provides a safe environment for running untrusted JavaScript, it does not have its own DOM, so it is up to the parent page to sanitise and render out HTML generated by the worker. Sanitising HTML is non-trivial and imposes further restrictions on what developers can do. It would also be up to the parent page to attach event handlers onto the generated DOM so it could be transmitted back to the worker.

Web Components

Web Components, once standardised, may provide a great alternative to iframes for sandboxing untrusted code. The current proposal includes many of the benefits of iframes, such as DOM and global scope isolation with added benefits, such as not requiring a separate origin and simplified integration with the host page. For more information about Web Components see Web Components at MDN.

Conclusion

Moving to an iframe based architecture for sandboxing apps was a major step forward for us to protect our users and also provide more flexibility for app developers. Although it adds some overhead and complexity to how your developers interact with your application, iframes come with extra security features provided by the browser, improved stability and flexibility for developers.