a glob of nerd­ish­ness

Sandboxing JavaScript in the browser

published by natevw on

I don't always run untrusted code in my webpages, but when I do, I prefer it to be sandboxed.

Unfortunately, many people have asked "how is it possible?" but this and this and this and this and this and this and this and this and this … don't have any solid "yes" answers; it seems really hard to sandbox JavaScript code unless it can be run asynchronously. Even then, code may have access to unwanted features.

However, I might have found a way!

Background

There are a number of reasons it is unsafe to run untrusted JavaScript code in the browser. Any code you eval will have:

Most of these have to do with state and scope. A sandbox isolates the state and limits the scope that can be accessed. By combining a few tricks, it seems we can sandbox JavaScript code in the browser to the point where the most evil it can do would be to halt things completely.

evel, a safer eval

Evel Knievel jumping the Snake River in an EVEL.js rocket bike

(Figure 1: mad thanks to Mike West for the rad mascot!)

I've shared evel, on github as a drop-in JavaScript library that provides evel() and evel.Function() which can be used in place of eval() and new Function() respectively. In theory code run via evel has no access to the DOM, no access to the outside world beyond JavaScript builtins, and no access to your code's variables except those intentionally passed in.

It works by:

  1. Sanitizing the provided source against e.g. escape characters (immediately, which also serves to flag syntax errors at the expected time)
  2. Wrapping source in a "use strict"; environment to eliminate global access via this tricks
  3. Shadowing all non-ES5 globals (each time called!) to eliminate direct access via name
  4. …doing the last two steps using a clean iframe's JS environment to isolate {}.__proto__ effects

Basically instead of returning the provided code directly, we wrap it like this:

function ({{g1}}, {{g2}}, …, {{gN}}) {          // imagine {g1:'document', g2:'XMLHttpRequest', g3:'d3', … }
    "use strict";
    var fn = {{sanitizedSource}};
    return fn.apply(non_window_ctx, original_args);
}

Note that all bets are off if browser doesn't support strict mode, so we check for that and refuse to proceed if support is unavailable.

There are certainly some caveats.

The biggest known issue is that untrusted code can still do the equivalent of while (true) ; and lock the page up in an infinite loop. I don't see any particular way around this except to arrange your code to call evel from a separate iframe/worker execution context that won't block the main page.

Oh, and because the provided code necessarily runs under strict mode, it may break or throw an exception unexpectedly. Most code should be fine though and of course this doesn't make the sandbox ineffective. So long as you surround the call with a try/catch block, your code is safe.

Or is it?

Everything above is unproven!

I can't think of a way out of its sandbox, but maybe someone else can. Like its namesake, evel is attempting a pretty audacious feat. Will it successfully jump the canyon?

Unlike Evel Knievel's famous jump attempt, which was across an impressively beautiful part of the Snake River, the way evel it works is kind of ridiculously ugly. Like the original jump, it may end up parachuting down into the river if any more serious flaws are found in its execution.

Snake River canyon near the attempted derring-do

So before we go and trust our life support/nuclear launch codes/baby seals to it I thought it'd be fun to get some more eyes on it. I've built a "challenge page" that serves as both an example playground and as a call to more thorough investigation of potential vulnerabilities. Please share the demo with anyone you think might be interested.

blog comments powered by Disqus HTTP/1.0 500 Internal Server Error Cache-Control: must-revalidate Connection: close Content-Length: 60 Content-Type: application/json Date: Sat, 23 Sep 2017 21:56:48 GMT Server: CouchDB/2.0.0 (Erlang OTP/19) X-Couch-Request-ID: 3518fac27a X-Couch-Stack-Hash: 2053811356 X-CouchDB-Body-Time: 0 {"error":"unknown_error","reason":"undef","ref":2053811356}