Formalizing a Security Hole

I'm working my way through Head First HTML5 Programming by Eric Freeman and Elisabeth Robson. The first couple chapters were essentially refresher for me, and then there was a chapter or two that helped to refile my ad hoc JavaScript knowledge in better order. But we've now moved on to JSON and JSONP.

JSON is JavaScript Object Notation, essentially a way to encapsulate a JavaScript Object in a human-readable form. It's purpose in life is primarily the transfer of information from a web server to a web browser. "Wait, isn't that what loading a web page does?" Yes, but for an interactive web page - one that doesn't reload every time new information is needed - a method is needed to transfer that information. The method is Ajax (using the mildly ugly XMLHttpRequest) and the data format is JSON. JSON is lovely: it's a very simple and straight-forward format.

But there's a problem in paradise: if a web page attempts to get JSON information from a web server that isn't the same one that provided the web page, it will fail because those annoying web browser designers think that there might be a security risk in what they call XSS or "Cross-site scripting," in which a malicious website injects scripts into your web page. (They're right.) So some genius - having noticed that this nasty restriction doesn't apply to scripts, only to Ajax data transfer - came up with JSONP, the idea of encapsulating JSON in a script, or "JSON with Padding." Essentially all this is doing is putting a formal name to a security exploit that the web browser designers and the web site designers both agreed needed to exist: ie. it's always been possible to load a script (although not data) from a non-local website. The distinction between script and data is essentially arbitrary given that a script is a form of data. So now it has an official name and protocol: "JSONP."

The book helpfully explains how you should choose what web services you link to carefully. And that's true, but to me they're missing the point: what the web developer risks if they make a bad link is their reputation. What the user risks is all the data on their computer. (Which is why I make such heavy use of NoScript.)

Using JSONP reminds me of the old Arabic curse "May your left ear wither and fall into your right pocket." This is what I hate so much about JavaScript: you have to get incredibly twisted up to do the most basic things.

window.onload = init;

function init() {
    setInterval(handleRefresh, 10000);
}

function updateSales(sales) {
    var salesDiv = document.getElementById("sales");
    for (var i = 0; i < sales.length; i++) {
        var sale = sales[i];
        var div = document.createElement("div");
        div.setAttribute("class", "saleItem");
        div.innerHTML = sale.name + " sold " + sale.sales + " gumballs";
        salesDiv.appendChild(div);
    }
}

function handleRefresh() {
    var url = "http://remote.url.com" +
        "?callback=updateSales" +
        "&random=" + (new Date()).getTime();
    var newScript = document.createElement("script");
    newScript.setAttribute("src", url);
    newScript.setAttribute("id", "jsonp");

    var oldScript = document.getElementById("jsonp");
    var head = document.getElementsByTagName("head")[0];
    if (oldScript == null) {
        head.appendChild(newScript);
    } else {
        head.replaceChild(newScript, oldScript);
    }
}

(Mostly copied from Head First HTML5 Programming.)

First, set up a timer tied to a function - in this case, handleRefresh(). That function generates the remote URL (including an essentially meaningless pseudo-random element whose only purpose is to defeat browser caching), and then creates a newScript element. But you can't just stuff that into the page, because if you did you'd eventually end up with a huge heap of <script> tags that would choke the performance on your page. So you find and replace oldScript. Once the element is appended/replaced, the script is immediately loaded. It brings in your data payload, and runs the callback function. This is a simple but not very good implementation: the page loads, but doesn't load any actual data for 10 seconds (the interval). There are ways around that, but of course they involve writing more code. In my mind, this should be maybe four lines of code (and I shouldn't have to resort to an external library to do that, if you were about to suggest jQuery or something like that).

I hope I can stick to XMLHttpRequest: I thought it was ugly until I met this arrangement and suddenly realized how elegant XMLHttpRequest is. But of course the choice isn't always up to the web designer. Want to use a remote data source? JSONP it is. <sigh>

Update: After attending a JavaScript Meet-up, I return considerably educated. Apparently the JSON standard is much more strict than I thought: its definition of "object" is extremely narrow and doesn't include active code (or even Date() objects). When you call JSON.parse() it'll throw a fit if the data doesn't fit the narrow official definition. So current use (as opposed to Head First HTML5, so five years ago) is to use JSONP without the callback, and apply JSON.parse() to the retrieved JSONP data.