Skip to main content

Utiliser Proxy d’ES2015 pour s’amuser et réussir

Écrit par
Headshot of Alon Niv

Alon Niv

23 août 2016

0 minutes de lecture

Beaucoup a été écrit sur ES2015 : ses fonctions fléchées, ses déclarations de variables à portée limitée et ses classes controversées. Pourtant, une fonctionnalité n’a pas encore reçu beaucoup d’attention : Proxy.En tant que développeurs JS, nous n’avons pas l’habitude de nous appuyer sur des mécanismes d’interception dans toute notre base de code, mais ils ont plusieurs applications très utiles. En voici quelques exemples :

  • Tests, simulation et monkey patching

  • Les modèles de conception Observer et Visitor

  • Des abstractions pour des concepts complexes

Jusqu’à présent, le langage ne nous offrait aucun mécanisme de ce type.J’ai l’impression que Proxy résout ce problème tout en préservant les habitudes de JS que nous connaissons et apprécions (c’est-à-dire sans méthodes [Symbol.__setattr__] pour nos objets).Dans cet article, je vais présenter un exemple des pièges les plus courants (get, set, has et deleteProperty), et, dans l’esprit de Snyk, il sera question de dépendances.

Notre objectif

Nous voulons créer un package qui permet à un développeur de require des modules en toute sécurité, en limitant leur accès à des modules spécifiques. Après tout, nous ne voudrions pas confier notre fs à un module d’utilitaires fonctionnels, n’est-ce pas ?

Posons le décor

Le constructeur Proxy accepte 2 paramètres :

  • target : l’objet que nous voulons placer derrière un proxy

  • handler : un objet contenant la spécification des pièges que nous voulons gérer (un piège, dans ce contexte, est une fonction appelée lors de certains événements affectant le proxy, par exemple l’accès à une propriété, sa modification ou sa suppression) — la suite en donne des exemples.

Fonctionnement de require

J’inclus cette partie uniquement parce que, même si la documentation de node explique très bien le sujet, nous allons manipuler le cache des modules de façon astucieuse. Je veux donc m’assurer que le fonctionnement de require est clair :

  • Le module x require le module y

  • Le nom y est résolu en chemin absolu (ou pas, s’il s’agit d’un module natif comme fs)

  • Le nom résolu est récupéré dans le cache, sans vérifier son existence

  • Si la valeur récupérée est définie (un objet Module), les valeurs exportées sont renvoyées

  • Sinon, node récupère le fichier depuis le système de fichiers et le compile en un objet Module, qui est placé dans le cache et dont les valeurs exportées sont renvoyées.

Ce cache est un singleton global, accessible via require('module')._cache et require.cache, et c’est un simple objet JS.

Enfin, passons au code

Pour simplifier, supposons que nous voulons simplement fournir une interface de liste de blocage pour les modules que le module requis ne doit pas utiliser. Voici à quoi elle va ressembler :

// 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']);

Bien, nous avons maintenant une idée de ce à quoi cela va ressembler. Commençons à coder :

// 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, c’est terminé, n’est-ce pas ?Pas tout à fait. Certes, nous avons veillé à ce que notre module malveillant ne puisse pas require un module inscrit sur la liste de blocage, mais il peut encore recourir à d’autres sales astuces :

// 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!
    }
  }
}

Remarque sur require.cache : pour corrompre un seul module dans le cache des modules, utilisez require.cache.En revanche, pour remplacer tout le mécanisme de mise en cache, utilisez require('module')._cache = ....Modifions notre code pour gérer cette situation :

// ...
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;
  }
// ...

Et… maintenant, c’est terminé… non ? Eh bien non. Examinons ce code malveillant :

// 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);
}

Ajoutons quelques dernières retouches :

// ...
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`);
  }
// ...

Et maintenant, c’est terminé. Enfin, je crois.

Avez-vous une idée pour contourner ce proxy ? Partagez-la avec nous sur Twitter @snyksec !

Le dépôt de cette preuve de concept est disponible ici.

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.

Publié dans:

Lire la suite

Blog

Stadium Summer : la tournée Fan Zone de Snyk Connect

La tournée Fan Zone de Snyk a proposé des ateliers sur la sécurité de l’IA, des occasions de réseauter et une compétition amicale dans 8 villes et lors de 3 sessions virtuelles. Les participants ont développé leurs compétences, échangé des idées et progressé ensemble.

Blog

Snyk VulnBench JS 1.0 : les LLM peuvent-ils détecter deux fois les mêmes bugs ?

Snyk VulnBench JS 1.0 : 300 analyses répétées montrent que les résultats de sécurité des LLM varient d’une exécution à l’autre, tandis que SAST et modèles détectent différentes vulnérabilités.

feature insights context
Blog

Construire la sécurité de l’IA avec nos clients : 5 leçons du programme de partenaires de conception d’Evo

Découvrez 5 leçons clés tirées du programme de partenaires de conception d’Evo de Snyk. Découvrez comment la découverte de l’IA, le renseignement sur les risques et l’automatisation des politiques aident les équipes à sécuriser l’IA générative et à maîtriser la prolifération de l’IA à grande échelle.