Monday, April 6, 2015

Cross-Site Scripting (Legitimately)

This weekend I completed one of the major features that I wanted to add to my GitHub Game Off 2015 game - the high scores list. While it may sound mundane, it has one feature that I don't use a whole lot - JSONP.

JSONP, or JSON with Padding, is in my opinion a technique that has a very misleading name. It's really about bypassing the same-origin policy that web browsers use to prevent cross-site scripting hacks. And it works by having the server respond with full JavaScript instead of just a JSON string. The JavaScript is typically a function call with the JSON string directly in the call. I guess that's where the "padding" part of the names comes from - the data is "padded" with the function call. Odd if you ask me, but whatever.

Here's a basic example. For it to work, the client defines a function to process the data it wants to request from the server.

function processData(data) {
    console.log(data); // or do something more useful, whatever
}

Now the request can be made. By injecting a <script> tag into the DOM, the browser will make a remote call to any server you want - it doesn't have to be the same as the one serving the original page. As part of the URL in that script tag, the name of the local function is typically passed as part of the query string. The script tag would look like this...

<script type="application/javascript"
     src="http://www.example.com/giveMeData?callback=processData"></script>

On the server, the data is then prepared for the response. Instead of just sending a JSON string, the server creates a string response that looks like a regular JavaScript function call. The function being called is the one specified in the script tag URL, and the JSON data is directly in the call. This is what the server response text looks like:

processData({"field1":"value1","field2":"value2"});

The browser treats the response just the same as any static JavaScript file requested from a server and executes it. Your data processing function gets called, you get the data, and everyone is happy.

For this to work, the server obviously has to be set up for this. You can't just pass a "callback" query string parameter to any random web server and expect a usable response. This pattern is most useful for creators of data services who want to make their product available on other peoples' web pages. Like the Facebook buttons, for example.

I found myself wanting to use this pattern because I put my game on caseyleonard.com, but that site just serves static content via Nginx. To maintain the high scores web service, I'd have to run an application somewhere else. Without the JSONP pattern, my game wouldn't be able to send and receive high score data. It was also useful to allow me to test updates to my game locally and still access the high scores on a remote server.

As it turns out, this pattern is common enough that jQuery includes some utilities to make it easier. They have a really nice API that makes performing a JSONP request almost the same as a "normal" AJAX request. Here's an example from my game code.

function processHighScores(data) {
 
    var $tbl = $("#scoreboard");
    for(var i=0; i < data.length; i++) {
     var n = data[i].name;
     var s = data[i].score;
        var $row = ("<tr><td>" + n + "</td><td>" + s + "</td></tr>");
        $tbl.append($row);
    }

}

function populateHighScores() {

    $.ajax({
        type: "GET",
        url: "http://amphibian.com/scores",
        jsonpCallback: "processHighScores",
        contentType: "application/json",
        dataType: "jsonp",
        success: function(json) {
            console.log(json); // don't really need this
        },
        error: function(e) {
            console.log(e.message);
        }
    }); 
}

The processHighScores function above is the one that receives the data from the JSONP request. The populateHighScores function makes the call. As you can see, jQuery makes it easy by allowing me to specify just the URL and then the name of the JSONP callback as another field (line 18). When it builds the script tag for me, it automatically adds the "?callback=processHighScores" to the end of the URL.

I wrote the high scores application using Node. All of the routes I set up are designed for JSONP callbacks. In each one, the response is built as a string using the callback parameter. One thing to note is that the proper content type for a JSONP response is "application/javascript" not "application/json". It is legitimate JavaScript, pretty much like you'd have in a static .js file. The only difference is that you are generating it dynamically. Here is the server-side route that handles the client request from above:

app.get("/scores", function(req, res, next) {

    var fn = req.query.callback;

    var data = JSON.stringify(scores);

    var js = fn + "(" + data + ");";

    res.setHeader("Content-Type", "application/javascript");
    res.send(js);

});

Dynamically generating JavaScript based on URL parameters in the request is nothing new. I was doing it back in the '90's as part of ASP web applications. But back then I never thought about it as a way to bypass the same-origin policy. I'm not even sure I even knew what the same-origin policy was back then. It was a simpler time...back when I drew these frogs by hand.

Amphibian.com comic for 6 April 2015

No comments:

Post a Comment