Skip to main content

ES2015 Proxy – zum Spaß und zum Erfolg nutzen

Artikel von
Headshot of Alon Niv

Alon Niv

23. August 2016

0 Min. Lesezeit

Viel 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 Observer und Visitor

  • Abstraktionen 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 x bindet Modul y per require ein.

  • Der Name y wird in einen absoluten Pfad aufgelöst (oder nicht, wenn es sich um ein Kernmodul wie fs handelt).

  • 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:

// our code here, patent pending ;)
const snykwire = require('snykwire');
// don't allow this module access either 'fs' nor 'net' core modules
const nefarious = snykwire('nefarious', ['fs', 'net']);

Gut, jetzt wissen wir ungefähr, wie das aussehen soll. Fangen wir mit dem Programmieren an:

// snykwire/index.js

const Module = require('module'); // we need this to access the global cache

module.exports = (moduleName, blacklist=[]) => {
  // let's make sure we have a set of RESOLVED blacklisted
  // modules we can easily check against.
  const blackSet = new Set(blacklist.map(require.resolve));
  // we're going to save a reference to the "clean" version of the cache,
  // so we can set it right afterwards
  const cache = Module._cache;
  Module._cache = new Proxy(cache, {
    /*
     * As I mentioned before, fetching [resolvedName] from the cache is
     * the first thing attempted, so we can be sure to trap any `require` call
     * here.
     * The `get` trap accepts 2 parameters: `target` (which is the object being
     * proxied - i.e. the cache) and the property being accessed (here it's the
     * resolved name of the module being accessed).
     * If we don't declare this trap, every property accessed will be passed
     * directly to the target, as if there were no proxy at all.
     */    get(target, resolvedName) {
      if (blackSet.has(resolvedName)) {
        // we could return a dummy module here, but it's easier to just throw an error for now
        throw new Error(
          `Module '${moduleName}' has attempted to access module '${resolvedName}'`
        );
      }
      // else, just act natural
      return target[resolvedName];
    }
  });
  try {
    // let's see if we can require the module now... ^-*_*-^
    return require(moduleName);
  } finally {
    // and... let's put things back where they belong
    Module._cache = cache;
  }
};

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:

// nefarious/index.js

// require('fs').writeFileSync('/etc/passwd', 'muhahaha');
// Drat! Foiled! Let's try something else...

require.cache.fs = {
  exports: {
    readFile() {
      process.exit(1); // muhahaha!
    }
  }
}

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:

// ...
Module._cache = new Proxy(cache, {
  get(target, resolvedName) {
    if (blackSet.has(resolvedName)) {
      throw new Error(
        `Module '${moduleName}' has attempted to access module '${resolvedName}'`
      );
    }
    return target[resolvedName];
  },
  /*
   * The `set` trap accepts 3 parameters: `target` (which is the object being
   * proxied - i.e. the cache), the property being set (here it's the
   * resolved name of the module being accessed) and the actual Module object.
   * If we don't declare this trap, every property set will be set
   * directly to the target, as if there were no proxy at all.
   */  set(target, resolvedName, mod) {
    if (blackSet.has(resolvedName)) {
      throw new Error(
        `Module '${moduleName}' has attempted to corrupt module '${resolvedName}'`
      );
    }
    target[resolvedName] = mod;
    // the `set` trap has to return `true` if it succeeded.
    // Returning a falsy value will throw a `TypeError`.
    return true;
  }
// ...

Und … jetzt sind wir fertig … oder? Nein. Sehen wir uns diesen bösartigen Code an:

// nefarious/index.js

if ('fs' in require.cache) {
  delete require.cache.fs;
  // if I can't use it, nobody can!
  // (as long as they run in strict mode...)
  Object.freeze(require.cache);
}

Fügen wir noch ein paar letzte Details hinzu:

// ...
Module._cache = new Proxy(cache, {
  get(target, resolvedName) {
    throwIfForbidden(blackSet, resolvedName);
    return target[resolvedName];
  },
  set(target, resolvedName, mod) {
    throwIfForbidden(blackSet, resolvedName);
    target[resolvedName] = mod;
    return true;
  },
  // This traps `resolvedName in proxy`
  has(target, resolvedName) {
    throwIfForbidden(blackSet, resolvedName);
    return resolvedName in target;
  },
  // This traps `delete proxy[resolvedName]`
  deleteProperty(target, resolvedName) {
    throwIfForbidden(blackSet, resolvedName);
    delete target[resolvedName];
    // like with the `set` trap, return `true` on success
    return true;
  },
  // Traps Object.preventExtensions, and by extension, Object.freeze
  preventExtensions(target) {
    // Let's not let anyone mess with our cache
    throw new Error(`Module '${moduleName}' tried to lock your module cache`);
  }
// ...

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.

Gepostet in:

Weiterlesen

Blog

Stadium-Sommer: Die Snyk Connect Fan Zone Tour

Die Fan Zone Tour von Snyk brachte KI-Security-Workshops, Networking und freundschaftlichen Wettbewerb in 8 Städte und 3 virtuelle Sessions. Die Teilnehmenden bauten ihre Kompetenzen aus, tauschten Ideen aus und entwickelten sich gemeinsam weiter.

Blog

Snyk VulnBench JS 1.0: Finden LLMs zweimal dieselben Fehler?

Snyk VulnBench JS 1.0: 300 wiederholte Scans zeigen, dass LLM-Sicherheitsergebnisse von Durchlauf zu Durchlauf variieren, während SAST und Modelle unterschiedliche Sicherheitslücken aufdecken.

feature insights context
Blog

KI-Sicherheit gemeinsam mit unseren Kunden gestalten: 5 Erkenntnisse aus dem Design-Partner-Programm von Evo

Lernen Sie 5 wichtige Erkenntnisse aus Snyks Design-Partner-Programm für Evo kennen. Erfahren Sie, wie KI-Erkennung, Risikoanalyse und automatisierte Richtlinien Teams dabei helfen, generative KI abzusichern und die Ausbreitung von KI-Anwendungen im großen Maßstab zu steuern.