Hackfox – Foxhacks

Viele von denen, die hier lesen dürften mein Projekt EdgeMonkey kennen. Für die paar, die es nicht tun: es handelt sich um ein Userscript für Greasemonkey, welches die Entwickler-Ecke verschönert.
Jedenfalls: viele der dort eingesetzten Techniken waren schon immer Bleeding Edge, was Userscripts angeht. Deswegen wirds da auch schnell mal blutig, wenn sich irgendwo etwas ändert.

So ist das bereits mehrmals passiert – die neue Sandbox der Greasemonkey 0.8-Reihe war wohl das offensichtlichste. Aber das war ja noch einfach zu umgehen. Richtig spaßig ist das aber erst viel später auf Firefox 4 geworden, welcher mich auch dazu gebracht hat die hier besprochene Thematik mal genauer zu analysieren.

Wie alles anfing…
Grundsätzlich bestehen die Foren der Entwickler-Ecke meistens aus mehreren Frames. Für viele User ist dabei mindestens die Shoutbox ein IFRAME, zusätzlich existiert auf den „Beitrag beantworten“-Seiten eine Kurzansicht der letzten paar Beiträge („Topicreview“). Dieser Aufbau stellt Userscripts vor einige Hürden, werden sie doch für jede Seite separat ausgeführt – ab GM 0.8 eben sogar in einer getrennten Sandbox, die verhindern soll dass sich Webseiten über den GM Chrome-Privilegien holen können. Man möchte aber an einigen Stellen, dass die Scripte zusammenarbeiten können und Funktionen sowie Daten gemeinsam nutzen.

Erster Ansatz
De Grundidee war nun, dass das zuerst geladene Frame (meistens wohl das Hauptfenster) eine Variable EM in das window injeziert. Später geladene Frames suchen in ihrem window.parent dann danach und weisen dieses Objekt einer eigenen Variable zu:

if (SOP_ok && !isEmpty(unsafeWindow.parent.EM)) {
  window.EM = unsafeWindow.parent.EM;
  unsafeWindow.EM = EM;
} else {
  window.EM = {};
  unsafeWindow.EM = EM;
}

Dies muss man natürlich immer zweimal tun: einmal für das window aus der Sandbox und einmal für das externe unsafeWindow.

Diese Variante hat gut funktioniert, bis Firefox 4 kam. Dieser erzeugt bei der genutzten Funktion evalInSandbox noch einen weiteren Kontext, so dass window nicht mehr das globale Objekt ist. Damit ändert sich das Verhalten des Codes signifikant, so dass nach ausführung des oberen Blocks

var foo= EM;

nicht funktionieren wird – EM ist undefined, window.EM wäre es nicht.

Damit war die bisherige Technologie ein Fall für gründliches Überdenken. Mit dem netten Nebeneffekt, dass gleich noch ein weiterer Fehler gefunden wurde. Leider ein konzeptioneller, denn diese Methode hat ziemliche Probleme damit, dass das Topicreview auch eine Pagehacks-Instanz hat. Diese überschreibt dann die der Hauptseite. Nicht gut das ist. Transparenter du denken musst, junger Padawan!

How it should have worked…
Dann ist mir etwas eingefallen, was ich vor der Lektüre von Crockford’s The Good Parts zwar wusste, aber nicht wirklich angewendet hatte: JavaScripts Prototypische Vererbung. In a nutshell bedeutet diese, dass ein Objekt zuerst nachsieht, ob es eine Eigenschaft selbst enthält, dann bei seinem Protoypten, dann bei dessen Prototypen etc., bis Object() dann keinen mehr hat.
Dies kann man nutzen, um bestimmte Eigenschaften global zu machen und andere Privat zu halten. Praktischerweise lässt einen die Mozilla-JS-Engine auch an das dafür verwendete __proto__-Pseudoproperty ran, so dass man schreiben kann (nicht wörtlich so, aber die Idee kommt so besser rüber als im Orignalcode, welcher noch Code enthält um auf Fertigstellung zu warten):

var newEM = {};
if (SOP_ok && !isEmpty(unsafeWindow.parent.EM)) {
  newEM.__proto__ = unsafeWindow.parent.EM.__proto__;
} else {
  newEM.__proto__ = {};
}
window.EM = newEM;
unsafeWindow.EM = newEM;

Dann gilt für jedes Frame:

EM.__proto__.foo=42; // ist danach überall sichtbar
EM.bar=23; // nur dieses Frame hat die Variable. Andere könnten EM.bar auch als 5 deklarieren!

Womit das Ziel erreicht wäre: es ist möglich, Felder zwischen Frames zu teilen. Es ist aber auch möglich, private Felder zu deklarieren.
Nun funktioniert dieser Ansatz sehr schön direkt im Browser, als ClientSide-Script. Aber als Greasemonkey-Script… nunja, ich weiß nicht was da passiert, aber es ist etwas anderes. Wer möchte, kann sich das im Proof Of Concept ansehen.

… and what we did instead
Nun ist das Problem nicht ganz unbekannt, weswegen mal jemand den Content Scope Runner erfunden hat. Keine Rocket Surgery, aber schon soweit genial dass einfach der Code ins DOM injiziert wird und damit im Content Scope läuft. Nett, aber wir brauchen ja die Chrome-Privilegien die uns GM gibt.

