Third-party JavaScript API security

Third-party JavaScript is a pattern of JavaScript programming that enables the creation of highly distributable web applications. Many websites (publishers) embed untrusted JavaScript code into their pages in order to provide advertisements, social integration, user’s analytics and so on. Since JavaScript may change look and feel of the contained page, steal cookies or force user to visit some page – it should be considered as untrusted code which may harm not only the page, but also other third-party API’s on the same page. From third-party API developer point of view, publisher’s page became untrusted as well. Developer doesn’t know what publisher’s real intentions are and how other third-party JavaScript will behave.

So it is essential to protect third-party API from another untrusted code which could be present on the page.  While third-party code may be isolated in iFrame, this reduces performance and restricts communication between the hosting page and third-party code. Therefore a lot of APIs distributed via pure JavaScript injection into publisher’s page.

Anonymous wrappers

Anonymous wrapper allows to avoid global object pollution. In addition, if you pass some frequent-accessed global variables as parameters – it may lead to smaller size of minified JavaScript file. This is because minifier will consider argument named window (in the example below) as variable, but not as a keyword. So all references to window argument in function body will be replaced with something shorter.

(function(window){

//a lot of references to window object here

})(window)

But is it a safe way to write third-party code? Not really, it could be easily spoofed by another, untrusted JavaScript if it is executed before yours. The more bulletproof approach would be to use keyword this in global context. Also it is a good technique to rely on JavaScript default behavior regarding arguments – if argument is missed it will be assigned to undefined internally! So, here is an example with anonymous wrapper which guarantee you correct references to window and undefined objects.

(function(window, undefined){

//here we definitely can trust those two variables

})(this)

Callstack inspection

Let’s consider another third-party JavaScript code. The main intent of the closure below is to restrict access to variable secret.

var API = (function () {
    var secret = 'my secret',
        safe = function (token, secret) {
            if (token == secret) {
                //do something here            
            }
        };
    return {
        safe: function (token) {
            return safe(token, secret);
        }
    }
})();

At first glance it looks like there is no way API.safe will leak value of secret variable. But here is an opportunity for bad guys:

var unsafe = {
    valueOf: function f() {
        document.write(f.caller.arguments[0] + ', '); // Chrome process arguments a little bit differently
        document.write(f.caller.arguments[1]); // FF
    }
};

API.safe(unsafe);

Before comparing object token and secret variables, the == operator will call valueOf method on each object, a function which the attacker now controls. So it is possible to retrieve caller function’s arguments and read them. In this particular case the attack could be disarmed by replacing equality (==) operator with identity (===) operator, which won’t call valueOf method during the comparsion.

Prototype poisoning

Malicious code on publisher’s page may alter the behavior of native objects, such as strings, functions, and regular expressions, by manipulating the prototype of these objects. For example, it is a common task to check current session protocol and request the resource asynchronously by using HTTP or HTTPS respectively. This can be done via Regex expression:

var protocol = /https?:/gi.exec(url)?

var newUrl = protocol[0] + …

Unfortunately this is not safe technique for third-party JavaScript and could be compromised by modifying prototype of RegExp object:

RegExp.prototype.exec = function () { return [‘http:’]; }

Instead of calling native exec the browser will instead call attacker’s function and the resource will be requested via unsecure HTTP protocol even if user accessed the page by HTTPS.

Confusing Callbacks and Methods

Here is a standard/lightweight implementation of observer pattern in JavaScript:

function Observer() {
    var subscribers = [];
    return {
        subscribe: function (subscriber) {
            subscribers.push(subscriber);
        },
        publish: function (publication) {
            for (var i = 0, len = subscribers.length; i < len; i++) {
                subscribers[i](publication);
            }
        }
    };
}

And again there is nothing wrong with it at first glance. Let’s assume that your third-party API expose instance of Observer class to allow publisher code subscribe to your widget events:

var observer = new Observer;
observer.subscribe(function(){alert('I'm normal event handler');});

The problem is that internal variable subscribers is exposed via thiskeyword inside event handler.

observer.subscribe(function untrusted() {
    this[0] = untrusted; //put our subscription first in the queue
    this.length = 1; //cut off all other event subscriptions
    alert('I'm the only one here in charge!');
});

Although first call of observer.publish() will rise both alerts, on the second and any consequent run only untrusted alert will be shown.

Fix is simple – replace execution context on something useless:

publish: function (publication) {
    for (var i = 0, len = subscribers.length; i < len; i++) {
        subscribers[i].call(undefined, publication);
    }
}

But even now we’re not safe, consider the following subscriber:

observer.subscribe(function untrusted() {
    throw ‘Skip those losers’; //will skip all subsequent event handlers to execute
});

I’ll leave you here to resolve the issue by yourself (use setTimeout(…, 0) to put all handlers in the queue and then let browser to execute them independently).

Mandatory mutability

Here is another example of hiding some private variables inside closure:

function Counter() {
    var count = 0;
    return {
        increment: function () {
            return ++count;
        },
        decrement: function () {
            return --count;
        }
    };
}

var counter = new Counter;
alert(counter.increment()); // 1

In case your API expose counter instance somehow, it’s methods could be easily substituted with attacker’s code:

//malicious code below
counter.increment = function() { return 10; }
alert(counter.increment()); // 10

Luckily, ECMA 5 provide us with a mechanism to prevent such unwelcome substitution by using Object.freeze approach:

function Counter() {
    var count = 0;
    return Object.freeze({
        increment: function () {
            return ++count;
        },
        decrement: function () {
            return --count;
        }
    });
}

Unfortunately it won’t work in old browsers.

Unusual Sandboxes

During writing this blog post I’ve found js.js library that allows an application to execute a third-party script inside a completely isolated, sandboxed environment. Haven’t tested it yet, but it looks promising. I’d like to check performance as well as security issues there…

  • Pingback: Unbound methods in JavaScript - Pavel Podlipensky

  • gabalafou

    Excellent article.

    The js.js library, by the way, is fascinating: a Javascript interpreter written in Javascript. Have you read the js.js paper?

    Terrace, Jeff, Stephen R. Beard, and Naga Praveen Kumar Katta.
    “JavaScript in JavaScript (js. js): Sandboxing third-party scripts.” USENIX WebApps (2012).

    https://www.usenix.org/system/files/conference/webapps12/webapps12-final6.pdf

    From what I could tell, the biggest downsides to js.js are:

    1. The third-party script is interpreted by js.js, which is on the order of 100 times slower than native Javascript interpreters.

    2. The third-party script is given almost no rights, so you have to spell out everything in painstaking detail that you will allow the third-party script to do on your site.

    3. It does not support Internet Explorer until version 10.

    • http://podlipensky.com Pavel Podlipensky

      @gabalafou:disqus completely agree with you, it is not production-ready yet. Probably we’ll see some solutions from browser vendors, until then iframe is the strongest defense against malicious third-party script.