Skip to main content

Usando o Proxy do ES2015 para se divertir e obter resultados

Escrito por
Headshot of Alon Niv

Alon Niv

23 de agosto de 2016

0 minutos de leitura

Muito já foi escrito sobre o ES2015 — com suas funções de seta, declarações de variáveis com escopo e classes controversas. No entanto, um recurso específico recebeu pouca atenção até agora: o Proxy. Como desenvolvedores de JS, não estamos acostumados a depender de mecanismos de interceptação em toda a nossa base de código, mas eles têm várias aplicações muito úteis. Para citar algumas:

  • Testes, mocks e monkeypatching

  • Os padrões de projeto Observer e Visitor

  • Abstrações para conceitos complexos

Até agora, a linguagem não nos oferecia nenhum mecanismo desse tipo. Acho que o Proxy resolve esse problema sem deixar de lado o estilo de JS que conhecemos e adoramos (ou seja, sem métodos [Symbol.__setattr__] nos nossos objetos). Neste post, vou dar um exemplo das interceptações mais comuns (get, set, has e deleteProperty) e, no espírito da Snyk, o assunto será dependências.

Nosso objetivo

Queremos criar um pacote que permita a um desenvolvedor usar require com segurança, limitando o acesso a módulos específicos. Afinal, não queremos confiar o nosso fs a um módulo de utilitários funcionais, certo?

Preparando o terreno

O construtor Proxy aceita 2 parâmetros:

  • target: o objeto que queremos usar como proxy

  • handler: um objeto que contém a especificação das interceptações que queremos tratar (nesse contexto, uma interceptação é uma função chamada quando determinados eventos ocorrem no proxy, como o acesso, a atribuição ou a exclusão de uma propriedade) — veja os exemplos a seguir.

Como o require funciona

Só estou incluindo esta parte porque, embora a documentação do Node explique isso muito bem, vamos fazer algumas alterações engenhosas no cache dos módulos, então quero deixar claro o fluxo do require:

  • O módulo x faz require do módulo y

  • O nome y é resolvido para um caminho absoluto (ou não, caso seja um módulo nativo, como fs)

  • O nome resolvido é buscado no cache, sem verificar se existe

  • Se o valor encontrado estiver definido (um objeto Module), os valores exportados são retornados

  • Caso contrário, o Node busca o arquivo no sistema de arquivos e o compila em um objeto Module, que é armazenado no cache; em seguida, seus valores exportados são retornados.

Esse cache é um singleton global, acessível por meio de require('module')._cache e require.cache, e é um objeto JS simples.

Agora, um pouco de código

Para simplificar, vamos supor que queremos apenas oferecer uma interface de lista de bloqueio para os módulos que não devem ser usados pelo módulo carregado com require. Ela será assim:

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

Beleza, agora já temos uma ideia de como vai ficar. Vamos começar a programar:

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

Legal, terminamos, certo? Bom, não exatamente. Sim, garantimos que nosso módulo mal-intencionado não consegue fazer require de um módulo bloqueado, mas ele ainda pode recorrer a outros truques:

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

Observação sobre require.cache: para corromper um único módulo no cache de módulos, use require.cache. Porém, se quiser substituir todo o mecanismo de cache, use require('module')._cache = .... Vamos ajustar nosso código para lidar com essa situação:

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

E... agora terminamos... certo? Não. Vamos considerar este código mal-intencionado:

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

Vamos dar só os últimos retoques:

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

E agora terminamos. Ou, pelo menos, acho que sim.

Você tem alguma ideia de como contornar esse proxy? Compartilhe com a gente no Twitter @snyksec!

O repositório desta POC está disponível aqui.

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.

Publicado em:

Leia mais

Blog

Verão nos estádios: a turnê Fan Zone do Snyk Connect

A turnê Fan Zone da Snyk levou workshops de segurança de IA, networking e competição amistosa a 8 cidades e 3 sessões virtuais. Os participantes desenvolveram habilidades, trocaram ideias e evoluíram juntos.

Blog

Snyk VulnBench JS 1.0: LLMs conseguem encontrar os mesmos bugs duas vezes?

Snyk VulnBench JS 1.0: 300 varreduras repetidas mostram que as descobertas de segurança dos LLMs variam a cada execução, enquanto SAST e modelos detectam diferentes lacunas de vulnerabilidade.

feature insights context
Blog

Construindo a segurança de IA com nossos clientes: 5 lições do programa de parceiros de design do Evo

Conheça 5 lições importantes do programa de parceiros de design do Evo, da Snyk. Descubra como a descoberta de IA, a inteligência de risco e a automação de políticas ajudam as equipes a proteger a IA generativa e controlar a proliferação de IA em grande escala.