ES2015 Proxy – zum Spaß und zum Erfolg nutzen
Alon Niv
23. August 2016
0 Min. LesezeitViel wurde über ES2015 geschrieben – über seine Arrow-Funktionen, bereichsgebundenen Variablendeklarationen und umstrittenen Klassen. Ein bestimmtes Feature hat bisher jedoch wenig Beachtung gefunden: Proxy. Als JS-Entwickler sind wir es nicht gewohnt, uns in unserer gesamten Codebasis auf Trap-Mechanismen zu verlassen, doch sie bieten einige sehr nützliche Einsatzmöglichkeiten. Hier nur einige Beispiele:
Testen, Mocking und Monkey-Patching
Die Entwurfsmuster
ObserverundVisitorAbstraktionen für komplexe Konzepte
Bislang stellte uns die Sprache keinen solchen Mechanismus zur Verfügung. Ich finde, Proxy löst dieses Problem und bewahrt dabei den Charakter des JS, das wir kennen und lieben (also keine [Symbol.__setattr__]-Methoden für unsere Objekte). In diesem Beitrag zeige ich ein Beispiel für die gängigsten Traps (get, set, has und deleteProperty). Ganz im Sinne von Snyk geht es dabei um Abhängigkeiten.
Unser Ziel
Wir möchten ein Package erstellen, mit dem Entwickler Module sicher per require einbinden und ihren Zugriff auf bestimmte Module beschränken können. Schließlich möchten wir einem Modul mit funktionalen Hilfsfunktionen nicht unser fs anvertrauen, oder?
Die Ausgangslage
Der Konstruktor von Proxy akzeptiert zwei Parameter:
target: Das ist das Objekt, für das wir einen Proxy erstellen möchten.handler: Das ist ein Objekt mit der Spezifikation für die Traps, die wir behandeln möchten. Eine Trap ist in diesem Zusammenhang eine Funktion, die bei bestimmten Ereignissen für den Proxy aufgerufen wird, etwa wenn auf eine Eigenschaft zugegriffen, sie gesetzt oder gelöscht wird. Beispiele folgen.
So funktioniert require
Ich nehme diesen Abschnitt nur auf, weil die Dokumentation von Node zwar sehr gut erklärt, wie das funktioniert, wir aber gleich einige raffinierte Änderungen am Modul-Cache vornehmen. Deshalb möchte ich sicherstellen, dass der Ablauf von require klar ist:
Modul
xbindet Modulyperrequireein.Der Name
ywird in einen absoluten Pfad aufgelöst (oder nicht, wenn es sich um ein Kernmodul wiefshandelt).Der aufgelöste Name wird aus dem Cache abgerufen, ohne seine Existenz zu überprüfen.
Ist der abgerufene Wert definiert (ein Module-Objekt), werden die exportierten Werte zurückgegeben.
Andernfalls ruft Node die Datei tatsächlich aus dem Dateisystem ab und kompiliert sie zu einem Module-Objekt. Dieses wird im Cache abgelegt und seine exportierten Werte werden zurückgegeben.
Dieser Cache ist ein globales Singleton, das über require('module')._cache und require.cache zugänglich ist, und ein einfaches JS-Objekt.
Und nun etwas Code
Der Einfachheit halber nehmen wir an, dass wir lediglich eine Blacklist-Schnittstelle für Module bereitstellen möchten, die von unserem eingebundenen Modul nicht verwendet werden sollen. Sie sieht so aus:
Gut, jetzt wissen wir ungefähr, wie das aussehen soll. Fangen wir mit dem Programmieren an:
Super, fertig, oder? Nicht ganz. Wir haben zwar sichergestellt, dass unser bösartiges Modul kein auf der Blacklist stehendes Modul per require einbinden kann, aber es kann noch andere schmutzige Tricks anwenden:
Hinweis zu require.cache: Wenn Sie ein einzelnes Modul im Modul-Cache manipulieren möchten, verwenden Sie require.cache. Wenn Sie dagegen den gesamten Caching-Mechanismus austauschen möchten, verwenden Sie require('module')._cache = .... Passen wir unseren Code so an, dass er auch diesen Fall behandelt:
Und … jetzt sind wir fertig … oder? Nein. Sehen wir uns diesen bösartigen Code an:
Fügen wir noch ein paar letzte Details hinzu:
Und jetzt sind wir fertig. Oder zumindest glaube ich das.
Haben Sie eine Idee, wie sich dieser Proxy umgehen lässt? Teilen Sie sie mit uns auf Twitter unter @snyksec!
Das Repository für diesen POC finden Sie hier.
Starten Sie mit Capture the Flag
Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.


