Building Widget Sandboxes with Web-workers

JavaScript

For a new web application I’m working on I want to allow third party widgets to run within the application’s pages. Of course running arbitrary third party JavaScript on you app’s pages is not something you should do lightly as it can leave your users vulnerable to attacks by malicious widget authors.

Ideally you want to control the access that widgets have both to your user’s data and to page content. For example you want to remove unvetted access to the DOM and to the global window object. In this article I’ll describe the approach I’m taking.

Plenty of options

There are a variety of approaches you can take to achieve restrict a widget’s access, each with their pros and cons.

Manual verification

This is the least amount of coding but obviously the least scalable to large numbers of widgets. It’s something that you could adopt if you expect to have only a few, infrequently updated widgets. But if you are going to all the effort to support third party widgets, that’s probably not the outcome you’re hoping for.

Assisted verification

The next level up is to allow widgets to be displayed only if they pass a manual or automated linting process. This process validates that the code conforms to certain restrictions that remove the ability for it to cause harm. Examples of this are Douglas Crockford’s Adsafe and Kalan MacRow’s Nerio.

Code rewriting

This is arguably the trickiest road to follow. Code is analysed and modified to remove unsafe operations. Google’s Caja project takes this approach. The rewrite effectively sandboxes the code prior to execution.

Emulation

This technique runs the third party code in an emulated JavaScript interpreter or virtual machine. An impressive example of this is Brandon Benvie’s Continuum which is a complete ES6 virtual machine written in ES3. This of course allows you to completely control the environment that you expose to the code at the expense of adding an additional layer of complexity.

Sandboxing with web-workers

The final approach and the one that I’ve taken is to sandbox the code within a context that does not provide direct access to the DOM or to the window object.

Web-workers provide a separate thread of execution to the main thread. This execution environment has no access to the DOM or window object and can only communicate with the main thread by message passing. This removes a large number of attack vectors for code executing within a web-worker.

A demonstration

This pen shows how you might go about isolating widget code within a web-worker while still letting it update an isolated part of the web page.

See the Pen Isolating third party widget code by Simon Collins (@devilish) on CodePen.

You can enter code for your widget in the text box to the left. The widget is constrained to write only to the container on the right side. It has access to a tiny subset of JQuery’s API – namely html() and text(). For instance:

$().html("<h1>Heading</h1><p>Hello World!</p>"); // set the entire container's HTML content
$("p").text("Hello fellow widgets"); // updates just the text content of the paragraph
$("h1").html("<i>Heading</i>"); // updates just the HTML content of the heading element

How it works

Running the widget code

We need our widget code to run in a web-worker context. When creating a web-worker we need to pass the Worker constructor a URL to the script that it will execute within the worker. In a real application we would probably load the widget code from a separate script file but for this demonstration we need to load it from the contents of the text box.

We first create a Blob from that content and then pass the constructor a URL to that blob.

var widgetCode = ... // grab the widget code somehow
    
// Needs support for Blob (IE10+)
var blob = new Blob([widgetCode], { type: 'application/javascript' });
var url = URL.createObjectURL(blob);
var worker = new Worker(url);

Restricting access

We could just leave it at that. The widget code does not have direct access to the DOM or window object so there’s a limit to the damage that it can do. However we might like restrict its access further. For example we could prevent it firing random messages into our main thread by removing direct access to the postMessage method.

We might also like to wrap certain built-in methods to prevent their mistreatment. The code below shows an abuse of Array.join which could potentially crash a browser tab, but could be prevented by wrapping Array.join to restrict the size of arrays that can be joined.

new Array(500000000).join("abcdefghijklmn");

In the demo we achieve this by prepending a boot-loader function to the widget code. This function nulls out self.postMessage to prevent direct access to it. It also freezes self (the global scope object) so that it cannot be modified by the widget code.

// remove widget code access to postMessage
self.postMessage = null;
  
