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 this
keyword 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…
Read more!