Die Lösung ist nun, beide Techniken zu kombinieren: Die Objekt-Verlinkung passiert im Content Scope, der Rest des Scripts läuft wie gewohnt als Userscript. Dies erfordert etwas mehr Aufwand, da die jeweiligen Warteschleifen natürlich auch mit gebaut werden müssen.

Der Loader an sich wird relativ einfach, da alles in 2 Unterfunktionen gemacht wird. Env wird vorher mit Informationen über die Umgebung gefüllt, Env.parentName enthält dann Bezeichner wie „window.parent“.

  if (Env.isSOPPass) {
    injectInitCode(Env.parentName);
    waitForObject(false);
  } else {
    injectInitCode();
    waitForObject(true);
  }

injectInitCode tut nun genau das: einen Code generieren, der bei Ausführung ein neues EM-Objekt erstellt und entweder mit leerem __proto__ oder dem __proto__ des übergebenen Objektes verknüpft. Sollte etwas übergeben worden sein, muss natürlich gewartet werden, bis das Objekt dann auch verfügbar ist. Dies passiert im Conent Scope, damit funktioniert der Code!

  function injectInitCode(p) {
    var code='';
    code+='(function(par){';
    code+=' var newEM = {};';
    code+=' if (par) {';
    code+='  var wait=setTimeout(function() {';
    code+='   if (typeof par.EM!=="undefined") {';
    code+='    clearInterval(wait);';
    code+='    newEM.__proto__ = par.EM.__proto__;';
    code+='    window.EM = newEM;';
    code+='   }';
    code+='  }, 10);';
    code+=' } else {';
    code+='  newEM.__proto__ = {};';
    code+='  window.EM = newEM;';
    code+=' } ';
    code+='})('+p+')';
 
    var script=document.createElement('script');
    script.setAttribute('type','text/javascript');
    script.innerHTML = code;
    document.documentElement.appendChild(script);
    document.documentElement.removeChild(script);
  }

Nun will aber der Rest des Scripts sein gemütliches Chrome Scope nicht verlassen… also muss die weitere Initialisierung so lange verzögert werden, biss das Content-Script unser Objekt verknotet hat – danach können wir nach belieben drauf operieren. Auch hier wieder die übliche setInterval-Methode, die nach wenigen (naja, so schnell ist Firefox ja nicht…) Versuchen ein EM-Objekt im unsafeWindow vorfindet und nach ein paar Scoping-Würgarounds dieses überall bekannt gemacht hat. Wenn das der Fall ist, können noch schnell globale Objekte erstellt werden (hier nur symbolhaft) und die eigentliche, seitenspezifische Initialisierung kann stattfinden. Wie sich vermuten lässt, passiert dies in startup(). Die ist dann auch so uninteressant und total by-the-book, dass sie hier nicht mehr Gegenstand ist.

  function waitForObject(buildglobals) {
    var wait=setInterval(function() {
      if (typeof unsafeWindow.EM!== "undefined") {
        clearInterval(wait);
        window.EM = unsafeWindow.EM;
        EM = window.EM;  // Für Fx4
        if (buildglobals) {
          EM.__proto__.Ajax = new AJAXObject();
        }
        //awesome, we're done
        startup(EM);
      }
    }, 20);
  }

Mein Fazit
Also zuerstmal: meine Aussage steht noch, dass man mit JS wirklich alles lösen kann. Glück gehabt 😉
Was lernen wir aus dem Ganzen: man sollte immer mal Out Of The Box denken, gerade wenn man eine Sandbox verlassen will. Um auf diese Idee zu kommen brauchte es immerhin einen Bug-Eintrag bei Greasemonkey. Ich bin immer noch überzeugt, dass das Verhalten von GM hier ein Bug ist. Wenn auch keiner der Sorte „geht nicht“, sonder „geht anders nicht als es nicht gehen sollte“.

Wichtigste Erkenntnis dürfte aber sein, dass man es mit Security nicht übertreiben sollte. Nicht immer, wenn etwas sicherer wird, wird es auch besser. Ohne die extrem fiese Sandbox müsste ich nichts Injecten und das Window hätte es nicht so leicht, rauszukriegen dass hier ein Edgemonkey am Werk ist. Dies sollte man wohl bei jeder Art Software-Entwicklung bedenken.

One thought on “Hackfox – Foxhacks

  1. Insbesondere ist das Verhalten interessant, wenn man bei GreaseMonkey unter FrickelFox 4 inzwischen 3 verschiedene Kontexte für Variablen zu beachten hat und man jedes Mal auf’s Neue von den Upstream-Patches die nötigen Instanzen kopieren darf.

    Wer ist bitteschön so krank, und führt neben einem window, ein unsafeWindow ein und brauch dann noch einen Default-Kontext (üblich = window) auch noch was ganz anderes als Default?

    Hatte ja gestern schon scherzhaft vorgeschlagen, wir veröffentlichen die GM-API im unsafeWindow und sparen uns dann die Mühen mit dem Content Scope Runner 😉

    Naja, zumindest geht der Affe ja jetzt im FrickelFox 4.

    Gruß,
    BenBE.

Comments are closed.