// freeze context so widget code can't change it
Object.freeze(self);
  
// now run the widget's code
// ...

As an aside, note the neat trick in the pen code where we grab the contents of the boot-loader function by calling its toString method. For the purposes of this demo this allows us to keep the code all in a single script file while still passing it across to the web worker. In a real application you might still do this to avoid extra network fetches and to keep all the the code for your page together.

Allowing controlled DOM access

Now our widget can run in a secured environment but it’s not very useful if it can’t display anything. So the final part of the demo is allowing it some scope to display and update HTML content.

We want to restrict the widget to writing within a controlled part of the page (in this case the container <div id='widget'/>). We achieve this by adding methods to the global scope that serialize their arguments and then call postMessage to send them for processing by the main thread. We setup these methods using a closure over postMessage before we remove the reference to it on the global object.

Within the web-worker context we have:

// grab this before we overwrite it
var postMessage = self.postMessage;
    
/* Constructs a view manipulation object */
function view(id) {
  function updateView(action, content) {
    postMessage.call(self, {
      id: id,
      action: action,
      content: content
    }); 
  }
    
  return {
    html: function(val) {
      updateView("html", val);
    },
    text: function(val) {
      updateView("text", val);
    }
  };
}
  
self.$ = view;

Sanitizing the content

Now that we’ve allowed our widget to send HTML to be rendered we need to ensure this HTML won’t cause any harm. If we’re not careful a nefarious widget could do such sneaky things as passing across malicious code wrapped in a script tag.

Within the main script we use the Caja project’s html_sanitize function to sanitize the content before updating the DOM. By default this removes dangerous tags such as script, base and style along with javascript: URLs. The pen below lets you explore in more detail how this works. Note that you can also control how sanitization of URLs and class/ID attributes is done which I’m not doing in the main demo.

See the Pen Using Caja’s HTML sanitizer by Simon Collins (@devilish) on CodePen.

Updating the content

Finally, we update the widget’s container with the sanitized HTML or text content. Note how we prepend #widget to the selector to ensure that it is scoped to the container. In a real world application you’d probably give each widget container its own generated ID for this purpose.

/*
 * Handle messages from the worker relating to updating the
 * widget's view content.
 */
function handleMessage(msg) {
  var data = msg.data;
  var content = html_sanitize(data.content);
  var sel = "#widget";
  if(data.id != null) sel += " " + data.id;
  
  if(data.action == 'html') {
    $(sel).html(content);  
  } else if(data.action == 'text') {
    $(sel).text(content);
  } else {
    content.warn("Unrecognized action: %s", data.action);
  }
}

worker.onmessage = handleMessage;

Building on this demo

There are some obvious limitations to this demo. The biggest one is that there’s no way of sending input back to the widget. This could be addressed in various ways. One simple way would be to have the display update code look for links, buttons etc. in the content provided by the widget and automatically attach event handlers to them. These handlers can then proxy user actions through to the widget for it to handle.

// -- Main page code --
function handleClick(e) {
  worker.postMessage({ action: 'click', id: e.target.id });
}

if(data.action == 'html') {
  $(sel).html(content);
  // add event listeners 
} 
// - snip -

// -- Widget code in the web-worker --
self.onmessage = function(msg) {
  var data = msg.data;
  if(data.action === 'click') {
    // handle click by key off data.id to determine
    // what was clicked
  }
};

If your widgets update their display frequently you may find performance is sluggish. On my Mac there’s a fixed overhead of around 10 to 60 milliseconds for sanitizing moderate amounts of content. There’s also a cost in serializing the data across thread contexts and updating the DOM. In future articles I’ll explore ways to address this.

Update

After I wrote this post I discovered the HTML5 sandbox attribute for iFrames. This looks like it might be a useful alternative to web worker encapsulation, particularly once it gets better browser support. It lets you grant fine grained whitelisted support for various features (invoking popups etc.) to the iFrame contents.

For more details see this great write up.