Using ES2015 Proxy for fun and profit

Datetime:2016-08-22 21:33:08          Topic: ECMAScript           Share

Much has been written about ES2015 - with its arrow functions, scoped variable declarations and controversial classes. However, a certain feature has received little love so far: the Proxy .

As JS developers, we’re not used to relying on trapping mechanisms throughout our codebase, but they have several very useful applications. To name a few:

  • Testing, mocking and monkeypatching
  • The Observer and Visitor design patterns
  • Abstractions over complicated concepts

Until now, the language hasn’t provided us with any such mechanism. I feel like Proxy solves this problem, while keeping the feel of the JS we know and love (i.e. no [Symbol.__setattr__] methods for our objects).

In this post, I’ll give an example for the most common traps ( get , set , has and deleteProperty ), and in keeping with the Snyk spirit, it’s going to be about dependencies.

Our goal

We want to create a package that allows a developer to safely require modules, limiting their access to specific modules. After all, we wouldn’t want to trust a functional utilities module with our fs , right?

Setting the stage

The Proxy constructor accepts 2 parameters:

  • target : this is the Object we want to proxy around
  • handler : this is an Object containing the spec for the traps we want to handle (a trap, in this sense, is a function that is being called on certain events happening to the proxy, such as a property being accessed, set or deleted) - examples follow.

How require works

I’m only including this part because, while node’s docs do a pretty great job explaining this, we’re about to do some nifty things to the modules cache, so I want to make sure the require flow is clear:

  • Module x require s module y
  • Name y is resolved to an absolute path (or not, if it’s a core module, such as fs )
  • The resolved name is fetched from the cache, without checking for its existence
  • If the fetched value is defined (a Module object), the exported values are returned
  • Otherwise, some magic happens node actually fetches the file from the FS, and compiles it into a Module object, which is put into the cache, and has its exported values returned.

This cache is a global singleton, accessible via require('module')._cache and require.cache , and is a plain JS Object.

Finally, some code

For simplicity’s sake, let’s assume we just want to provide a blacklisting interface for modules that shouldn’t be used by our required module. It’s going to look like this:

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

Okay, so now we have a feel for what it’s going to look like, let’s start coding:

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

Cool, we’re done, right? Well, not exactly. Yes, we made sure our nefarious module can’t require a blacklisted module, but there are other dirty tricks it can pull off:

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

Side-note about require.cache : if you want to corrupt a single module in the module cache, use require.cache . If, however, you want to switch out the entire caching mechanism, use require('module')._cache = ... .

Let’s fix our code to handle this 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;
  }
// ...

And… now we’re done… right? Nope. Let’s consider this nefarious code:

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

Let’s just add some last touches, then:

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

And now we’re done. Or, at least, I think so.

Do you have an idea how to circumvent this proxy? Share them with us on Twitter @snyksec !

The repo for this POC is available here

Got any questions about this blog post? Let us know on Twitter at @snyksec .





About List