Safer JSONP

I read a nice write up on on JSON and JSONP by Angus Croll. He provided a sample implementation of a safe JSONP library.

The library is concise and does the trick for a single connection, but I found it didn’t handle multiple connections. It would overwrite a single global callback, so if you had connections of varying speeds, you would undoubtedly get errors. For instance, “Request 1” is very slow, and “Request 2” gets finished before it. When “Request 1” gets finished, it uses the callback for “Request 2”. This actually happened the first time I loaded it up and ran it. Not good.

Here is a simple updated version that takes care of that particular problem.

/*
An implementation of: http://javascriptweblog.wordpress.com/2010/11/29/json-and-jsonp/
that handles multiple connections at once (don't want to replace the
global callback if second request is sent after the first, but returns before).
*/

(function(global) {

var evalJSONP = function(callback) {
  return function(data) {
    var validJSON = false;
    if (typeof data == "string") {
      try {validJSON = JSON.parse(data);} catch (e) {
        /*invalid JSON*/}
    } else {
      validJSON = JSON.parse(JSON.stringify(data));
      window.console && console.warn(
        'response data was not a JSON string');
    }
    if (validJSON) {
      callback(validJSON);
    } else {
      throw("JSONP call returned invalid or empty JSON");
    }
  }
};

var callbackCounter = ;
global.saferJSONP = function(url, callback) {
  var fn = 'JSONPCallback_' + callbackCounter++;
  global[fn] = evalJSONP(callback);
  url = url.replace('=JSONPCallback', '=' + fn);

  var scriptTag = document.createElement('SCRIPT');
  scriptTag.src = url;
  document.getElementsByTagName('HEAD')[].appendChild(scriptTag);
};

})(this);

To test it out:

var obamaTweets = "http://www.twitter.com/status/user_timeline/BARACKOBAMA.json?count=5&callback=JSONPCallback";
saferJSONP(obamaTweets, function(data) {console.log(data[].text)});

var reddits = "http://www.reddit.com/.json?limit=1&jsonp=JSONPCallback";
saferJSONP(reddits , function(data) {console.log(data.data.children[].data.title)});

If you are really worried about multiple global objects JSONPCallback_1, JSONPCallback_2, etc - you could store them all in an array instead. Like this:

(function(global) {

var evalJSONP = function(callback) {
  return function(data) {
    var validJSON = false;
    if (typeof data == "string") {
      try {validJSON = JSON.parse(data);} catch (e) {
        /*invalid JSON*/}
    } else {
      validJSON = JSON.parse(JSON.stringify(data));
      window.console && console.warn(
        'response data was not a JSON string');
    }
    if (validJSON) {
      callback(validJSON);
    } else {
      throw("JSONP call returned invalid or empty JSON");
    }
  }
};

var callbackCounter = ;
global.JSONPCallbacks = [];
global.saferJSONP = function(url, callback) {
  var count = callbackCounter++;
  global.JSONPCallbacks[count] = evalJSONP(callback);
  url = url.replace('=JSONPCallback', '=JSONPCallbacks[' + count + ']');

  var scriptTag = document.createElement('SCRIPT');
  scriptTag.src = url;
  document.getElementsByTagName('HEAD')[].appendChild(scriptTag);
};

})(this);

Comments

  1. Peter van der Zee Says:

    The only but main problem is that you’re still open for “attacks”. This is because you can’t fix the problem of remote script execution with jsonp as it stands. The proper fix is using CORS, but in that case you’re left to the mercy of the other party. If they won’t allow CORS (but do intend to distribute data through jsonp), your only option is unsafe jsonp. My proposal (at http://qfox.nl/weblog/216 ) tries to address that part. Have the browser fetch the jsonp and return a json to your callback. That’s pretty much the only way to get things safe (assuming json is safe, of course).

  2. Brian Says:

    Peter,
    I agree about still being open for attacks. That is an unfortunate side effect of loading a script from another domain (one you don’t control).

    With your proposal, it seems you are not even adding a script to the page, instead you are having the browsers implement a loadJsonp() function that ignores the same origin policy. That is fine, but I’m not sure you could still call it JSONP at that point. JSONP implies loading a script tag, exploiting the fact that a script doesn’t abide by the same origin policy.

    Your site says that your proposal does not introduce anything new, but it couldn’t be implemented today without new functionality being introduced by browser vendors, right? Moving forward, a proposal like yours or the http://json-p.org will be a great change for the better for security concerns.

Comments are closed. Please contact me instead.