Skip to main content

Usar ES2015 Proxy para divertirse y obtener beneficios

Escrito por
Headshot of Alon Niv

Alon Niv

23 de agosto de 2016

0 minutos de lectura

Mucho se ha escrito sobre ES2015, con sus funciones flecha, declaraciones de variables con ámbito y sus polémicas clases. Sin embargo, una función no ha recibido mucha atención hasta ahora: Proxy.Como desarrolladores de JS, no estamos acostumbrados a depender de mecanismos de interceptación en todo nuestro código, pero tienen varias aplicaciones muy útiles. Por nombrar algunas:

  • Pruebas, simulaciones y monkey patching

  • Los patrones de diseño Observer y Visitor

  • Abstracciones para conceptos complejos

Hasta ahora, el lenguaje no nos había proporcionado ningún mecanismo de este tipo.Considero que Proxy resuelve este problema, sin perder la esencia del JS que conocemos y nos encanta (es decir, sin métodos [Symbol.__setattr__] para nuestros objetos).En esta publicación, daré un ejemplo de las trampas más comunes (get, set, has y deleteProperty) y, siguiendo el espíritu de Snyk, hablaremos de dependencias.

Nuestro objetivo

Queremos crear un paquete que permita a los desarrolladores usar require de forma segura, limitando el acceso a módulos específicos. Al fin y al cabo, no querríamos confiarle nuestro fs a un módulo de utilidades funcionales, ¿verdad?

Preparar el terreno

El constructor de Proxy acepta 2 parámetros:

  • target: es el objeto que queremos convertir en proxy

  • handler: es un objeto que contiene la especificación de las trampas que queremos manejar (en este sentido, una trampa es una función que se llama cuando se producen ciertos eventos relacionados con el proxy, como acceder, establecer o eliminar una propiedad); a continuación, veremos algunos ejemplos.

Cómo funciona require

Solo incluyo esta parte porque, aunque la documentación de node lo explica muy bien, estamos por hacer cosas ingeniosas con la caché de módulos y quiero asegurarme de que el flujo de require quede claro:

  • El módulo x require el módulo y

  • El nombre y se resuelve como una ruta absoluta (o no, si es un módulo integrado, como fs)

  • El nombre resuelto se obtiene de la caché, sin comprobar si existe

  • Si el valor obtenido está definido (es un objeto Module), se devuelven los valores exportados

  • De lo contrario, node obtiene el archivo del sistema de archivos, lo compila en un objeto Module, lo guarda en la caché y devuelve sus valores exportados.

Esta caché es un singleton global, accesible mediante require('module')._cache y require.cache, y es un objeto JS simple.

Por fin, algo de código

Para simplificar, supongamos que solo queremos ofrecer una interfaz de lista de bloqueo para los módulos que no debería usar el módulo requerido. Se vería así:

// 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, ya tenemos una idea de cómo se verá. Empecemos a escribir el código:

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

Genial, ya terminamos, ¿verdad?Bueno, no exactamente. Sí, nos aseguramos de que nuestro módulo malicioso no pueda usar require para acceder a un módulo bloqueado, pero puede recurrir a otros trucos sucios:

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

Nota al margen sobre require.cache: si quieres corromper un módulo específico en la caché de módulos, usa require.cache.Sin embargo, si quieres reemplazar todo el mecanismo de almacenamiento en caché, usa require('module')._cache = ....Modifiquemos el código para manejar esta situación:

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

Y… ahora sí terminamos… ¿verdad? No. Veamos este código malicioso:

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

Agreguemos unos últimos detalles:

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

Y ahora sí terminamos. O al menos eso creo.

¿Se te ocurre cómo evadir este proxy? ¡Compártelo con nosotros en Twitter @snyksec!

El repositorio de esta prueba de concepto está disponible aquí.

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.

Publicado en:

Leer más

Blog

Verano en los estadios: la gira Snyk Connect Fan Zone

La gira Fan Zone de Snyk llevó talleres de seguridad de IA, networking y competencia amistosa a 8 ciudades y 3 sesiones virtuales. Los asistentes desarrollaron habilidades, compartieron ideas y crecieron juntos.

Blog

Snyk VulnBench JS 1.0: ¿Pueden los LLM encontrar los mismos errores dos veces?

Snyk VulnBench JS 1.0: 300 análisis repetidos muestran que los hallazgos de seguridad de los LLM varían en cada ejecución, mientras que SAST y los modelos detectan distintas vulnerabilidades.

feature insights context
Blog

Cómo construir seguridad de IA junto con nuestros clientes: 5 lecciones del programa de socios de diseño de Evo

Conoce 5 lecciones clave del programa de socios de diseño de Evo de Snyk. Descubre cómo el descubrimiento de IA, la inteligencia de riesgos y la automatización de políticas ayudan a los equipos a proteger la IA generativa y controlar la proliferación de IA a gran escala.